В микросервисном мире добавление новой функциональности осуществляется путем написания нового сервиса. При этом стоимость добавления новой единицы составляет минимум 150 Мб оперативной памяти, хотя нашего кода в нем достаточно мало и используются, как правило, одни и те же сборки с небольшими отличиями в версиях.
В этой статье будут показаны пути оптимизации исключительно за счет настроек сервера, таким образом переписывание и перекомпиляция приложений не потребуется. Будет достигнут результат 25 Мб в среднем на один микросервис.
2. Структура памяти процесса
В качестве первого шага следует определить, чем же занята память процесса, и значима ли данная оптимизация в конкретном случае. Для анализа структуры памяти нам понадобится одно приложение от Sysinternals — VMMap.
Откроем VMMap и рассмотрим структуру конкретного w3wp процесса, где хостится наш сайт. В верхней части приложения мы видим 3 горизонтальные диаграммы:
Committed — память “доступная” для процесса
Private Bytes — виртуальная память
Working Set — физическая память (RAM)
Цветами в диаграммах обозначены различные типы памяти, приведем лишь некоторые из них, составляющие топ 4 для Working Set:
Image — исполняемые файлы, такие как .exe или .dll, которые могут быть загружены в процесс image loader
Managed Heap — память выделенная .NET runtime, обычно содержащая данные приложения
Page Table — область памяти, отвечающая за отображение виртуальных адресов в физические
Heap — память выделенная С или C++ runtime, обычно содержащая данные приложения
Для каждого типа памяти можно получить детальную информацию о том, как и где он выделен:
Total WS — кол-во физической памяти
Private WS — кол-во физической памяти, которое не может быть использовано совместно (shared) с другими процессами
Sharable WS — кол-во физической памяти, которое может быть использовано совместно с другими процессами
Shared WS — кол-во физической памяти, которое в настоящее время используется совместно с другими процессами.
Подробное описание приложения VMMap см. по ссылкам [1], [2]. Но вернемся к нашему w3wp процессу и рассмотрим топ 3 по типам памяти:
73 из 220 Мб отходит к Image
60 из 220 мб к Heap
58 из 220 Мб к Managed Heap
Следует дополнительно отметить, что в Managed Heap включается память алоцированная JIT. Ее можно вычислить как размер Managed Head минус размер GC.
2.1 Начальные условия
В нашем распоряжении имеется инстанс Amazon t2.large с 8 Gb оперативной памяти, 2 ядрами Intel Xeon ES-2676 v3 2.40GHz и Windows Server 2012 R2 в качестве ОС. Внутри 47 микро-сервисов под управлением IIS.
В каждом микросервисе есть контроллер с методом, возвращающим версию сборки (см. пример ниже). Именно его мы будем вызывать для “прогрева” сайтов.
public class VersionController : ApiController
{
[Route("version")]
[HttpGet]
public IHttpActionResult Version()
{
return Ok(Assembly.GetExecutingAssembly().GetName().Version);
}
}
Теперь последовательно запустим и “прогреем” наши сайты, чтобы увидеть картину в целом. Для автоматизации этого процесса приведем скрипт на PowerShell (ссылка на github).
В результате 47 сайтов запустились за 6 минут 43 секунды и заняли всю оперативную память сервера. Средний размер одного микросервиса составил 7 Гб / 47 = 152 Мб (1 Гб на ОС).
3. Sharing сборок между доменами в одном приложении, понятие домен нейтральной сборки (domain neutral assembly)
Теперь рассмотрим w3wp процесс через призму ProcessExplorer. Перейдя на вкладку .NET Assemblies, мы увидим 3 т.н. application domains: sharedDomain, defaultDomain и siteDomain (/LM/W3SVC/3/). Последнее справедливо, впрочем, только когда мы создаем для каждого сайта свой application pool.

Какие изменения последуют в случае, если мы объединим несколько сайтов в один application pool? Будут добавлены N appDomains с именами /LM/W3SVC/3/… — siteDomain, где N число добавленных сайтов.
Теперь обратим внимание, что часть сборок находятся в sharedDomain, а часть сборок в appDomains. При этом сборки находящиеся в sharedDomain присутствуют в приложении в единственном экземпляре, а сборки находящиеся siteDomains будут загружаться независимо для каждого домена.
Следует отметить, что сборки находящиеся в sharedDomain имеют флаг DomainNeutral и путь, который указывает либо в GAC (Global Assembly Cache), либо в кэш нативных образов (native images).
MSDN [3] дает следующее определение “Domain Neutral Assembly”:
- сборка, которая существует в единственном экземпляре и разделяется между appDomains в рамках одного процесса
- сборка, которая Jitted единожды и разделяет с другим appDomain общие структуры данных: MethodTables, MethodDescs
- сборка может быть Domain Neutral, если она и все её зависимости помещены в GAC (только подписанные сборки могут быть помещены в GAC)
В книге “Pro .NET Performance: Optimize Your C# “ [4] рекомендуется помещать подписанные сборки (strong name assemblies) в GAC, в противном случае загрузка сборки потребует её полного чтения для подтверждения ее цифровой подписи.
Последнее также облегчает создание нативных образов для всех приложений, ссылающихся на эту сборку.
Следовательн�� для облегчения размера приложения нам потребуется поместить подписанные сборки в GAC и сгруппировать сайты в applicationPools по их профилю нагрузки.
Для решения первой задачи существует консольное приложение aspnet_intern.exe, поставляемое вместе с Windows SDK.
Приложение анализирует используемые сборки и далее копирует их в указанный каталог, при этом заменяя исходный файл на символическую ссылку, чем экономит место на диске и ускоряет запуск w3wp процесса [5].
Пример:
откроем командную строку в режиме администратора и перейдем в каталог Windows SDK
cd C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\для получения справки о всех возможных опциях выполним
aspnet_intern.exe /?для получения списка всех сборок без их фактического интернирования выполним
aspnet_intern -mode analyze -sourcedir "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files" > C:\internReport.txt*для 32 битного приложения используйте путь: C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files
непосредственно для интернирования сборок к каталог C:\ASPNETCommonAssemblies выполним следующую команду
aspnet_intern -mode exec -sourcedir "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files" -interndir C:\ASPNETCommonAssembliesПример на PowerShell
$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Для загрузки сборок в GAC нам вновь потребуется обратиться к Windows SDK, но уже за приложением gacutil.exe. После интернирования мы получили кат��лог, содержащий подписанные и неподписанные сборки.
Так как инсталлировать в GAC можно только те из них, которые подписаны, то нам потребуется написать небольшой скрипт на Powershell для их инсталляции:
$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)
#}Проводить эксперимент с объединением сайтов в applicationPools будем вручную, хотя последнее возможно сделать средствами PowerShell через модуль WebAdministration [7].
В конкретном случае мы будем объединять 47 микросервисов в 6 applicationPools по их профилю нагрузки. Для бэкапа, восстановления или переноса конфигурации на другие машины, рекомендую обратить внимание на 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"После проделанных изменений мы имеем следующую картину: 47 сайтов "прогрелись" за 2 минуты и 33 секунды, что быстрее в 2.6 раза. Общий размер используемой оперативной памяти составил 4.1 Гб. При этом средний размер одного микросервиса составил 3.1 Гб / 47 = 67 Мб, что меньше в 2.2 раза.
4. Sharing сборок между различными приложениями, понятие нативного образа (native image)
Помимо совместного использования (sharing) сборок между доменами в рамках одного процесса возможно совместное использование сборок между различными процессами, но последнее требует создания нативного образа и помещения его в кэш. Для этой цели мы будем использовать ngen.exe [9].
Перечислим плюсы от использования нативных образов:
- могут использоваться совместно между процессами
- могут использоваться совместно между доменами в рамках одного процесса
- используют меньше оперативной памяти, поскольку не требуют JIT компиляции
- загружаются быстрее, поскольку не требует JIT компиляции и type-safety верификации
Создание нативных образов возможно как для подписанных, так и для неподписанных сборок. Однако здесь есть нюансы: если сборка будет загружена не в sharedDomain, то она не будет иметь возможности совместно использоваться с другими 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)
#}На данном этапе нам придется повторить все предыдущие шаги и выполнить один новый:
- загрузка подписанных сборок в GAC
- объединение сайтов в applicationPools
- создание нативных образов для загруженных в GAC сборок
В результате мы увидим следующую картину:
- общее время "прогрева" 2 минуты 12 сек
- общий размер используемой оперативной памяти 2.2 Гб
- средний размер одного микросервиса 1.2 Гб / 47 = 26.1 Мб
5. Результаты
| Шаг | Общее время "прогрева" | Общий размер используемой оперативной памяти | Средний размер одного микросервиса | Примечание |
|---|---|---|---|---|
| Начальные условия | 6 мин 43 сек | 8 Гб | 7 Гб / 47 = 152 Мб | 1 Гб на ОС. Кэш нативных образов не используется IIS |
| Объединение сайтов в appPools, загрузка сборок в GAC | 2 мин 33 сек | 4.1 Гб | 3.1 Гб / 47 = 67 Мб | 1 Гб на ОС. Кэш нативных образов не используется IIS |
| Объединение сайтов в appPools, загрузка сборок в GAC, создание нативных образов | 2 мин 12 сек | 2.2 Гб | 1.2 Гб / 47 = 26.1 Мб | 1 Гб на ОС. IIS использует кэш нативных образов |
Ссылки
- Windows Sysinternals Administrator’s Reference. Pages 216-218
- http://blogs.microsoft.co.il/sasha/2016/01/05/windows-process-memory-usage-demystified/
- https://blogs.msdn.microsoft.com/junfeng/2004/08/05/domain-neutral-assemblies/
- Pro .NET Performance: Optimize Your C# Applications. Page 289
- Introduction .NET 4.5 Alex Mackey,William Stewart Tulloch, Mahesh Krishnan. Page 149
- https://technet.microsoft.com/ru-ru/library/ee790599.aspx
- http://www.microsoftpro.nl/2011/01/27/exporting-and-importing-sites-and-app-pools-from-iis-7-and-7-5/
- https://technet.microsoft.com/en-us/library/ea8d442e-9a0c-49bb-b940-50b22fa64dd4
- https://docs.microsoft.com/en-us/dotnet/framework/tools/ngen-exe-native-image-generator







