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

# теория · шаг 1 из 4

Мастер, воркеры и цикл событий

Коротко

nginx - это один мастер и несколько воркеров, и работу делают только воркеры.

  • Мастер не обрабатывает запросы: он читает конфиг, занимает порты 80 и 443 (для них нужен root) и следит за воркерами. Воркеры работают без привилегий.
  • Воркер не ждёт конкретного клиента. Он спрашивает у ядра, по каким из его тысяч соединений что-то произошло, и занимается только готовыми.
  • Поэтому воркеров нужно примерно столько, сколько ядер: они не ждут, они считают.
  • И поэтому одна блокирующая операция внутри воркера останавливает ВСЕ его соединения. Медленный код должен жить в бэкенде, а не в прокси.

Знаешь про цикл событий и epoll - листай до «Теперь сам».

Сколько потоков нужно на двадцать тысяч клиентов

Запусти на сервере с nginx:

$  команда
ps aux | grep nginx
↳  выводps aux | grep nginx
root      1234  nginx: master process /usr/sbin/nginx
www-data  1235  nginx: worker process
www-data  1236  nginx: worker process
www-data  1237  nginx: worker process
www-data  1238  nginx: worker process

Один процесс от root и четыре от непривилегированного пользователя. Эти четыре процесса спокойно держат двадцать тысяч одновременных соединений.

Сколько потоков они для этого заводят? Ни одного. Внутри каждого воркера - один поток исполнения, и он ни разу никого не ждёт. Как это возможно - дальше.

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

Представь повара, у которого на плите тысяча кастрюль.

Плохой способ: приставить к каждой кастрюле по человеку, и пусть каждый стоит и смотрит, закипела ли. Тысяча человек, каждому нужно место на кухне, и большую часть времени все они просто стоят. Кухня забита людьми, которые ничего не делают.

Хороший способ: один повар, и на каждой кастрюле таймер. Повар не смотрит на кастрюли вообще - он ждёт звонка. Зазвонило три таймера - подошёл, помешал в трёх кастрюлях, вернулся. Тысяча кастрюль, один человек, и он ни секунды не стоит впустую.

Классический сервер - это первый способ, поток на соединение. nginx - второй. Роль таймеров играет ядро операционной системы: оно само следит за всеми сокетами и говорит, по каким из них появились данные.

Механизм: цикл событий

Внутри каждого воркера крутится один и тот же цикл:

бесконечно:
    спросить ядро: по каким из моих 5000 соединений что-то произошло?
    ядро отвечает: по 17 из них
    обработать эти 17, каждое - маленьким кусочком работы
    вернуться к началу
воркер ядро 17 готовых что произошло? вот эти обработал - и снова спрашивает 5000 соединений висят на воркере и не стоят почти ничего
Ключевое слово - «произошло». Воркер никогда не ждёт конкретного клиента, он спрашивает про всех сразу.

Соединение в состоянии «клиент думает» не стоит почти ничего: несколько сотен байт структуры и запись в списке у ядра. Именно поэтому пять тысяч соединений на воркер - нормальная цифра, а не подвиг.

Кто чем занят

Мастер не обрабатывает запросы вообще. Он делает три вещи: читает конфиг, занимает привилегированные порты (80 и 443 - ниже 1024, для них нужен root) и следит за воркерами. Умер воркер - мастер поднимет новый. Пришла команда reload - мастер прочитает конфиг заново и аккуратно заменит воркеров.

Воркеры делают всю работу и работают без привилегий. Если в nginx найдут дыру, атакующий окажется в процессе от www-data, а не от root. Это не теория: разделение привилегий - причина, по которой веб-серверы вообще так устроены.

Правило

Воркеров имеет смысл ровно столько, сколько процессорных ядер. Больше не поможет: воркеры не ждут, они считают, а посчитать быстрее, чем позволяет железо, нельзя.

В воркере не должно быть ничего медленного. Цикл событий один на всё; любая операция, которая заблокирует его на 10 миллисекунд, заблокирует на эти 10 миллисекунд все пять тысяч соединений.

Разбор: почему поток на соединение не масштабируется

Сравним два способа держать 10 000 соединений на одной машине.

Поток на соединение. Стек потока в Linux по умолчанию - 8 МБ адресного пространства:

10 000 × 8 МБ = 80 ГБ виртуальной памяти

Физически будет занято меньше - страницы выделяются по мере использования, - но остаётся вторая цена, и она хуже. Планировщик ядра обязан по очереди раздавать процессорное время десяти тысячам потоков; каждое переключение контекста стоит порядка микросекунды плюс промахи кеша. При сколько-нибудь живом трафике машина начинает тратить на переключения больше, чем на работу.

Цикл событий. Четыре процесса, четыре стека. Каждое соединение - структура в памяти и запись в списке ядра, сотни байт. Переключений контекста между соединениями нет вовсе: это не разные потоки, это ветки одного цикла.

Отсюда и цифры из начала урока: четыре процесса, двадцать тысяч клиентов, никакого героизма.

Разбор: где модель ломается

Из первого разбора легко унести «цикл событий всегда лучше». Вот случай, где он подводит, и подводит громко.

Воркер держит 4096 соединений. Приходит запрос на файл, которого нет в дисковом кеше. Обычное чтение с диска - операция блокирующая: воркер уходит в неё и не возвращается, пока диск не ответит. На вращающемся диске это около 10 мс.

10 мс блокировки × 100 таких запросов в секунду = 1 секунда простоя в секунду

То есть при сотне промахов кеша в секунду воркер занят ожиданием диска постоянно, и все 4096 его соединений стоят вместе с ним. Снаружи это выглядит как «сайт периодически подвисает целиком», причём процессор снова свободен.

Лечится выносом чтения в отдельный пул потоков:

/etc/nginx/nginx.confhttp
aio threads;

Теперь блокирующее чтение уходит в пул, а цикл событий продолжает крутиться. Это же объясняет, почему nginx принципиально не хочет выполнять твой код в своём процессе: любой пользовательский код рано или поздно во что-нибудь заблокируется, и заплатят за это все соединения воркера сразу.

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

«Сайт замирает на секунду-другую, потом отпускает». Классическая блокировка воркера: медленный диск, сетевая файловая система, тяжёлый модуль. Процессор при этом свободен.

«Поставил worker_processes 64 - лучше не стало». Воркеры не ждут, им нечего параллелить сверх числа ядер. Стало даже хуже: 64 процесса дерутся за 8 ядер.

«nginx работает от root». Значит воркеры тоже, и любая дыра в модуле сразу даёт root. Проверяется тем самым ps aux: у воркеров обязан стоять непривилегированный пользователь.

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

Первое - разбор подвисаний. Когда сайт замирает целиком и одновременно, а процессор свободен, круг подозреваемых сразу сужается до блокировок в воркере: диск, DNS-резолв в рантайме, сторонний модуль. Без модели процессов на это уходит вечер, с моделью - десять минут.

Второе - разговор о безопасности. Вопрос «под кем работает nginx» имеет ровно один правильный ответ, и ps aux показывает его за секунду.

Теперь сам

На сервере 8 ядер. Администратор увидел, что nginx упирается в производительность, и поставил worker_processes 32, а заодно worker_connections 65535. Что из этого поможет, что навредит и чего он, скорее всего, не проверил?

Ответ. worker_processes 32 навредит: воркеры не ждут, и 32 процесса будут конкурировать за 8 ядер, добавив переключений контекста. Правильное значение - auto, то есть 8. worker_connections 65535 само по себе не вредит, но бесполезно, если не поднят лимит открытых файлов процесса - тогда в логе появится worker_connections exceed open file resource limit. А не проверил он, скорее всего, главное: упёрлось ли вообще что-нибудь. Если процессор свободен, а сайт медленный, дело не в числе воркеров, а в блокировке внутри них или в бэкенде - и тюнинг этих двух чисел не даст ничего.

Главное

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

Комментарии

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

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

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