Each time a request for a .NET resource (.aspx, page, .ascx control, etc.) comes in a thread is grabbed from the available worker thread pool in the asp.net worker process (aspnet_wp.exe on IIS 5.x, w3wp.exe on IIS 6/7) and is assigned to a request. That thread is not released back into the thread pool until the final page has been rendered to the client and the request is complete.
Inside the ASP.Net Worker Process there are two thread pools. The worker thread pool handles all incoming requests and the I/O Thread pool handles the I/O (accessing the file system, web services and databases, etc.). But how many threads are there in these thread pools? I had assumed that the number of threads would vary from machine to machine – that ASP.NET and IIS would carefully balance the number of available threads against available hardware, but that is simply not the case. ASP.Net installs with a fixed, default number of threads to play with: The CLR for the 1.x Framework sets these defaults at just 20 worker threads and 20 I/O thread per CPU. Now this can be increased by modifying the machine.config, but if you are not aware of this, then 20 threads is all you’re playing with. If you have multiple sites sharing the same worker process, then they are all sharing this same thread pool.
So long as the number of concurrent requests does not exceed the number of threads available in the pool, all is well. But when you are building enterprise level applications the thread pool can become depleted under heavy load, and remember by default heavy load is more than just 20 simultaneous requests. When this happens, new requests are entered into the request queue (and the users making the requests watch an hour glass spin). ASP.NET will allow the request queue to grow only so big before it starts to reject requests at which point it starts returning Error 503, Service Unavailable.
If you are not aware of this “Glass Ceiling of Scalability”, this is a perplexing error – one that never happened in testing and may not reproducible in your test environment, as it only happens under extreme load.
So the first thing you can do to improve scalability is to raise the values. The defaults for the ASP.NET 2.0 are 100 threads in each pool per CPU and the defaults for the ASP.NET 3.x CLR is 250 per CPU for worker threads and 1000 per CPU for I/O threads, however you can tune it further using the guidelines below. 32 bit windows can handle about 1400 concurrent threads, 64 bit windows can handle more, though I don’t have the figures.
You can tune ASP.NET thread pool using maxWorkerThreads, minWorkerThreads, maxIoThreads, minFreeThreads, minLocalRequestFreeThreads and maxconnection attributes in your Machine.config file. Here are the default settings.
<add address="*" maxconnection="2" />
<httpRuntime minFreeThreads="8" minLocalRequestFreeThreads="4" />
<processModel maxWorkerThreads="100" maxIoThreads="100" />
Here is the formula to reduce contention. Apply the recommended changes that are described below, across the settings and not in isolation.
- Configuration setting Default value (.NET Framework 1.1) Recommended value
- maxconnection 2 12 * #CPUs
- maxIoThreads 20 100
- maxWorkerThreads 20 100
- minFreeThreads 8 88 * #CPUs
- minLocalRequestFreeThreads 4 76 * #CPUs
- Set maxconnection to 12 * # of CPUs. This setting controls the maximum number of outgoing HTTP connections that you can initiate from a client. In this case, ASP.NET is the client. Set maxconnection to 12 * # of CPUs.
- Set maxIoThreads to 100. This setting controls the maximum number of I/O threads in the .NET thread pool. This number is automatically multiplied by the number of available CPUs. Set maxloThreads to 100.
- Set maxWorkerThreads to 100. This setting controls the maximum number of worker threads in the thread pool. This number is then automatically multiplied by the number of available CPUs. * Set maxWorkerThreads to 100.
- Set minFreeThreads to 88 * # of CPUs. This setting is used by the worker process to queue all the incoming requests if the number of available threads in the thread pool falls below the value for this setting. This setting effectively limits the number of requests that can run concurrently to maxWorkerThreads – minFreeThreads. Set minFreeThreads to 88 * # of CPUs. This limits the number of concurrent requests to 12 (assuming maxWorkerThreads is 100).
- Set minLocalRequestFreeThreads to 76 * # of CPUs. This setting is used by the worker process to queue requests from localhost (where a Web application sends requests to a local Web service) if the number of available threads in the thread pool falls below this number. This setting is similar to minFreeThreads but it only applies to localhost requests from the local computer. Set minLocalRequestFreeThreads to 76 * # of CPUs.
Note The recommendations that are provided in this section are not rules. They are a starting point. Test to determine the appropriate settings for your scenario. If you move your application to a new computer, ensure that you recalculate and reconfigure the settings based on the number of CPUs in the new computer.
By raising these values, you raise the “glass ceiling of scalability”, and in many cases that may be all you need to do, but what happens when you start getting more than 250 simultaneous requests? To make optimum use of the thread pool all IO requests that you know could take a second or more to process should be made asynchronously. More information on Asynchronous programming in ASP.NET is coming soon.
I recommend testing your new machine.config locally or virtually somewhere first because if you make a mistake - for example you paste this in to the wrong area, or there is already a <system.web> element and you paste in a second one - ALL websites on the box will stop functioning as ASP.Net will not be able to parse the machine.config!!!