Vodka, или почему nginx.exe у нас в шесть раз быстрее, чем под Wine

Под прошлой статьёй я пообещал показать бенчмарки nginx.exe. Так вот — показываю!
Но прежде чем перейти к подробному разбору бенчмарков, расскажу об устройстве Vodka и покажу, как мы получаем оригинальные файлы Windows, не нарушая лицензий.
Чем же мы занимаемся?
Кратко повторюсь: идея — создать универсальный слой совместимости, чтобы ПО могло работать на нашей ОС, независимо от того, для какой ОС оно изначально было создано. Мы не пытаемся гнаться за всем многообразием библиотек пространства пользователя. Нам важно, чтобы программа не просто запускалась, а работала на наших условиях, под полным контролем и при этом максимально приближалась по производительности к тому, что даёт её штатный путь исполнения. Поэтому мы берём оригинальные файлы целиком, всей связкой, как есть и подключаем к нашему «полу» так, чтобы они чувствовали себя как дома.
Vodka можно разделить на четыре слоя, и выглядеть это будет так:
Программы в пространстве пользователя (apps)
Это то самое программное обеспечение, которое мы хотим запустить независимо от операционной системы, для которой оно было спроектировано. Сюда же, если речь о ПО для Windows, будет входить большое количество разнообразных библиотек и сопутствующих файлов.
Библиотеки прослойки (os-proxy)
Сюда входят файлы, которые мы даём программам под видом их зависимостей. При этом их список довольно небольшой, так как мы подменяем самые низкоуровневые библиотеки пространства пользователя, которые являются API‑обёртками над системными вызовами к ядру ОС. Мы грамотно и изящно реализуем это API, опираясь на возможности следующего ОС-независимого слоя. Это сложная и муторная работа, но мы планомерно реализуем функцию за функцией, находя кандидатов по журналу необработанных вызовов при падениях очередной тестируемой программы. Сейчас реализовано 68,7% Nt-вызовов ntdll и 77,5% Nt-вызовов win32u.
Немаловажно добавить, что на этом же слое находится и Ke‑API, который нам необходим для запуска оригинальных драйверов и прочей «ядерной» работы.
Примитивы Vodka (vdk primitives)
Это самый важный слой, который позволяет нам выделить абстрактный, ОС‑независимый API к системе. Он разделён на несколько частей, и у каждой своя зона ответственности. Здесь располагаются компоненты: vdkldr — загрузка исполняемых файлов, vdkio — работа с файлами, vdksync — примитивы синхронизации, vdkgl — работа с графикой и другие.
Бэкенд примитивов (primitives backend)
Здесь мы описываем всё то, что является ОС‑спецификой, то есть как и с чем будут работать примитивы. Простой пример: в backend-части vdkio_open() использует open() на Linux и NtOpenFile() на Windows. Мы выбрали Linux основной платформой и сейчас полноценно развиваем backend-часть именно для него.
Откуда взять оригинальные файлы и их зависимости?
Под прошлой статьей множество комментариев было посвящено юридическим вопросам.
И действительно, как Vodka может функционировать, если легально и без нарушения лицензий никак нельзя получить те самые библиотеки WinAPI: kernel32.dll, kernelbase.dll, user32.dll и множество других?
Так вот, в прошлой статье все зависимости, те самые файлы из директории winfiles, были получены с примонтированного диска с установленной Windows. И этот способ рабочий и легальный, но, будем честны, довольно неприятный для пользователя, так как требует много действий. А для нас, как для разработчиков, он не даёт необходимой воспроизводимости полученного результата, да и порой сложно определить, что именно нужно взять. И тут мы задумались и придумали достаточно, скажем так, радикальное решение.
А что, если установить Windows на Linux?
Логика проста: если мы не можем взять зависимости по одной — мы возьмём их все, следуя официальному пути распространения. Через установку образа Windows. И для этого у нас появилась отдельная утилита — vodka‑decant. Она автоматизирует получение зависимостей несколькими способами; один из них — создание миров через полноценную установку из ISO-образа.
Миры?
Под миром в контексте Vodka подразумевается определённая конфигурация окружения для запуска.
Откуда брать зависимости?
С какими опциями запускается vodka?
Где файлы реестра?
Чей это процесс?
Как видеть файловую систему?
Вот на эти и прочие вопросы о глобальных настройках отвечает мир. Кроме того, миры могут создаваться из уже готовых: в этом случае мы не копируем всю ораву файлов и библиотек, но реестр всегда берём копией. Дочерний мир просто видит и использует необходимые компоненты, прямо обращаясь к родительским файлам. Если дочерний мир меняет файл, он создаёт свою копию. Если удаляет — оставляет пометку и дальше файла не видит. А если пометки нет и у первого предка файла нет, поиск переходит к следующему.
Что внутри ISO-образа?
$ vodka-decant windows list --iso ~/Windows-11-25H2.iso
EDITION SIZE NAME
1 20.7 GiB Windows 11 Home
2 20.1 GiB Windows 11 Home N
3 20.6 GiB Windows 11 Home Single Language
4 21.5 GiB Windows 11 Education
5 20.9 GiB Windows 11 Education N
6 21.6 GiB Windows 11 Pro
…
11 edition(s). `--edition N` says what is in one of them, and `import --edition N` takes it.Здесь мы просто вывели список выпусков Windows, которые можно установить из образа. И возьмём мы шестой выпуск — Windows 11 Pro.
Как выглядит установка?
$ vodka-decant windows import --iso ~/Windows-11-25H2.iso --edition 6 --world win11
world 'win11' made at ~/.local/share/vodka/worlds/win11
windows: edition 6 of 11: Windows 11 Pro
…
laying the medium out at ~/.cache/vodka/decant/medium/Windows-11-25H2
975 file(s), 7372 MiB laid out
laying the image down: C:\Windows\System32\Dism.exe /Apply-Image /ImageFile:D:\sources\install.wim /Index:6 /ApplyDir:T:\
…
Deployment Image Servicing and Management tool
Version: 10.0.26100.5074
Applying image
[==========================100.0%==========================]
The operation completed successfully.
…
the registry, out of the hives the image laid down:
Windows/System32/config/SYSTEM 25666 key(s), 65528 value(s) -> Windows\Machine\SYSTEM
Windows/System32/config/SOFTWARE 258578 key(s), 412931 value(s) -> Windows\Machine\SOFTWARE
Windows/System32/config/SECURITY 1 key(s), 0 value(s) -> Windows\Machine\SECURITY
Windows/System32/config/SAM 1 key(s), 0 value(s) -> Windows\Machine\SAM
Windows/System32/config/DRIVERS 23924 key(s), 28090 value(s) -> Windows\Machine\DRIVERS
Windows/System32/config/DEFAULT 64 key(s), 185 value(s) -> Windows\User\.DEFAULT
…/targets/windows.unattend.xml -> Windows/Panther/unattend.xml
what the new machine runs before its setup: C:\Windows\System32\bcdboot.exe C:\Windows /s C: /f UEFI
…
Boot files successfully created.
booting the new machine through the end of its setup
world 'win11' finishing its setup
setup: SystemSetupInProgress 1, OOBEInProgress 1, SetupType 1
boot 1
…
setup execute: setupcl.exe
setupcl.exe 762987 session 0, …/setupcl.log
exit 0
wininit.exe 762993 session 0, …/wininit.log
winlogon.exe 787449 session 1, …/winlogon.log
after 6s
setup: SystemSetupInProgress 1, OOBEInProgress 1, SetupType 0
after 8s
setup: SystemSetupInProgress 1, OOBEInProgress 1, SetupType 2
after 114s
setup: SystemSetupInProgress 0, OOBEInProgress 1, SetupType 2Самое ценное, что мы здесь видим, — это строчка про DISM. Образ на диск кладёт не наш код, а Dism.exe — та самая программа, с помощью которой это делает установщик Windows. Главное — мы не имитируем установку, а выполняем настоящую, просто дав ей возможность отработать на нашем слое. Затем Windows настраивает себя сама, и мы получаем всё, что нужно.
Также важно обратить внимание на строку 23:…/targets/windows.unattend.xml -> Windows/Panther/unattend.xml
Мы подкладываем специальный файл перед перезагрузкой и следующим этапом установки. Если вы с таким не сталкивались, поясню: этот файл содержит ответы на вопросы установки, на которые должен отвечать человек. Это штатный механизм, и его используют при автоматическом развёртывании.
Вот так выглядит этот XML
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<OOBE>
<SkipMachineOOBE>true</SkipMachineOOBE>
<SkipUserOOBE>true</SkipUserOOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<UserAccounts>
<LocalAccounts>
<LocalAccount wcm:action="add">
<Name>vodka</Name>
<Group>Administrators</Group>
<DisplayName>vodka</DisplayName>
<Password>
<Value>vodka</Value>
<PlainText>true</PlainText>
</Password>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<AutoLogon>
<Enabled>true</Enabled>
<Username>vodka</Username>
<LogonCount>999999999</LogonCount>
<Password>
<Value>vodka</Value>
<PlainText>true</PlainText>
</Password>
</AutoLogon>
<TimeZone>UTC</TimeZone>
</component>
</settings>
</unattend>Что получилось после установки?
Теперь у нас есть вот такая директория, которая представляет мир:
$ tree -L1 ~/.local/share/vodka/worlds/win11
/home/ub/.local/share/vodka/worlds/win11
├── files // его C:
├── registry.hiv // его реестр — кусты, которые привёз образ
├── removed // имена, которые он удалил
└── world.conf // что мир о себе говоритИ сейчас создадим из мира win11 новый мир nginx:
$ vodka world new nginx --from win11
WORLD ENGINES WHERE
nginx 0 ~/.local/share/vodka/worlds/nginx
falls through to ~/.local/share/vodka/worlds/win11И да, благодаря устройству миров форк занял 0,12 секунды.
Теперь сам nginx. Возьмём ветку Mainline — на момент написания статьи это версия 1.31.6. Официальная сборка для Windows — ZIP‑архив, и распаковывать его мы будем средствами самой Windows: в Windows 11 есть tar.exe. Директории с архивом на один запуск даём букву диска:
$ vodka --world nginx --map 'i"D:" => "/home/ub/Downloads"' \
tar.exe -xf 'D:\nginx-1.31.6.zip' -C 'C:\'
$ vodka --world nginx 'C:\nginx-1.31.6\nginx.exe' -v
nginx version: nginx/1.31.6
$ vodka --world nginx 'C:\nginx-1.31.6\nginx.exe' -p 'C:\nginx-1.31.6' -t
nginx: the configuration file C:\nginx-1.31.6/conf/nginx.conf syntax is ok
nginx: configuration file C:\nginx-1.31.6/conf/nginx.conf test is successfulА вот карта подгружаемых зависимостей для nginx.exe
$ vodka --world nginx --print-map 'C:\nginx-1.31.6\nginx.exe' -v
-- loaded modules (engine at 0x000000005663c000) --
0x0000000000400000 +0x676000 nginx.exe
0x00000000737e2000 +0x107000 crypt32.dll
0x000000007c62a000 +0x9000 dpapi.dll
0x000000007393d000 +0x61000 ws2_32.dll
0x00000000738e9000 +0x54000 mswsock.dll
0x000000007fe1b000 +0x1c5000 user32.dll
0x000000007fdf8000 +0x23000 gdi32.dll
0x0000000073b33000 +0xec000 gdi32full.dll
0x0000000074559000 +0x37000 DXCore.dll
0x0000000073aae000 +0x85000 msvcp_win.dll
0x000000007399e000 +0x110000 ucrtbase.dll
0x00000000741b9000 +0x7f000 advapi32.dll
0x000000007ffe2000 +0x14000 cryptsp.dll
0x000000007fff6000 +0xa000 cryptbase.dll
0x0000000073c1f000 +0xbc000 rpcrt4.dll
0x0000000073cdb000 +0x83000 sechost.dll
0x0000000073d5e000 +0xc7000 msvcrt.dll
0x000000007c633000 +0x23d000 win32u.vdk
0x0000000010000000 +0xe5000 kernel32.dll
0x0000000073e25000 +0x2cb000 KernelBase.dll
0x0000000074238000 +0x2c8000 ntdll.vdk
nginx version: nginx/1.31.6И из этого набора только ntdll.vdk и win32u.vdk наши.
Запускаем со штатным конфигом и стучимся:
$ vodka --world nginx 'C:\nginx-1.31.6\nginx.exe' -p 'C:\nginx-1.31.6' &
$ curl -sD - http://127.0.0.1:63080/ | head -2
HTTP/1.1 200 OK
Server: nginx/1.31.6
$ vodka bar --world nginx
MARK PROGRAM WORLD STATE PID W AGE CPU HELD
a676 nginx.exe nginx running 2412367 32 0:00:00 0:00:00 368M
e9cf nginx.exe nginx running 2412418 32 0:00:00 0:00:00 149MВы можете заметить порт 63080 вместо 80, и это не опечатка. Linux по умолчанию отдаёт порты ниже 1024 только пользователю root, а в данном случае Vodka работает от обычного пользователя. Поэтому на стороне хозяина мир занимает порт из верхнего диапазона. Для nginx.exe и для всех программ мира это по‑прежнему порт 80: на вопрос о своём адресе nginx отвечает: «80», и соседи по миру находят его на 80. Снаружи, из Linux, к нему стучатся на 63080.
Цифры и бенчмарки!
Пора перейти к самому главному и сравнить производительность Vodka и Wine.
Что с чем сравниваем
Один и тот же файл.
nginx.exe1.31.6 — официальная сборка с nginx.org, 32-битная.Wine в полной комплектации. Wine 11.16 из репозитория дистрибутива с
ntsyncв ядре. И отдельной строкой — тот же Wine безntsync.Для масштаба — nginx той же версии, но для Linux и 64-битный. Просто чтобы видеть потолок.
Конфиг один на всех: ответ в 18 байт, keep‑alive, журнал запросов выключен.
Нагрузка —
wrk -t8 -c100, десять секунд после прогрева. Пять повторов, движки чередуются, в таблицах медианы.Процессорное время на запрос считается по всем процессам сервера. У Wine в счёт входит
wineserver: без него половина работы Wine осталась бы за кадром.
Что на одном воркере?
В шесть раз по ответам в секунду. Почти в восемь — по процессорному времени. И 80% от nginx, собранного под Linux, притом что это программа для Windows на настоящих ws2_32.dll и mswsock.dll.
Почему?
Не потому, что мы что‑то хитро оптимизировали. Давайте просто посмотрим на путь одного запроса у Vodka и у Wine:
У Wine объекты ядра Windows — сокеты, события, описатели — живут в отдельном процессе, который называется wineserver. Чтобы прочитать запрос, nginx.exe сначала спрашивает сервер (recv_socket), потом читает сам и докладывает серверу, чем кончилось (set_async_direct_result). Чтобы ответить — то же самое ещё раз.
У нас сервера нет. Вообще. NtDeviceIoControlFile из подлинной mswsock.dll приходит в нашу ntdll.vdk, и та в том же потоке делает системный вызов Linux. А то, что должно быть общим для нескольких процессов, лежит в общей памяти мира, и процесс берёт это сам: свободный замок — одна инструкция процессора и ни одного системного вызова.
А теперь, вооружившись strace и /proc, посмотрим системные метрики:
Наши 2,6 вызова — это recvmsg, sendmsg и десятая доля poll: те же три, что делает родной nginx, плюс часы. У Wine на тот же запрос 43, и делятся они почти поровну: 22 делает nginx.exe — пишет серверу, читает ответ, закрывает и открывает сигналы вокруг каждого обращения — и 21 сам wineserver.
Теперь посмотрите на предпоследнюю строку. В ядре Linux nginx.exe на Vodka проводит почти столько же, сколько родной nginx: 3,5 микросекунды против 3,2. Вся наша наценка — микросекунда с небольшим в пользовательском режиме: это настоящие библиотеки Windows и наш слой под ними. А у Wine в ядре 27 микросекунд — и это не сеть. Тут два процесса разговаривают друг с другом: почти половину процессорного времени запроса, 47%, тратит не nginx.exe, а сервер.
Поэтому я и говорю, что дело не в оптимизации. Wine здесь медленный не оттого, что его плохо написали: его тридцать лет пишут очень сильные и храбрые люди. Он так устроен — между программой и ядром стоит посредник. Посредника нельзя ускорить в шесть раз. Его можно только убрать.
А что ntsync?
Когда я показал знакомому первые результаты, он сказал: «Конечно, в 6 раз! Поставь Wine с ntsync, он очень сильно ускоряет wineserver!»
Так вот, ntsync — модуль ядра Linux, который берёт на себя часть работы wineserver: ожидание мьютексов, семафоров и событий. Там, где программа ждёт именно их, например в играх, то да, он помогает очень заметно, и в Wine 11 это штатный режим.
Но nginx ждёт не мьютексов, а сокетов, — а сокеты как жили в сервере, так и живут. Смотрите в таблицу: обращений к серверу и переключений контекста с ntsync столько же, а системных вызовов больше — к прежним 43 сервер добавил шесть ioctl на запрос: теперь ему приходится держать в курсе ещё и модуль. В итоге Wine с ntsync здесь стабильно медленнее, чем без него: на 6% с keep‑alive, на 5% на новых соединениях.
Что на нескольких воркерах?
До сих пор у nginx был один рабочий процесс. Попросим больше: worker_processes 2, потом 4, 8 и 16. Клиент — wrk -t12 -c240, один и тот же на любое число воркеров; три повтора, в таблице медианы.
На шестнадцати воркерах nginx.exe на Vodka отдаёт 1,13 миллиона ответов в секунду. На Wine — те же 23 тысячи, что и на одном. В целых 48 раз медленнее!
И причин тут две.
Какая первая причина?
nginx для Windows не передаёт воркерам слушающий сокет: каждый воркер открывает порт сам и просит разрешения занять уже занятый адрес. Под Wine второй воркер получает отказ. В error.log остаётся:
[emerg] 332#336: bind() to 127.0.0.1:8242 failed (10013: Access denied)
[alert] 296#300: worker process 332 exited with code 1И nginx остаётся с одним воркером, сколько бы их ни просили.
А вторая причина?
Вторая интереснее. На настоящей Windows порт откроют все воркеры, но соединения достанутся одному — это прямо описано в документации nginx: «Although several workers can be started, only one of them actually does any work». У нас просьбу «разделить адрес» получает ядро Linux, а оно умеет раздавать соединения по всем сокетам, которые слушают порт. Работают все шестнадцать.
И выходит, программа для Windows получила то, чего у неё на самой Windows нет, и при этом осталась собой. Круто же!
А если серверов несколько?
Справедливое возражение: может, под Wine всё упирается только в этот bind()? Уберём из эксперимента модель воркеров nginx совсем. Запустим несколько отдельных nginx.exe, каждый на своём порту, и каждому дадим своего клиента. Под Wine все они живут в одном префиксе, у нас — в одном мире.
Сначала Wine растёт уверенно. Два сервера — вдвое больше ответов. Но у всех этих серверов один wineserver, а он однопоточный. На четырёх серверах он занят на 92%, на восьми — на все 100%, и на этом рост кончается: 90 тысяч ответов в секунду — потолок на весь префикс, сколько бы у меня ни было ядер. У нас потолка нет, и восемь серверов идут на тех же 80% от родного nginx, что и один.
Итог
nginx.exeна Vodka отдаёт 139 тысяч ответов в секунду на одном воркере. Тот же файл на Wine 11.16 — 23 тысячи. В шесть раз, а по процессорному времени на запрос — почти в восемь.На шестнадцати воркерах — 1,13 миллиона против тех же 23 тысяч. Восемь отдельных серверов — 771 тысяча против 90. Дальше Wine упирается в собственный сервер.
Причина не в оптимизации, а в устройстве: у нас между программой и ядром нет процесса‑посредника. 2,6 системных вызова на запрос против 43, одно переключение контекста на сотню запросов против восьми на каждый.
Файлы Windows в мир попадают через собственный установщик ОС, с образа пользователя. В поставке Vodka нет ни одного файла Microsoft, и код Microsoft мы не изменяем ни на байт.
Мир — это директория. Создание мира через наследование другого, даже с полноценно установленной Windows, — 0,12 секунды.
Мы не писали быстрый сервер объектов. Не писали ws2_32. Не писали установщик Windows. Мы сделали слой совместимости, тонкую границу — и всё, что над ней, заработало само.
Всё?
Почти. Последнее, что хочу сказать, — это про лицензирование Vodka.
В прошлой статье я спрашивал вашего мнения о лицензии, и вы ответили — в основном «открывайте». Несмотря на это, мы решили, что код Vodka будет закрытым, но лицензий будет две — бесплатная и коммерческая для компаний.
Одну причину я уже называл в комментариях: загрузчик, формат, правка импортов и запуск драйверов, да ещё всё это с полным контролем — вместе это инструмент, которым можно сделать «доверенного клиента» там, где его не предполагалось, а это может сильно навредить. Выложить такое целиком — решение, которое потом не отменить. Если же мы перестанем его поддерживать и развивать, проект станет открытым.
Всем спасибо за прочтение! Будем рады ответить на ваши вопросы, жалобы и предложения в комментариях :)
Скрытый текст
P. S.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.