Home .NET Sharing common assemblies between processes and domains in IIS

Sharing common assemblies between processes and domains in IIS

by admin

In the microservice world, adding new functionality is done by writing a new service.The cost of adding a new unit is a minimum of 150 MB of RAM, even though our code is quite small and generally uses the same assemblies with minor versioning differences.

This article will show how to optimize solely through server configuration, so rewriting and recompiling applications will not be necessary. There will be a result of 25 MB on average per microservice.

2. Process memory structure

The first step is to find out what is occupied by the process memory and if this optimization is relevant in this case. To analyze the memory structure we need one application from Sysinternals VMMap

Open VMMap and let’s look at the structure of the particular w3wp process where our site is hosted. At the top of the application we see 3 horizontal diagrams :
Committed – memory "available" to the process
Private Bytes – virtual memory
Working Set – physical memory (RAM)

Sharing common assemblies between processes and domains in IIS

The colors in the charts represent different types of memory, here are just a few of the top 4 for Working Set:

Image – executable files, such as .exe or .dll, which can be loaded into the image loader process
Managed Heap – memory allocated by the .NET runtime, usually containing application data
Page Table is an area of memory responsible for mapping virtual addresses to physical addresses
Heap, a memory area allocated by the C or C++ runtime, usually containing the application data.

For each memory type you can get detailed information about how and where it is allocated :

Total WS – amount of physical memory
Private WS – amount of physical memory which cannot be shared with other processes
Sharable WS – amount of physical memory which can be shared with other processes
Shared WS – amount of physical memory currently shared with other processes.

For a detailed description of the VMMap application, see references [1], [2]. But let’s go back to our w3wp process and consider the top 3 by memory type :

73 of 220 MB goes to Image
60 of 220 mb to Heap
58 of 220 MB to Managed Heap

It should be additionally noted that the Managed Heap includes the memory alocated by JIT. It can be calculated as the size of Managed Head minus the size of GC.

2.1 Initial conditions

We have an Amazon t2.large instance with 8 Gb of RAM, 2 Intel Xeon ES-2676 v3 2.40GHz cores and Windows Server 2012 R2 as the OS. Inside 47 microservices running IIS.

Every microservice has a controller with a method that returns the build version (see example below). This is what we will call to "warm up" the sites.

public class VersionController : ApiController{[Route("version")][HttpGet]public IHttpActionResult Version(){return Ok(Assembly.GetExecutingAssembly().GetName().Version);}}

Now let’s run and "warm up" our sites one by one to see the whole picture. To automate this process, here is a PowerShell script (link on github).
The result is that 47 sites started in 6 minutes and 43 seconds and took all the RAM of the server. The average size per microservice was 7 GB / 47 = 152 MB (1 GB per OS).

Sharing common assemblies between processes and domains in IIS

Sharing common assemblies between processes and domains in IIS

3. Sharing assemblies between domains in the same application, the concept of domain neutral assembly

Now consider the w3wp process through the prism of ProcessExplorer Clicking on the .NET Assemblies tab, we will see 3 so-called. Application domains: sharedDomain, defaultDomain, and siteDomain (/LM/W3SVC/3/). The latter is only true when we create a separate application poolfor each site.
Sharing common assemblies between processes and domains in IIS

What changes will follow if we merge several sites into one application pool ? Will be added N appDomains named /LM/W3SVC/3/… – siteDomain where N is the number of sites added.

Now notice that some of the assemblies are in sharedDomain and some of the builds are in appDomains The assemblies that are in sharedDomain are present in the application in a single instance, and the assemblies that are siteDomains will be loaded independently for each domain.

It should be noted that the assemblies that are in sharedDomain have a DomainNeutral flag and a path that points either to the GAC (Global Assembly Cache) or to the native images cache.

MSDN [3] defines "Domain Neutral Assembly" as follows:

  • An assembly that exists in a single instance and is shared between appDomains within a single process
  • An assembly that Jitted once and shares common data structures with another appDomain : MethodTables, MethodDescs
  • An assembly can be Domain Neutral if it and all its dependencies are placed in the GAC (only signed assemblies can be placed in the GAC)

The book "Pro .NET Performance: Optimize Your C# " [4] recommends putting signed assemblies (strong name assemblies) in the GAC, otherwise loading an assembly will require reading it completely to confirm its digital signature.
The latter also makes it easier to create native images for all the applications referencing this assembly.

Consequently, to lighten the size of the application, we need to put the signed assemblies in the GAC and group the sites into applicationPools according to their load profile.

To solve the first problem there is a console application aspnet_intern.exe that comes with the Windows SDK.
The application analyzes the used assemblies and then copies them into the specified directory, replacing the original file with a symbolic link, which saves disk space and speeds up the w3wp process [5].

Example :
Open a command prompt in administrator mode and go to the Windows SDK directory

cd C:Program Files (x86)Microsoft SDKsWindowsv10.0AbinNETFX 4.6 Tools

to get help about all possible options let’s execute

aspnet_intern.exe /?

to get a list of all assemblies without actually interning them, run

aspnet_intern -mode analyze -sourcedir "C:WindowsMicrosoft.NETFramework64v4.0.30319Temporary ASP.NET Files" > C:internReport.txt

*For 32-bit application use path : C:WindowsMicrosoft.NETFrameworkv4.0.30319Temporary ASP.NET Files
To intern the assemblies directly to the directory C:ASPNETCommonAssemblies we execute the following command

aspnet_intern -mode exec -sourcedir "C:WindowsMicrosoft.NETFramework64v4.0.30319Temporary ASP.NET Files" -interndir C:ASPNETCommonAssemblies

PowerShell example

$intern_path = 'C:Program Files (x86)Microsoft SDKsWindowsv10.0AbinNETFX 4.6 Toolsaspnet_intern.exe'$intern_param = '-mode', 'exec', '-sourcedir', 'C:WindowsMicrosoft.NETFramework64v4.0.30319Temporary ASP.NET Files', '-interndir', 'C:ASPNETCommonAssemblies' $intern_path $intern_param

To load assemblies in the GAC, we again need to turn to the Windows SDK, but for the application gacutil.exe After interning we got a directory containing signed and unsigned assemblies.
Since only the signed ones can be installed into the GAC, we need to write a little Powershell script to install them :

$asm_path = 'C:gitIISSharingAssembliescommon-assemblies-legacy'$gac_path = 'C:Program Files (x86)Microsoft SDKsWindowsv10.0AbinNETFX 4.6 Toolsgacutil.exe'#install assembly to GACGet-ChildItem -recurse $asm_path | where {$_.extension -eq ".dll"} | ForEach-Object {Write-Host "Try to install assembly $_" $gac_path "/i", $_.FullName}#uninstall assembly from GAC#Get-ChildItem $asm_path | where {$_.extension -eq ".dll"} | ForEach-Object {# $gac_path "/u", ([System.Reflection.AssemblyName]::GetAssemblyName($_.FullName).FullName)#}

We’ll experiment with combining sites into applicationPools manually, although the latter can be done using PowerShell via the WebAdministration module [7].
In this particular case, we will merge 47 microservices into 6 applicationPools according to their load profile. For backups, restore or configuration transfer to other machines, I recommend to pay attention to appcmd.exe [7], [8]

#clean all sitescmd.exe /c "%windir%system32inetsrvappcmd.exe list site /xml | %windir%system32inetsrvappcmd delete site /in"#cleam all poolscmd.exe /c "%windir%system32inetsrvappcmd.exe list apppool /xml | %windir%system32inetsrvappcmd delete apppool /in"#To Export the Application Pools on IIS 7 :#cmd.exe /c "%windir%system32inetsrvappcmd list apppool /config /xml > c:apppools.xml"#To Export all you’re website:#cmd.exe /c "%windir%system32inetsrvappcmd list site /config /xml > c:sites.xml"#To import the Application Pools:cmd.exe /c "%windir%system32inetsrvappcmd add apppool /in < c:apppools.xml"#Stop all Application Pools:cmd.exe /c "%windir%system32inetsrvappcmd.exe list apppool /xml | %windir%system32inetsrvappcmd stop apppool /in"#To Import the website:cmd.exe /c "%windir%system32inetsrvappcmd add site /in < c:sites.xml"

After the changes we have the following picture: 47 sites "warmed up in 2 minutes and 33 seconds, which is 2.6 times faster. The total size of the used RAM was 4.1 GB. At the same time, the average size of one microservice was 3.1 GB / 47 = 67 MB, which is 2.2 times less.

Sharing common assemblies between processes and domains in IIS

Sharing common assemblies between processes and domains in IIS

4. Sharing assemblies between different applications, the concept of native image

Besides the sharing of assemblies between domains in one process, it is possible to share assemblies between different processes, but the latter requires creating a native image and caching it. For this purpose we will use ngen.exe [9].

Let’s list the benefits of using native images :

  • can be shared between processes
  • can be shared between domains within one process
  • use less RAM, because they do not require JIT compilation
  • Load faster, since they do not require JIT compilation and type-safety verification

Creating native images is possible for both signed and unsigned assemblies. However, this is a bit tricky: if an assembly is not uploaded to a sharedDomain, it will not be able to be shared with other appDomains.

$asm_path = 'C:gitIISSharingAssembliescommon-assemblies-legacy'$ngn_path = 'C:WindowsMicrosoft.NETFramework64v4.0.30319ngen.exe'#install native images from cacheGet-ChildItem -Recurse $asm_path | where {$_.extension -eq ".dll"} | ForEach-Object { $ngn_path "install", ([System.Reflection.AssemblyName]::GetAssemblyName($_.FullName).FullName)}#uninstall native images from cache#Get-ChildItem $asm_path | where {$_.extension -eq ".dll"} | ForEach-Object {# $ngn_path "uninstall", ([System.Reflection.AssemblyName]::GetAssemblyName($_.FullName).FullName)#}

At this point we have to repeat all the previous steps and do one new one :

  • Loads signed assemblies into the GAC
  • merging sites in applicationPools
  • Create native images for GAC-loaded assemblies

As a result, we will see the following picture :

  • total "warm-up"time 2 minutes 12 seconds
  • Total size of used RAM 2.2 Gb
  • average size of one microservice 1.2 GB / 47 = 26.1 MB

Sharing common assemblies between processes and domains in IIS

Sharing common assemblies between processes and domains in IIS

5. Results

Step Total "warm-upquot time; Total size of used RAM Average size of one microservice Note
Initial conditions 6 min 43 sec 8 GB 7 GB / 47 = 152 MB 1 GB per OS. The native image cache is not used by IIS
Merge sites into appPools, load builds into GAC 2 min 33 sec 4.1 GB 3.1 GB / 47 = 67 MB 1 GB per OS. The native image cache is not used by IIS
Merge sites in appPools, upload builds to GAC, create native images 2 min 12 sec 2.2 GB 1.2 GB / 47 = 26.1 Mb 1 GB per OS. IIS uses the native image cache

Links

  1. Windows Sysinternals Administrator’s Reference. Pages 216-218
  2. http://blogs.microsoft.co.il/sasha/2016/01/05/windows-process-memory-usage-demystified/
  3. https://blogs.msdn.microsoft.com/junfeng/2004/08/05/domain-neutral-assemblies/
  4. Pro .NET Performance: Optimize Your C# Applications. Page 289
  5. Introduction .NET 4.5 Alex Mackey, William Stewart Tulloch, Mahesh Krishnan. Page 149
  6. https://technet.microsoft.com/ru-ru/library/ee790599.aspx
  7. http://www.microsoftpro.nl/2011/01/27/exporting-and-importing-sites-and-app-pools-from-iis-7-and-7-5/
  8. https://technet.microsoft.com/en-us/library/ea8d442e-9a0c-49bb-b940-50b22fa64dd4
  9. https://docs.microsoft.com/en-us/dotnet/framework/tools/ngen-exe-native-image-generator

Source code

  1. https://github.com/sflusov/IISSharingAssemblies

You may also like