Thread management in asp.net core / kestrel

Matt Roberts picture Matt Roberts · Nov 16, 2017 · Viewed 10.8k times · Source

I'm troubleshooting performance / scalability issues with an asp.net app we've migrated to asp.net core 2.0. Our app is hosted on azure as an app service, and is falling over far too easily with any moderate traffic.

One thing that's puzzling me is how multiple concurrent requests are handled. From what I've read here, Kestrel uses multiple event loops to handle your requests. But the actual user code is handled on the .net thread pool (that's from here).

So, as an experiment - I've created a new asp.net core 2.0 MVC app, and added a rather nasty action method:

    [AllowAnonymous]
    public ActionResult Wait1()
    {
        System.Threading.Tasks.Task.Delay(1000).Wait();
        return new StatusCodeResult((int)HttpStatusCode.OK);
    }     

Now, when I push this to azure, I'd expect that if I send say 100 requests at the same time, then I should be OK, because 100 requests sounds like minor load, right? And the waiting will happen on the thread pool threads, right?

So - I do just this and get some rather poor results - sample highlighted in red:

enter image description here

Hmm, not what I expected, about 50 seconds per request... If however I change the frequency so the requests are spaced a second apart, then the response time is fine - back to just over 1000ms as you'd expect. It seems if I go over 30 requests at the same time, it starts to suffer, which seems somewhat low to me.

So - I realise that my nasty action method blocks, but I'd have expected it to block on a thread pool thread, and therefore be able to cope with more than my 30.

Is this expected behaviour - and if so is it just a case of making sure that no IO-bound work is done without using async code?

Answer

Tom Sun - MSFT picture Tom Sun - MSFT · Nov 17, 2017

Is this expected behaviour - and if so is it just a case of making sure that no IO-bound work is done without using async code?

Based on my experience, it seems that is as expected behaviour. We could get answer from this blog.

Now suppose you are running your ASP.Net application on IIS and your web server has a total of four CPUs. Assume that at any given point in time, there are 100 requests to be processed. By default the runtime would create four threads, which would be available to service the first four requests. Because no additional threads will be added until 500 milliseconds have elapsed, the other 96 requests will have to wait in the queue. After 500 milliseconds have passed, a new thread is created.

As you can see, it will take 100*500ms intervals to catch up with the workload.

This is a good reason for using asynchronous programming. With async programming, threads aren’t blocked while requests are being handled, so the four threads would be freed up almost immediately.

I recommand that you could use async code to improve the performance.

public async Task<ActionResult> Wait1()
{
    await Task.Delay(TimeSpan.FromSeconds(15));
    return new StatusCodeResult((int)HttpStatusCode.OK);
}

I also find another SO thread, you could refernce to it.