Pull to refresh

Comments 24

Скорее всего разработчики конечно сделали профилировку, но вообще интуиция говорит что переустановка события каждые 16 коннектов — это очень часто. Да, качество «размывания» это улучшит, но я бы поставил хотя бы 64 на более-менее нагруженный сервер.

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

Лимиты и `worker_connections` то можно подкрутить. Но тогда зачем вообще другие рабочие процессы нужны, если все один обрабатывает.

Как раз основная проблема - это неравномерное распределение нагрузки по рабочим процессам и, как следствие, ухудшение масштабируемости по ядрам.

Если конкретнее — use epoll, accept_mutex off, reuseport выключен.

а есть причины, по которым стоит выключать reuseport?

Как минимум потому, что у reuseport есть своя проблема с распределением нагрузки по рабочим процессам. Там соединения раскидываются просто в режими round-robin и это неплохо работает, когда все соединения абсолютно одинаковы и дают одинаковую нагрузку. В реальности же часто бывает так, что соединения и запросы пользователей в них могут быть сильно разными. Могут быть соединения в которых вообще нет никаких запросов - их браузер может открывать "про запас", а могут быть тяжелые соединения, создающие большую нагрузку на рабочий процесс. Одни соединения долгоживущие, а другие короткоживущие. В итоге процесс, которому попались тяжелые долгоживущие соединения - будет перегружен, а те, в которых больше легких и короткоживущих - недогружены.

Идеальная ситуация - это когда все процессы разбирают соединения из общей очереди по мере наличия у них свободных ресурсов для их обработки. А это невозможно с reuseport.

Erratum: коллеги поправили, там не round-robin, а хэш от ip и порта сервера и клиента. Что ещё хуже, т.к. теоретически можно попытаться умышленно обмануть механизм и упростить DoS-атаку, если всё это смотрит в интернет.

хэш от ip и порта сервера и клиента

то есть если nginx стоит за прокси, то вся нагрузка ляжет на один воркер?

Скорее всего не ляжет, т.к. исходящий порт для каждого соединения будет выбираться разный. Иначе как идентифицировать какие TCP пакеты какому соединению принадлежат?

Все же непонятно, в чем проблема. Воркеру становится плохо от того, что он постоянно используется? Чем? Если он успевает обрабатывать работу до следующего запроса то чем плохо то, что и следующая попадет к нему? Если не успевает, то он и пробужен не будет и работу получит следующий в очереди. Вроде все логично

Проблема в том, что статистически рабочие процессы стоящие выше в очереди - загружены сильнее, чем те, кто в конце (добавили себя в список слушающих последними при запуске). Всё это приводит к росту задержек из-за того, что в итоге часть ядер процессора оказывается перегружены работой, а другие недогружены.

Погодите… что значит "перегружены"? Процесс же успевает справляться с работой, тогда о какой перегрузке идет речь? В моем понимании "перегрузка" — это когда вся работа наваливается на один процесс, он ее выполнять не успевает, а соседние процессы эту работу не берут, но ведь такое невозможно. Если загруженный процесс не может взять работу, ее возьмет следующий в очереди, просто потому, что в этот момент он будет первый свободный в ней.


Я могу гипотетически предположить, что есть такая работа, когда Задача 1 что-то загрузила в кеш, затем Задача 2 загрузила что-то свое в кеш, что привело в вытеснению данных от Задачи 1, а затем Задача 3 потребовала в кеше то же самое, что Задача 1. Тогда да, имеет смысл, чтобы на первом ядре выполнялись только задачи 1 и 3, а задача 2 — на втором. Но автоматически это же не сделается, это само приложение должно сказать о таком паттерне работы.


Еще можно предположить более равномерный нагрев процессора, когда будут задействованы все ядра, но с другой стороны, можно предположить и пониженное энергопотребление, когда работает только одно.


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

Речь не идет, когда сервер максимально загружен и работает на пределе своих возможностей (чаще всего это не так и какое-то количество ресурсов остается свободно). Вы рассматриваете процесс обработки нагрузки бинарно: справился или не справился, успел - не успел. Но все гораздо сложнее.

Попробую объяснить максимально просто. Представьте, что у вас есть 12 соединений, на которых случились события. Предположим, что все эти соединения оказались в одном процессе. Значит этот процесс будет обрабатывать эти события следующим образом: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 последовательно. В результате наибольшая задержка будет при обработке 12-го события и она будет равна суммарному времени затраченному на обработку всех предыдущих 11 событий.

А теперь представьте, что у нас есть 4 ядра и запущено 4 рабочих процесса и соединения были распределены равномерно по этим процессам (по 3 соединения на процесс).

Тогда для каждого из процессов обработка всех событий будет выглядеть так:
CPU1: 1, 2, 3.
CPU2: 1, 2, 3.
CPU3: 1, 2, 3.
CPU4: 1, 2, 3.

Очевидно, что в этом случае будет ниже как средняя задержка, так и максимальная. 4 ядра работают параллельно друг другу и одновременно обрабатывается до 4-х событий.

Сервер будет отвечать на запросы в среднем быстрее, т.к. в первом случае он задействует вычислительную мощность всего одного ядра, а во втором всех 4-х. Суммарная вычислительная мощность, используемая для обработки событий, во втором случае будет в 4 раза выше.

Предположим, что все эти соединения оказались в одном процессе

так изначальный посыл Mingun, как я его понял, был в том, что по мере увеличения нагрузки на воркер (и увеличения задержек на нём) его шансы «подхватить» новое соединение падают.

К моменту, когда эти шансы упадут заметно - нагрузка на воркер и задержки также возрастут заметно. А именно этого хотелось бы избежать.

Я ещё чуть больше учтною вопрос. Воркер нгинкса (уже внутри себя) тоже использует конкурентность? Тоесть сначала через еполл натягивает себе запросов, а потом одновременно их "крутит" ?

Один рабочий процесс имеет один поток. Поэтому в один момент времени он может из очереди брать и обрабатывать только одно событие. А сколько событий будет в очереди - зависит от того, сколько вернуло их ядро в одном вызове epoll_wait(). По умолчанию один вызов может вернуть до 512 событий.

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

К моменту, когда эти шансы упадут заметно — нагрузка на воркер и задержки также возрастут заметно

проводились тесты? что-то у меня есть сомнения, что в типичном сценарии использования задержки заметно вырастут.

Предположим, что все эти соединения оказались в одном процессе.

Если они оказались в одном процессе, это значит, что соединение 2 пришло после того, как соединение 1 обработалось. Т.е. что в одном процессе, что в разных никакой параллельности не будет, так как сами соединения приходят последовательно, а не одновременно — параллельно обрабатывать просто нечего.


Вот если бы все 12 соединений пришли одновременно и все почему-то оказались в первом процессе… но с чего бы? Первый процесс заберет первое соединение и пойдет его обрабатывать, второе заберет второй и так далее.

Мне кажется, что недоразумение возникает из-за того, что Вы путаете понятия "принял соединение" и "обработал запрос". Воркер может принять соединение и опять уйти в epoll_wait, если в данный момент по этому соединению еще не пришли все данные от клиента или от бекенда ( если он проксирует запрос) и продолжать принимать новые соединения. По ходу обработки одного запроса, воркер может множество раз уходить в ожидание и принимать новые соединения. Но все принятые соединения продолжают на нем висеть, и когда на них появятся данные, воркеру придется их все обрабатывать. Более того, после обработки HTTP запроса, соединение продолжит висеть на воркере в keepalive и соответственно последующие запросы по этому соединению также будут поступать к нему.

Если они оказались в одном процессе, это значит, что соединение 2 пришло после того, как соединение 1 обработалось. Т.е. что в одном процессе, что в разных никакой параллельности не будет, так как сами соединения приходят последовательно, а не одновременно — параллельно обрабатывать просто нечего.

Обработка одного соединения - это не одно событие. Первое событие - лишь о том, что у нас вообще есть новое соединение. Рабочий процесс получает уведомление об этом событии, делает на соединении accept(), добавляет дескриптор нового соединения в ядро для мониторинга и далее может ждать новых событий. С этого момента все события на этом соединении будут обрабатываться только в данном рабочем процессе. А событий таких может быть тысячи: получены новые данные - событие, освободился буфер отправки - событие, случилась ошибка - событие, закрыли соединение - событие.

Более того. Ядро одновременно сообщает о множестве событий. Т.е. одним вызовом рабочий процесс может получить сразу сотни новых событий.

Так работают асинхронные сервера. Один рабочий процесс nginx может обрабатывать события на миллионах соединений.

Вот если бы все 12 соединений пришли одновременно и все почему-то оказались в первом процессе… но с чего бы? Первый процесс заберет первое соединение и пойдет его обрабатывать, второе заберет второй и так далее.

Первый процесс может забрать все 12 соединений. Обработка соединения сводится к вызову accept(). Я даже не уверен, что ядро вообще разбудит другие процессы, если все 12 соединений были им приняты за один такт обработки сетевого трафика. Но это не точно.

В любом случае, любая приоритезация одних рабочих процессов над другими - создает перекос. Как я уже упомянул выше, чаще всего рабочие процессы заняты ожиданием новых событий.

А нынче с HTTP/2 и веб-сокетами у нас часто преобладают долгоживущие соединения. Поэтому событие установки нового соединение - это одно сравнительно редкое событие. Зато каждое такое соединение затем порождает тысячи событий в процессе его обслуживания.

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

Соответственно, как только приходит новое событие, процесс его моментально отработает без всяких задержек. Даже если вдруг придёт сразу много событий, их обработка не займёт настолько много времени, чтобы это существенно повлияло на задержку. Сервер nginx способен обрабатывать сотни тысяч событий в секунду (т.к. обработка связана не с вычислительными затратами, а с операциями ввода-вывода), и лаг даже при 90% загрузке потока будет составлять доли миллисекунды.


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

Вот. Вот это вот объяснение, которого очень не хватает в статье. Что проблема то не просто в том, что один поток забирает все входящие соединения. А в том, что он забирает соединения "с долгом" — и самый первый ждущий накапливает себе долги на обработку, и потом не справляется с ними. Причем набрать "долгов" можно быстро, а "расплачиваться" с ними приходится медленно

Sign up to leave a comment.

Articles