High performance
Website development
Nginx
June 2015 11

NGINX — История перерождения под Windows

Раз уж тут у нас «неделя» nginx, например тут или тут, то попробую и я внести свою, так сказать, лепту. Речь пойдет про nginx 4 windows, а именно про более-менее официальную сборку для этой пропритарной, некоторыми не очень любимой платформы.

Почему Windows. Все просто, в корпоративном секторе Windows на сервере, да и на рабочих станциях — нередко обязательная программа. И от этих требований к платформе, например в ультимативной форме озвученных клиентом, никуда не денешься.
И раз уж имеем Windows, но не хочется мучиться с IIS, apache и иже с ними, если хочется использовать любимые инструменты, а nginx однозначно к ним относится, то приходится иногда мириться даже с некоторыми ограничениями на этой платформе. Вернее приходилось…

Хотя нужно заметить, что даже с этими ограничениями, nginx даст фору практически любому веб-серверу под windows по многим факторам, в том числе по стабильности, потреблению памяти, а главное производительности.

Спешу сразу поделится хорошей новостью — больше ограничений, критичных к высокой производительности, при использовании nginx под windows практически не существует, и последнее из критичных, с высокой долей вероятности, тоже скоро отпадет. Но по порядку…

Здесь описаны известные проблемы nginx 4 windows, а именно:

  • Рабочий процесс может обслуживать не более 1024 одновременных соединений.
  • Кэш и другие модули, требующие поддержки разделяемой памяти, не работают под Windows Vista и более поздними версиями в связи с тем, что на этих версиях Windows включена рандомизация адресного пространства.
  • Хоть и возможен запуск нескольких рабочих процессов, только один из них реально работает.

Я немного изменил порядок, т.к. именно в такой последовательности я разбирался с этими ограничениями, так сказать отсортировано «исторически».

1024 одновременных соединений


На самом деле это не правда, вернее не совсем правда — nginx с незапамятных времен можно было собрать под Windows без этого ограничения — нужно было просто на этапе сборки определить FD_SETSIZE равным нужному вам количеству соединений.
Например для VS добавив директиву --with-cc-opt=-DFD_SETSIZE=10240, воркер nginx сможет управляться с 10K одновременными соединениями, если в конфигурации вы укажете worker_connections 10240;.

Кэш и другие модули, требующие поддержки разделяемой памяти


Все эти функции и модули до недавнего времени действительно не работали под Windows, начиная с версии x64 или там где по умолчанию вся система работает с включенным ASLR.
Причем отключение ASLR для nginx ничего не меняет, т.к. функции для работы с shared memory зашиты глубоко в kernel, т.е. ASLR (а с ним вероятно и DEP, с ним почему-то не получалось) нужно отключать для всей системы.

Это на самом деле довольно не маленький список функционала: Кэш, любые зоны, соответственно и limit_req и т.д. и т.п. Кстати без поддержки разделяемой памяти гораздо труднее было бы убрать 3-е ограничение, т.е. реализовать поддержку multiple workers под windows.

Не буду утомлять читателя как я с этим боролся, но совместно с Максом (спасибо mdounin) мы таки допилили это до релизной версии. Немного об этом, кому интересно см. под спойлером или в исходниках hg.nginx.org или github
Немного теории...
Сама разделяемая память может быть использована и с рандомизацией адресного пространства. Одно другому как бы не мешает, просто при включенном ASLR вы в другом процессе практически гарантировано получите указатель на «ту же память», но под другим адресом. Это на самом деле не критично, пока содержимое этого пространства само не содержит прямых указателей, aka pointer. Т.е. указатели-смещения относительно начального адреса shmem допустимы, но не прямые указатели как они есть.
Соответственно без того чтобы переписать весь функционал, работающий с pointer внутри shared mem в nginx — есть единственный вариант, обмануть заставить таки windows выдать ссылку на shmem под постоянным для всех рабочих процессов, адресом. Ну а далее все не очень сложно, на самом деле.
Начало дискуссии про это можно почитать здесь. Максим, кстати починил упущенную мной проблему (remapping), иногда возникающую после перезагрузки воркеров (reload налету).
Viva open source!

Т.е. официально это ограничение больше не действует с Release 1.9.0 от 28 Apr 2015:
Feature: shared memory can now be used on Windows versions with 
address space layout randomization.

Только один рабочий процесс реально работает.


В nginx есть процесс мастер и дочерние процессы, называемые рабочими или worker.
Под windows у nginx может быть запущено несколько рабочих процессов, т.е. указав в конфигурации "worker_processes 4;", вы заставите мастера запустить четыре дочерних рабочих процесса. Проблема состоит в том, что только один из них, «украв» listener-соединение у мастера (используя SO_REUSEADDR) будет действительно слушать этот сокет, т.е. делать accept входящих соединений. В результате же у других воркеров — нет входящих соединений — нет работы.
Это ограничение связано с технической реализацией winsock, и единственный способ получить распределенное listener-соединение для всех рабочих процессов в Windows — это клонировать сокет из мастер-процесса, т.е. использовать inherited handle от сокета из него.
Кому интересны подробности реализации, могут посмотреть их под спойлером или в исходниках, пока только у меня на гитхаб.
Подробнее...
Начнем с того, что даже если запускать дочерние процессы (CreateProcess), используя bInheritHandle=TRUE, и установив SECURITY_ATTRIBUTES::bInheritHandle при создании сокета тоже равным TRUE, скорее всего у вас ничего не выйдет, т.е. в рабочем процессе используя этот handle, вы получите «failed (10022: An invalid argument was supplied)». А «успешно» продублировав этот сокет с помощью DuplicateHandle, дублированный handle тоже не примет ни одна функция работающая с сокетами (вероятно с ошибкой 10038 — WSAENOTSOCK).
Почему так происходит приоткрывает одна цитата из MSDN — DuplicateHandle:
Sockets. No error is returned, but the duplicate handle may not be recognized by Winsock at the target process. Also, using DuplicateHandle interferes with internal reference counting on the underlying object. To duplicate a socket handle, use the WSADuplicateSocket function.
Проблема в том, что для дублирования handle с помощью WSADuplicateSocket, необходимо заранее знать pid процесса, т.е. это нельзя сделать до того как процесс был бы запущен.
В результате, чтобы сообщить дочернему процессу информацию полученную мастером от WSADuplicateSocket, необходимую для создания сокета-клона в рабочем процессе — имеем два варианта, либо использовать что-нибудь вида IPC, например как это описано в MSDN — WSADuplicateSocket, либо передать это через shared memory (благо мы уже это выше починили).
Я решил использовать второй вариант, т.к. считаю, что это наименее трудоемкий из двух и наиболее быстрый способ реализации наследования соединения.
Ниже изменения в алгоритме запуска рабочих процессов под windows (помечены *):
  • Мастер-процесс создает все listener-сокеты;
  • [cycle] Мастер-процесс создает рабочий процесс;
  • * [win32] мастер вызывает новую функцию ngx_share_listening_sockets: для каждого listener-сокет опрашивается информация (для наследования) конкретно для этого нового воркера, («клонированная» через WSADuplicateSocket для pid), которая будет сохранена в shared memory — shinfo (protocol structure);
  • Мастер-процесс ждет пока worker установит событие о готовности — event «worker_nnn»;
  • * [win32] Рабочий процесс выполняет новую функцию ngx_get_listening_share_info для получения информации наследования shinfo, которая будет использоваться для создания нового дескриптора сокета для shared listener-сокета мастер-процесса;
  • * [win32] Рабочий процесс создает все listener-сокеты, используя информацию shinfo от мастер-процесса;
  • Рабочий процесс устанавливает событие — event «worker_nnn»;
  • Мастер-процесс прекращает ожидание, и создает следующий рабочий процесс, повторяя [cycle].

Если нужно, здесь ссылка на дискуссию о фиксе, дабы она будет.

В результате, nginx под windows запускает теперь N «полноценных», с точки зрения «прослушивания», а главное установления соединения, рабочих процессов, которые обрабатывают входящие соединения действительно параллельно.

Этот фикс правда еще лежит «пул-реквестом» (я отправил changeset в nginx-dev), но его уже можно попробовать например скачав с моего github и самостоятельно собрав под windows. Если будут желающие выложу куда-нибудь бинарник.

Я довольно долго истязал свое железо, гоняя это тестами и под нагрузочными «скриптами» — результат, все воркеры нагружены более-менее равномерно и действительно работают параллельно. Также я пробовал на лету (reload-ом) перезагружать nginx и случайным образом «убивал» некоторых воркеров имитируя «crash» последних — все работает без малейшего нарекания.
Пока проявился единственный «недостаток», имхо — если запустить
netstat /abo | grep LISTEN
то вы увидите только мастер-процесс в списке «слушателей», хотя в реальности как раз он никогда не устанавливает соединение, только его дочерние рабочие процессы.

Кстати, мой опыт пока говорит, что accept_mutex для windows-платформы вероятно нужно отключать "accept_mutex off;", т.к. по крайней мере на моих тестовых системах, с включенным accept_mutex они работали ощутимо медленнее, чем с выключенным. Но это думаю каждый должен проверять экспериментально (т.к. зависит от кучи параметров, типа количество ядер, воркеров, keep-alive соединений и т.д. и т.п.).

Ну и как без красивых табличек с числами сравнения производительности, до (первый столбец помечен **NF) и после.
Тест сделан на Windows7 — i5-2400 cpu @ 3.10GHz (4 core).
Request: статика, 452 байта (+ header) — маленькие gif-иконки.
Workers x Concur. 1 x 5 **NF 2 x 5 4 x 5 4 x 15
Transactions 5624 hits 11048 hits 16319 hits 16732 hits
Availability 100.00 % 100.00 % 100.00 % 100.00 %
Elapsed time 2.97 secs 2.97 secs 2.97 secs 2.96 secs
Data transferred 2.42 MB 4.76 MB 7.03 MB 7.21 MB
Response time 0.00 secs 0.00 secs 0.00 secs 0.00 secs
Transaction rate 1893.60 trans/sec 3719.87 trans/sec 5496.46 trans/sec 5645.07 trans/sec
Throughput 0.82 MB/sec 1.60 MB/sec 2.37 MB/sec 2.43 MB/sec
Concurrency 4.99 4.99 4.99 14.92
Successful transactions 5624 11048 16319 16732
Failed transactions 0 0 0 0
Longest transaction 0.11 0.11 0.11 0.11
Shortest transaction 0.00 0.00 0.00 0.00

И да пребудет с вами nginx и под windows.

+63
40.8k 203
Support the author
Comments 81
Top of the day