Pull to refresh

Comments 31

При добавлении этой опции начинает ругаться на:
nginx: [emerg] duplicate listen options for 0.0.0.0:80 in /etc/nginx/common.conf:1


cat common.conf
listen 80 reuseport;


Других директив listen нет.
Посмотрите в других блоках server. Директива задает слушающий сокет, который может разделяться множеством виртуальных серверов, поэтому его параметры можно указывать только единожды.
Понятно, то есть так делать нельзя:
server {
        server_name abyrva.lg;
        include common.conf;
}

server {
        server_name glavry.ba;
        include common.conf;
}



Странноватенько.
включение reuseport в 2-3 раза увеличивает количество запросов в секунду
Подтверждаю, пробовал на siege, в моем случае не в 2-3 раза, но на реальном тесте, а не «OK» «hello, world» — мелкий (1,5K) и быстрый реквест, 15 tester, 6 nginx-worker, 4x2 core cpu:
with reuseport
Transactions              5099 hits
Availability              100.00 %
Elapsed time              2.97 secs
Data transferred          7.86 MB
Response time             0.00 secs
Transaction rate          1716.83 trans/sec
Throughput                2.65 MB/sec
Concurrency               14.90
Successful transactions   5099
Failed transactions       0
Longest transaction       0.11
Shortest transaction      0.00 
without reuseport
Transactions              3924 hits
Availability              100.00 %
Elapsed time              2.97 secs
Data transferred          5.89 MB
Response time             0.01 secs
Transaction rate          1320.77 trans/sec
Throughput                1.98 MB/sec
Concurrency               14.90
Successful transactions   3924
Failed transactions       0
Longest transaction       0.13
Shortest transaction      0.00 

nginx: [emerg] reuseport is not supported on this platform

Ядро 3.18.5-x86_64
nginx/1.9.1 из официальных deb репозиториев
оно просто собрано там без SO_REUSEPORT
#if (NGX_HAVE_REUSEPORT)
  ...
#else
  log("reuseport is not supported on this platform, ignored");
#endif

А какой дистрибутив? Вероятно в нём старая версия glibc.
Вы, скорее всего, в курсе. За счёт чего такой выйгрыш? Я так понимаю, чтобы засигналить один воркер, а не все сразу, ОС должна линеаризовать доступ к порту. То есть. по сути, то, что раньше делал accept_mutex, сейчас делает сама ОС. Почему перекладывание ответственности дало такой выйгрыш? Неужели синхронизация nginx была тяжелее, чем ос?
Подозреваю, что «accept» в воркере происходит почти атомарно…
Если вы посмотрите на этот код, то увидите что сокет «клонируется» для каждого воркера — ну а «атомарно» реализовать акцептирование соединения слушающим воркером, не есть трудная задача.
Чтобы положить соединение в очередь или забрать из очереди ядро берет лок на сокете. Один единственный сокет на множество процессов при высокой частоте поступления новых соединений становится бутылочным горлышком. Получается так, что за один лок конкурирует ядро с множеством процессов.

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

accept_mutex тут не причем, это просто еще один лок, который не вносит ничего нового, потому что у нас все равно есть лок в ядре.
Вы меня не поняли, или я вас не понимаю. Физически то данные, пришедшие на один порт, хранятся в одном месте в ядре (не уверен, что это так, не силён в сетях), А сокетов много. И тут опять проблема сериализации доступа, только уже сокетов. А раньше был один сокет, но много слушателей, всё та же проблема сериализации, только уже слушателей. Или я где-то ошибаюсь? Если я прав, то проблема, по факту, не исчезла, просто теперь её решает ядро, а не nginx. Вот я и спрашиваю, почему стало быстрее.
Это вы не поняли, или фраза про бутылочное горлышко вам ничего не говорит? Один listener (с локом) при большом количестве входящих соединений на него — означает неоправданную конкуренцию, т.е. множественное переключение контекста и иже с ним.
«Клонирование» же listener соответственно и очередей (для каждого процесса-воркера по одной) позволяет избавится как минимум от контекст-свича между всеми воркерами (за лок борется только ядро и сам воркер, а не другие).

А то, что вы имеете ввиду под «переложением работы на ядро» — это совершенно другая история.
Во первых, у ядра гораздо больше возможностей организовать оптимальное распределение входящих соединений, да и сам «проброс» установления соединения до каждого listener. А некоторые вещи в принципе можно и нужно организовать только в ядре. А то так и до написания собственного tcp-стека недалеко.
Во вторых, как оно собственно сделано в ядре вы на досуге можете в исходниках оного глянуть — головную боль до завтра я вам гарантирую.
В третьих, использовать reuseport на самом деле очень просто — главная проделанная работа, заключалась в «правильном» вписывании его в nginx, чтобы значит усе стабильно было, например reuseport с reload на лету (когда воркеры перезагружаются для новой конфигурации) и т.д.
у ядра гораздо больше возможностей организовать оптимальное распределение входящих соединений
Не совсем так. Точнее даже совсем не так. Ядро понятия не имеет, когда тот или иной рабочий процесс освободится и сможет забрать соединение. Сейчас Linux просто раскладывает их псевдослучайно. Такое распределение хорошо только в том случае, если у нас все соединения одинаковые с точки зрения ресурсов, которые потребуются для их обработки. Но это более-менее так только в синтетических тестах. В реальности все сложнее. И какому-то из рабочих процессов могут в итоге насыпать тяжелых запросов в то время, как другой будет простаивать.
Я имел ввиду оптимальные в смысле архитектуры проброса от порта до listener, не в смысле оптимизации распределения как такового. Про псевдослучайность последнего знаю, но пока по другому никак. Хотя видел как-то один алгоритм, если не ошибаюсь в solaris, там вводилось понятие веса очереди, «перемещением» в топ «ложились» очереди наименее полные. Т.е. как бы не совсем псевдослучайное, а с учетом «веса» очереди в топе.
Я теперь понял, что никакой сериализации сокетов больше нет, грубо говоря, epoll() и т.д. так же больше не нужны? Теперь ядро само пробуждает потоки по очереди?

Как было:
все воркеры спят -> new socket data -> notification всех воркеров -> все воркеры вступают в борьбу за лок -> один захватывает лок и принимает коннект, остальные засыпают

Как стало:
воркеры спят в accept() -> new socket data -> ядро выбирает любой воркер, пробуждает и т.д.

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

UPD: увидел новые комментарии, стало понятно, что прав
nginx — асинхронный сервер, т.е. будет у какого либо воркера «очередь» длиннее и только. Реально же нужно оценивать совокупность всех вместе…
Я теперь понял, что никакой сериализации сокетов больше нет, грубо говоря, epoll() и т.д. так же больше не нужны? Теперь ядро само пробуждает потоки по очереди?
Epoll никуда не делся, он всегда был отдельный в каждом рабочем процессе. Помимо принятия новых соединений, рабочие процессы делают много другой работы: читают запросы, отправляют ответы, обрабатывают таймауты, устанавливают соединения с бекендами. Рабочий процесс не может ждать на accept(), ему нужно работать с другими событиями, мониторить другие дескрипторы. Поэтому нужен механизм уведомления о событиях.

Как было:
все воркеры спят -> new socket data -> notification всех воркеров -> все воркеры вступают в борьбу за лок -> один захватывает лок и принимает коннект, остальные засыпают
Если интенсивность поступления новых соединения маленькая — то да. Для борьбы с этим эффектом как раз и существует accept_mutex, который отключает нотификацию у отдельных процессов в этом случае. Это можно видеть во втором бенчмарке, его включение снижает нагрузку на CPU.

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

Как стало:
воркеры спят в accept() -> new socket data -> ядро выбирает любой воркер, пробуждает и т.д.
См. выше. Воркерам не позволительно ждать на accept(), им нужно другие соединения обрабатывать.

Но то, что теперь будет уведомляться только один воркер, поскольку он мониторит только свои собственные дескрипторы, а не общие на все процессы — это верно. Но спать он по-прежнему будет в epoll_wait().

Тогда штука реально опасная, особенно учитывая политику распределения коннектов. Какой-нибудь залогиненный пользователь для которого страница генерируется гораздо дольше чем для анонимного всегда будет попадать на один и тот же воркер, забивая его.
Не настолько на самом деле. Псевдослучайное распределение будет раскидывать новые подключения от него случайным образом. С тем же успехом и раньше, установив keepalive соединение, можно было нагрузить запросами один рабочий процесс больше другого. Но в масштабах десятков и сотен тысяч соединений — все это довольно незначительные эффекты.
С точки зрения приложения — слушающий сокет это дескриптор, по сути идентификатор, который передают в системном вызове. Со стороны ядра — это структура с данными, в частности с очередью соединений ожидающих accept'a. Поскольку к этой структуре могут обращаться одновременно из разных потоков, то она защищена локом.

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

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

Да, множество сокетов с SO_REUSEPORT создают множество точек хранения данных пришедших на один адрес-порт.

«просто теперь её решает ядро, а не nginx»
У nginx не было проблемы, проблема всегда была в ядре и её всегда решало ядро. Теоретически разработчики ядра могли бы решить её и без необходимости вносить какие-либо изменения в приложения, но было проще сделать так, как сейчас сделали.
Да, по умолчанию там старая версия eglibc (если не ошибаюсь 2.13, от 2011 года), в которой нет SO_REUSEPORT.
Однако, это также означает, что когда один из рабочих процессов заблокировался на какой-нибудь долгой операции, то это скажется не только на соединениях, которые им уже обрабатывает, но также и на тех, что еще ожидают в очереди.

Можно поподробнее этот момент расписать, что тут имелось ввиду? По-вашему, ОС ждет завершения обработки запроса от процесса?

Кстати, в FreeBSD при REUSE распределение запросов на процессы не равномерное, а «лесенкой». Интересно, как с этим обстоят дела в других ОС.
В случае одного сокета на все рабочие процессы, мы имеем одну очередь соединений, ожидающих accept()-а. Когда один рабочий процесс заблокировался на долгой операции, другой освободившийся процесс может тем временем accept()-ить новые поступившие соединения.

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

Во FreeBSD опция SO_REUSEPORT работает иначе, мы ее там не поддерживаем.
В tengine эта фича где-то полгода назад появилась.
Тут есть большая разница. Одно дело реализовать функцинальность и все будет работать, а другое дело реализовать функциональность и при этом сломать что-нибудь еще, например, релоад на лету без потери соединений. В tengine второй случай.

Тяп-ляп можно сделать быстро всё что угодно, просто у нас другой подход.
Согласен, у вас хороший подход. Но, честно говоря не заметил того бага про который вы сказали.
Я код смотрел. Они просто открывают и закрывают сокеты прямо в рабочих процессах. Все соединения, которые ядро между последним accept()-ом и close() успеет сложить в этот сокет — будут дропнуты.

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

Просто вы не тестировали достаточно тщательно. Можете запустить Tengine, включить там SO_REUSEPORT, подать на него достаточную нагрузку с помощью wrk, а затем под этой нагрузкой поотдавать команды на обновление конфига или на обновление бинарника, посмотреть как он это переживет. Вот nginx при этом не потеряет ни одного запроса.
Ну вот, теперь каждая статья из серии «оптимизация производительности блога на wordpress» будет включать упоминание SO_REUSEPORT

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

1. Безопасность. Другой процесс может незаметно начать слушать на этом же порту и получать часть соединений.
2. Потеря соединений при изменении числа рабочих процессов.

2 разве не обрабатывается в nginx?

Это фундаментальная проблема Linux ядра, в nginx невозможно это нормально исправить. На DragonFlyBSD такой проблемы нет.
Sign up to leave a comment.

Articles