To fully understand the problem and the partial solution we found, I'll review how an application in IIS and the .NET CLR is logically structured.
Global Assembly Cache (GAC)
The GAC is a cache of Common Language Infrastructure assemblies available be shared by multiple applications. Assemblies loaded from the GAC are loaded with Full Trust, note that in .NET 4 Code Access Security (CAS) has changed significantly and permissions are determined largely by the permissions of the executing account.
Application Pools
Application Pools (App Pools) are grouped sets of Web Applications under IIS that share the same W3WP worker process. Each application in an app pool runs under the same service account and as a result with the same account permissions. Application pools can be viewed as a mechanism for creating isolation between applications in IIS. Often sys admins create a separate application pool for each web application. Generally you'll have a single worker process for an application in IIS though you can configure multiple; this is refereed to as a Web Garden. Just a Note: Generally you want to avoid Web Gardens as they can create all sorts of problems for you. Their purpose is intended to be supporting long running requests without creating a blocking situation for your application.
Thread
Within a worker process, one or more threads, configurable in IIS, are available. Threads are like the workers on an assembly line with App Domains being the different stations for a worker to do their work. A thread does all the work inside of an application and they can only work on one App Domain at any given time. A thread can however work for multiple domains over the life cycle of the application bouncing back and forth between as work needs to be done.
AppDomain
An application domain is a unit of isolation within the .NET framework. Every application can have one or more application domains. By default, every application has 1 AppDomain however you can create AppDomains within your code to manage assembly dependencies. Within an AppDomain, there are three contexts for assemblies that are loaded within that domain. These contexts are a further isolation unit within an AppDomain into which assemblies are loaded based on the method of loading.
Logical Runtime Structure of IIS Hosted Websites |
The GAC is a cache of Common Language Infrastructure assemblies available be shared by multiple applications. Assemblies loaded from the GAC are loaded with Full Trust, note that in .NET 4 Code Access Security (CAS) has changed significantly and permissions are determined largely by the permissions of the executing account.
Application Pools
Application Pools (App Pools) are grouped sets of Web Applications under IIS that share the same W3WP worker process. Each application in an app pool runs under the same service account and as a result with the same account permissions. Application pools can be viewed as a mechanism for creating isolation between applications in IIS. Often sys admins create a separate application pool for each web application. Generally you'll have a single worker process for an application in IIS though you can configure multiple; this is refereed to as a Web Garden. Just a Note: Generally you want to avoid Web Gardens as they can create all sorts of problems for you. Their purpose is intended to be supporting long running requests without creating a blocking situation for your application.
Thread
Within a worker process, one or more threads, configurable in IIS, are available. Threads are like the workers on an assembly line with App Domains being the different stations for a worker to do their work. A thread does all the work inside of an application and they can only work on one App Domain at any given time. A thread can however work for multiple domains over the life cycle of the application bouncing back and forth between as work needs to be done.
AppDomain
An application domain is a unit of isolation within the .NET framework. Every application can have one or more application domains. By default, every application has 1 AppDomain however you can create AppDomains within your code to manage assembly dependencies. Within an AppDomain, there are three contexts for assemblies that are loaded within that domain. These contexts are a further isolation unit within an AppDomain into which assemblies are loaded based on the method of loading.
- default-load context
- Assemblies resolved by the CLR from the Global Assembly Cache (GAC) or the private application bin.
- load-from
- Assemblies loaded from an application's code using the Assembly.LoadFrom method.
- reflection-only
- Assemblies loaded only for reflection.
Now with all that background information out of the way, we found that placing those large assemblies I mentioned above, along with all of their references in the GAC caused the CLR to load them as domain-neutral and thus share the same assemblies in memory across applications in the same application pool. This reduced our memory footprint per application pool somewhat and with several application pools running several applications made an impact to resource demands on our servers.
So what you might be asking, the moral of this story and the reason I decided to blog about it is that all too often we as developers or systems administrators don't pay enough attention to how our applications are deployed and configured. We take for granted all the things that the .NET framework does for us and our application performance and users suffer because of it. Now, deploying assemblies in the GAC isn't what i'm recommending for every solution and we didn't end up putting all of our application assemblies in the GAC. You should however take some of the information i've provided above, research a bit for yourself and evaluate your application to optimize it's use of resources which can translate to a big performance impact for your end users.
Reference Links: