📜 ⬆️ ⬇️

Sharing shared assemblies between processes and domains in IIS

In the microservice world, adding new functionality is done by writing a new service. At the same time, the cost of adding a new unit is at least 150 MB of RAM, although our code in it is rather small and, as a rule, the same assemblies are used with slight differences in versions.


This article will show how to optimize solely due to server settings, so rewriting and recompiling applications is not required. The result will be 25 MB on average for one microservice.


2. The structure of the memory process


As a first step, you should determine what the process memory is occupied with, and whether this optimization is significant in a particular case. To analyze the structure of the memory, we need one application from Sysinternals - VMMap .


Let's open VMMap and consider the structure of a specific w3wp process where our site is hosted. At the top of the application, we see 3 horizontal charts:
Committed - memory “available” for the process.
Private Bytes - virtual memory
Working Set - physical memory (RAM)



The colors in the diagrams denote different types of memory; we will cite only some of them, which make up the top 4 for the Working Set:


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


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


Total WS - amount of physical memory
Private WS - the amount of physical memory that cannot be shared with other processes.
Sharable WS - the amount of physical memory that can be used in conjunction with other processes
Shared WS is the amount of physical memory currently used in conjunction with other processes.


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


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


It should be further noted that the memory allocated JIT is included in the Managed Heap. It can be calculated as the size of the Managed Head minus the size of the 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 an OS. Inside 47 micro services running IIS.


Each microservice has a controller with a method that returns the version of the assembly (see the example below). That is what we will call to “warm up” sites.


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

Now we will consistently launch and “warm up” our sites to see the big picture. To automate this process, here’s a script on PowerShell (link to github).
As a result, 47 sites were launched in 6 minutes 43 seconds and occupied all the RAM of the server. The average size of one microservice was 7 GB / 47 = 152 MB (1 GB per OS).




3. Sharing inter-domain assemblies in a single application, the concept of a domain neutral assembly


Now consider the w3wp process through the prism of a ProcessExplorer . Going to the tab. NET Assemblies, we will see 3 so-called. application domains: sharedDomain, defaultDomain and siteDomain (/ LM / W3SVC / 3 /). The latter is true, however, only when we create our own application pool for each site.


What changes will follow if we combine several sites into one application pool ? N appDomains with the names / LM / W3SVC / 3 / ... will be added - siteDomain , where N is the number of added sites.


Now let's note that some of the assemblies are in sharedDomain , and some of the assemblies in appDomains . In this case, the assemblies that are located in sharedDomain are present in the application in a single instance, and the assemblies that are located siteDomains will be loaded independently for each domain.


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


MSDN [3] gives the following definition of “Domain Neutral Assembly”:



In the book “Pro .NET Performance: Optimize Your C #“ [4], it is recommended to place signed assemblies (strong name assemblies) in the GAC, otherwise loading the assembly will require its full reading to confirm its digital signature.
The latter also facilitates the creation of native images for all applications that reference this assembly.


Therefore, to facilitate the size of the application, we will need to place the signed assemblies in the GAC and group the sites in applicationPools by their load profile.


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


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


 cd C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\ 

for help on all possible options run


 aspnet_intern.exe /? 

To get a list of all the assemblies without actually interning them, let's execute


 aspnet_intern -mode analyze -sourcedir "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files" > C:\internReport.txt 

* for a 32-bit application, use the path: C: \ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ Temporary ASP.NET Files
directly for interning the assemblies to the C: \ ASPNETCommonAssemblies directory, execute the following command


 aspnet_intern -mode exec -sourcedir "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files" -interndir C:\ASPNETCommonAssemblies 

PowerShell Example


 $intern_path = 'C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\aspnet_intern.exe' $intern_param = '-mode', 'exec', '-sourcedir', 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files', '-interndir', 'C:\ASPNETCommonAssemblies' & $intern_path $intern_param 

To upload the assemblies to the GAC, we will need to again refer to the Windows SDK, but already for the gacutil.exe application. After internment, we received a directory containing signed and unsigned assemblies.
Since it is possible to install into GAC only those that are signed, we will need to write a small Powershell script to install them:


 $asm_path = 'C:\git\IISSharingAssemblies\common-assemblies-legacy' $gac_path = 'C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\gacutil.exe' #install assembly to GAC Get-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 will conduct an experiment with the integration of sites in applicationPools manually, although the latter can be done using PowerShell through the WebAdministration module [7].
In the specific case, we will combine 47 microservices into 6 applicationPools according to their load profile. To backup, restore or transfer the configuration to other machines, I recommend to pay attention to appcmd.exe [7], [8]


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

After the done 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 RAM used 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.




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


In addition to sharing between assemblies between domains in a single process, sharing between different processes is possible, but the latter requires creating a native image and putting it in a cache. For this purpose we will use ngen.exe [9].


We list the advantages of using native images:



Creating native images is possible for both signed and unsigned assemblies. However, there are nuances: if the assembly is loaded not in the sharedDomain, then it will not be able to be shared with other appDomains.


 $asm_path = 'C:\git\IISSharingAssemblies\common-assemblies-legacy' $ngn_path = 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ngen.exe' #install native images from cache Get-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 stage we will have to repeat all previous steps and execute one new one:



As a result, we will see the following picture:





5. Results


StepTotal warm-up timeThe total size of the used RAMThe average size of one microserviceNote
Initial conditions6 min 43 sec8 GB7 GB / 47 = 152 MB1 GB on OS. Native image cache not used by IIS
Combining sites into appPools, uploading assemblies to the GAC2 min 33 sec4.1 GB3.1 GB / 47 = 67 MB1 GB on OS. Native image cache not used by IIS
Combining sites into appPools, loading assemblies into GAC, creating native images2 min 12 sec2.2 GB1.2 Gb / 47 = 26.1 Mb1 GB on OS. IIS uses 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


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

')

Source: https://habr.com/ru/post/341950/


All Articles