Profiling NserviceBus Transaction Options

I set out to write a new post on NServiceBus and ended up on an awesome journey of profiling the behavior when you turn transactions off. This post dives into what I found out.

Transactions and NServiceBus

In an article I have coming up on scaling NServiceBus with Routing Slips, one of the things to consider is what you can scale UP before you scale OUT. Scaling up typically involves maximizing hardware and processing power. With NServiceBus, an additional option you have available is to disable transactional overhead if you don’t need it.

NServiceBus primarily uses transactions to provide durability to message handling. By that I mean, if you throw an exception in one of your handlers, you can rely on NServiceBus to handle and re-queue any messages as needed. Unfortunately it’s not a perfrect world and transactions add overhead to your operations.

In some scenarios this is a must have. In others, you may not need it - hell, maybe you’re even okay with losing a message here and there. What I’ll dive into here is the impact that disabling these features has on throughput. If you’re planning on going this route, you should take a moment to read through a great post covering what actually happens under the covers.

Sample Application and Environment

Our sample application will be a widget builder. Our widget in simply a record in a SQL Server database table. It only has 1 field, the primary key, which we will be setting in our handler.

What are we using?

  • NPoco
  • NServiceBus host version 6 // Core version 5
  • Rustflakes - a lightweight ordered ID generator

What are we running on?

  • Windows 8 VM (using Parallels) on Macbook Pro 2014
  • CPU: 2.49 GHZ
  • 2 Cores
  • 4.4GB Memory
  • SQL Server 2014 hosted locally

Benchmark

Benchmarks are collected while processing the queue to empty over 5 consecutive runs. The focus will be on two factors - the maximum value encountered along with the average of the runs. {“Our goal is to measure throughput.”} Values are collected in 4 different scenarios:

  1. Transactions enabled
  2. Distributed Transactions disabled (DTC)
  3. DTC and Transaction Scope disabled
  4. Transactions disabled completely

Endpoint Config

Each scenario will show the adjustments to the EndpointConfig depending on the benchmark. In addition to that the number of worker threads has been fixed for all benchmarcks.

<TransportConfig MaximumConcurrencyLevel="2" MaxRetries="2" MaximumMessageThroughputPerSecond="0"/>

Startup Config

When the service first starts, we’ll configure any existing data we want along with initializing the test. For this test, I insert a million records to give us a starting point rather than an empty table.

public class Startup : IWantToRunWhenBusStartsAndStops
{
    public void Start()
    {  
        using (var db = new Database("connstr"))
        {
            var pocos = new List<Widget>();
            db.Execute("DELETE FROM Widgets");

            for (var o = 0; o < 100; o++)
            {
                for (var i = 0; i < 10000; i++)
                {
                    pocos.Add(new Widget() { Id = KeyGen.NewKey });
                }
                    
                db.InsertBulk(pocos);
                pocos.Clear();
            }
        }

        Timer = new Stopwatch();
        Timer.Start();

        for (var i = 0; i < 10000; i++)
            Bus.SendLocal(new BuildWidgetCommand());

        Bus.SendLocal(new BuildWidgetCommand() { Tracer = true });
    }
}

Handler

The benchmark handler is very simple - just giving us an idea of how fast it can process a new widget.

public class BuildWidgetHandler : IHandleMessages<BuildWidgetCommand>
{
    public void Handle(BuildWidgetCommand message)
    {
        using (var db = new Database("connstr"))
        {
            var widget = new Widget() { Id = KeyGen.NewKey };
            db.Insert("Widgets", "id", false, widget);
        }

        if (!message.Tracer) return;

        Startup.Timer.Stop();
        LogManager.GetLogger(GetType()).InfoFormat("Elapsed seconds: {0}", Startup.Timer.Elapsed.TotalSeconds);
    }
}

Benchmark Results

Included with the results is a graph of “# of msgs sucessfull processed / sec”. Since there were different throughput behaviors observed, I thought this would be useful to include.

Transactions Enabled

Avg Elapsed Seconds: 300.8
Avg Msg/s: 31.2
Avg Max/s: 78

Run Elapsed Seconds Avg Msg/s Maximum Msg/s Graph
1 285 33 79
2 293 32 79
3 324 29 78
4 304 31 79
5 298 31 75



Distributed Transactions (DTC) Disabled

configuration.Transactions().DisableDistributedTransactions();

Avg Elapsed Seconds: 174.6
Avg Msg/s: 51
Avg Max/s: 129.6

Run Elapsed Seconds Avg Msg/s Maximum Msg/s Graph
1 186 49 135
2 174 53 132
3 184 49 125
4 156 56 131
5 173 48 125



DTC and Transaction Scope Disabled

configuration.Transactions().DisableDistributedTransactions();
configuration.Transactions().DoNotWrapHandlersExecutionInATransactionScope();

Avg Elapsed Seconds: 180
Avg Msg/s: 50
Avg Max/s: 131.8

Run Elapsed Seconds Avg Msg/s Maximum Msg/s Graph
1 163 55 129
2 166 51 133
3 189 48 137
4 179 51 132
5 203 45 128



###Transactions Completely Disabled Considder this the “nuclear” option. You’re disabling any safe-guards for message loss in the event that something goes wrong.

The only time you can risk loosing messages are when you turn transactions off, Configure.Transactions.Disable(). This means that we pull the message off the queue without either a DTC-aware TX or a native MSMQ tx. This means that any exception in the handler pipeline will result in message loss. - andreas.ohlund

configuration.Transactions().Disable();

Avg Elapsed Seconds: 79.6
Avg Msg/s: 97.4
Avg Max/s: 289.2

Run Elapsed Seconds Avg Msg/s Maximum Msg/s Graph
1 72 117 285
2 84 81 329
3 79 94 278
4 78 105 303
5 85 90 251

Summary

At the end of the day, there is not much of a performance difference between disabling just DTC or disabling both DTC and the wrapped transaction scope. All other factors aside, you can pick up somewhere in the neighborhood of 50% more throughput by chosing one of these options.

Source Code

If you want to try this out yourself, the soruce code can be found here