1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 3 из 5

nginx, Apache, IIS: чем они разные

Коротко

Apache и nginx по-разному отвечают на два вопроса. Соединение: у Apache запрос, пока он читается, держит процесс или поток, и их конечное число - в нашем замере восемь медленных клиентов положили сайт с восемью воркерами; у nginx тысячи соединений живут в одном цикле событий, и семьсот медленных ничего не меняют. Конфиг: Apache складывает пересекающиеся секции и, если это разрешено AllowOverride, читает .htaccess на каждом запросе; nginx выбирает location; наследование зависит от конкретной директивы. IIS - отдельный мир Windows и .NET.

Если с Apache дела не имел - тебе хватит «Правило» и последнего блока про выбор.

Восемь клиентов с плохим интернетом. Не атака, не нагрузка - просто восемь человек, у которых запрос идёт по сети медленно. Что станет с сайтом?

↳  выводстенд: медленные клиенты против одного обычного запроса
Apache prefork (8 воркеров):   не ответил, таймаут 5 с
Apache event   (8 потоков):    не ответил, таймаут 5 с
nginx (1 воркер):              ответ за 0,001 с

У nginx тот же замер повторён на семистах медленных соединениях - ответ всё те же 0,001 секунды, и в системе по-прежнему два процесса.

На что это похоже

Apache в классическом виде - это касса на посетителя: пришёл человек - открыли кассу, ушёл - закрыли. Пока он ищет кошелёк, касса занята и никого больше не обслуживает. Касс столько, сколько разрешил владелец; кончились - очередь стоит на улице.

nginx - один кассир, который никогда никого не ждёт. Пока посетитель ищет кошелёк, кассир пробивает следующего, а к первому вернётся, когда тот будет готов. Отсюда и разница: у Apache потолок - число касс, у nginx - объём работы, а не число людей в зале.

Вторая разница - в конфиге, и она устроена так же зеркально: Apache читает правила по дороге и складывает их, nginx выбирает одно и работает по нему.

Механизм: две модели обслуживания

Apache: касса на клиента клиент 1 процесс 1 клиент 2 процесс 2 клиент 9 касс нет потолок - число процессов nginx: один цикл тысячи соединений воркер потолок - объём работы медленный клиент занимает кассу, но не занимает цикл
Соединение у Apache стоит процесса или потока, у nginx - записи в списке

Правило

Apache nginx IIS
соединение стоит процесса (prefork) или потока (worker); у event поток нужен, пока запрос читается и обрабатывается записи в списке одного воркера HTTP.sys принимает соединения; потоки обрабатывают запросы
конфиг секции по каталогам, правила складываются блоки по адресу, побеждает один графический интерфейс и web.config
правила рядом с сайтом .htaccess, читается на каждый запрос, если включён AllowOverride (по умолчанию выключен) нет ничего подобного web.config в каталоге
модули грузятся из конфига (LoadModule) вкомпилированы или динамические .so ставятся в систему
где живёт везде везде только Windows

Замер на стенде: у Apache из конфига загружено 10 модулей, у nginx 30 флагов сборки плюс 14 динамических файлов в образе. Отсюда практическое следствие: установленный динамический модуль подключают в конфиге. Если файла модуля нет, сначала нужен подходящий пакет или сборка; это относится и к Apache, и к nginx.

Разбор: восемь медленных клиентов

Стенд: Apache 2.4.68 из образа httpd:alpine с MaxRequestWorkers 8 (в варианте event - восемь потоков) и умолчаниями mod_reqtimeout, и nginx с одним воркером и worker_connections 1024. Клиент подключается и отправляет заголовки, но не дописывает их до конца - обычное поведение мобильной сети, а не атака. Параллельно идёт один нормальный запрос, который ждёт ответа пять секунд.

медленных клиентов Apache prefork Apache event nginx
4 0,002 с 0,002 с 0,001 с
7 1,567 с 0,001 с -
8 не ответил не ответил 0,001 с
100 - - 0,001 с
700 - - 0,001 с

Обрыв ровно на восьмой связи - это и есть модель: незаконченный запрос держит процесс (prefork) или поток (event), а их ровно восемь. Девятому клиенту некому ответить, и сайт «лежит», хотя нагрузки нет никакой.

Счётчики процессов под той же нагрузкой:

↳  выводсводка по docker top при 30 медленных соединениях
Apache prefork:  5 процессов в покое -> 9 под нагрузкой
Apache event:    11 потоков, число не меняется
nginx:           2 процесса, число не меняется

Важная тонкость про event, из-за которой его часто переоценивают: он вынес в отдельный поток простаивающие keepalive-соединения, но не чтение запроса. Пока заголовки не дочитаны, поток занят - и порог оказался тем же, восьмым.

Эта разница и есть причина, по которой nginx вообще написали: в 1999 году задача называлась «десять тысяч соединений на одной машине», и модель «процесс на соединение» её не решала ни при каком объёме памяти.

Разбор: заголовки на границе каталога

Второе различие видно на одном замысле, записанном дважды: общий заголовок на сайт плюс свой - для каталога /sub/.

/usr/local/apache2/conf/httpd.conf
<Directory /www>
    Header set X-Level "root"
    Header set X-Common "общий"
</Directory>
<Directory /www/sub>
    Header set X-Level "sub"
</Directory>
/etc/nginx/conf.d/lab.confhttp › server
add_header X-Level  "root"  always;
add_header X-Common "общий" always;

location /sub/ {
    add_header X-Level "sub" always;
}
↳  выводзаголовки ответа на /sub/
Apache:  X-Level: sub    X-Common: общий
nginx:   X-Level: sub

Apache для /sub/ собрал правила из обеих секций: свой X-Level перекрыл общий, а X-Common остался. nginx выбрал один блок - location /sub/, - и его add_header заменил унаследованный набор целиком.

Это не частный случай заголовков, а общее правило nginx, которое встретится ещё не раз: наследуется целиком или никак. Так же ведут себя proxy_set_header и списки allow/deny.

Практический вывод для переезда: каждый блок, где появился свой add_header, обязан повторить общие заголовки - либо один раз написать add_header_inherit merge; (nginx 1.29.3), и тогда поведение станет ближе к привычному по Apache.

Разбор: .htaccess после переезда

На Apache файл в каталоге меняет поведение без перезагрузки - замер: положили .htaccess с одной строкой, следующий же ответ пришёл с новым заголовком. Это удобство и есть причина, по которой хостинги дают .htaccess вместо доступа к конфигу.

У nginx такого механизма нет вовсе, и это осознанное решение: за него платят чтением файлов на каждом запросе по всему пути каталогов. Но опасность другая - что nginx сделает с уже лежащим файлом:

↳  выводcurl http://сайт/sub/.htaccess на nginx
код 200, тело: Header set X-From-Htaccess "да"

Файл отдаётся как обычная статика. После переезда со старого хостинга в корне сайта остаются .htaccess, .env, .git - и всё это открыто, пока не закрыто явно (двенадцатый раздел). Первое, что делают после переноса, - проверяют запросом, а не глазами.

Что ломается без этого

Расчёт «клиентов столько же, значит и памяти столько же». У Apache память и потолок соединений связаны: каждый клиент - процесс или поток со своим стеком, и MaxRequestWorkers приходится держать таким, чтобы парк влез в оперативную память. У nginx эта связь разорвана: соединение стоит записи в списке, а память растёт от работы - от буферов, кеша и тел ответов. Переносить настройки «в лоб» бессмысленно, у чисел разный смысл.

Надежда на MPM event. Он вынес простаивающие keepalive-соединения в отдельный поток, и его часто считают ответом на медленных клиентов. Замер показал обратное: порог остался тем же, восьмым, потому что незаконченный запрос по-прежнему держит поток. Вывод касается этой конфигурации, а не всех режимов Apache: медленного клиента рано или поздно выгоняет RequestReadTimeout из mod_reqtimeout (по умолчанию на заголовки даётся 20-40 секунд), и чем жёстче этот таймаут и больше MaxRequestWorkers, тем выше порог. Но пока таймаут не истёк, поток занят. Против медленного чтения надёжнее всего помогает сервер перед приложением - тот самый буфер из первого урока главы.

.htaccess по привычке. Чтение файла на каждом запросе - не обязательная плата за Apache: AllowOverride None его отключает, и в стандартном httpd.conf так и стоит. В замере про переезд его включили, чтобы показать механизм.

Конфиг, переписанный построчно. <Directory> и location похожи ровно до первого пересечения: в Apache секции складываются в порядке от общего к частному, в nginx выигрывает один блок по своим правилам приоритета (четвёртый раздел). Правило из Apache «более частная секция дополняет общую» здесь не работает.

Ожидание, что «положу файл - и подействует». Всё, что в Apache решалось .htaccess, в nginx решается конфигом и перезагрузкой. Для сайтов, которые раздают пользователям каталоги, это меняет схему работы целиком.

Переезд с IIS. Там кроме конфига есть привязка к системе: аутентификация Windows, приложения на .NET в пуле приложений, права через ACL файловой системы. nginx перед .NET-приложением ставят, но заменять им IIS целиком означает переносить и то, что жило в интеграции с доменом.

Зачем это в работе

Короткий ответ на вопрос «что выбрать»:

  • nginx - когда нужен фронт: статика, TLS, балансировка, лимиты, кеш. Ниша, ради которой он и написан, - много одновременных соединений при малой памяти.
  • Apache - когда нужен .htaccess для чужих сайтов (классический хостинг) или модуль, которого больше нигде нет; mod_php внутри процесса сегодня почти везде заменён на php-fpmРазбирается в разделе 7, глава «PHP и FastCGI»: Как nginx разговаривает с PHP - отдельную службу для PHP, - к которой одинаково ходят оба сервера.
  • IIS - когда стек Windows: .NET, аутентификация домена, управление через графический интерфейс.
  • Caddy - когда хочется сертификаты «из коробки» и конфиг в пять строк; цена - меньше рычагов, когда понадобится тонко.
  • HAProxy, Envoy, Traefik - это уже не веб-серверы, а балансировщики и маршрутизаторы: статику они не отдают, зато у них своё в наблюдаемости и динамической конфигурации.

Связка «nginx впереди, Apache сзади» - до сих пор живая: nginx держит соединения и отдаёт статику, Apache исполняет старое приложение с его модулями. И знание фаз nginx помогает читать чужие конфиги в обе стороны.

Теперь сам

Сайт на Apache prefork с MaxRequestWorkers 150 вечерами перестаёт отвечать, хотя процессор загружен на десять процентов, а в access_log в эти минуты почти пусто. Коллега предлагает поднять MaxRequestWorkers до 500. Что тут не так и куда смотреть?

Ответ: пустой access_log - главная улика. Строка пишется, когда запрос завершён; если соединения висят на медленном чтении, они держат процессы и в логе не появляются вовсе. Загруженность процессора при этом низкая - работы нет, есть ожидание. Поднимать MaxRequestWorkers опасно: каждый процесс prefork несёт свою память (а с mod_php - ещё и интерпретатор), и 500 процессов кончатся не отказом в обслуживании, а свопом или OOM. Смотреть надо на число занятых воркеров против числа завершённых запросов (mod_status), а лечится это не числом касс, а тем, чтобы медленных клиентов принимал кто-то другой: nginx впереди дочитывает запрос целиком и отдаёт бэкенду готовый.

Главное

При выборе веб-сервера проверь, как он обслуживает медленных клиентов и как объединяет настройки. При переносе с Apache перепиши правила .htaccess в конфиг nginx и закрой служебные файлы от выдачи. Копирование директив без проверки маршрутов не сохраняет поведение сайта.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.