В микросервисном мире добавление новой функциональности осуществляется путем написания нового сервиса. При этом стоимость добавления новой единицы составляет минимум 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 использует кэш нативных образов

Ссылки


  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

Исходный код


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