Как известно, SO_REUSEPORT позволяет нескольким сокетам прослушивать один и тот же IP-адрес и порт, он увеличивает количество запросов в секунду от 2 до 3 раз и уменьшает время ожидания (~ 30%) и стандартное отклонение для задержки (8 раз): https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/
В выпуске NGINX 1.9.1 представлена новая функция, которая позволяет использовать SO_REUSEPORT, который доступен в более новых версиях многих операционных систем, включая DragonFly BSD и Linux (ядро версия 3.9 и выше). Эта опция сокета позволяет использовать несколько сокетов для прослушивания одного и того же IP-адреса и комбинации портов. Ядро затем нагрузка выравнивает входящие соединения через сокеты....
Как показано на рисунке, повторное использование увеличивает количество запросов в секунду на 2 до 3 раз и уменьшает как латентность, так и стандартное отклонение для латентность.
SO_REUSEPORT
доступен на большинстве современных ОС: Linux (kernel >= 3.9, поскольку 29 апреля 2013 г.), Free/Open/NetBSD, MacOS, iOS/watchOS/tvOS, IBM AIX 7.2, Oracle Solaris 11.1, Windows (только SO_REUSEPORT
, который ведет себя как два флага вместе SO_REUSEPORT
+ SO_REUSEADDR
в BSD) и может быть на Android: qaru.site/info/15470/...
Linux >= 3.9
- Кроме того, ядро выполняет некоторую "специальную магию" для сокетов
SO_REUSEPORT
, которая не найдена в других операционных системах: Для сокетов UDP он пытается распределить дейтаграммы равномерно, для TCP прослушивающих сокетов, он пытается распространять входящие запросы на подключение(те, которые принимаются путем вызоваaccept()
) равномерно по всем сокетам которые используют один и тот же адрес и комбинацию портов. Таким образом приложение может легко открыть один и тот же порт в нескольких дочерних процессах, а затем использоватьSO_REUSEPORT
, чтобы получить очень недорогую балансировку нагрузки.
Также известно, что во избежание блокировки спин-блокировки и достижимой высокой производительности не должно быть сокетов, которые считывают более 1 потока. То есть каждый поток должен обрабатывать собственные сокеты для чтения/записи.
-
accept()
- это поточно-безопасная функция для одного и того же дескриптора сокета, поэтому его следует защищать с помощью блокировки - , так что конфликт блокировки снижает производительность: http://unix.derkeiler.com/Newsgroups/comp.unix.programmer/2007-06/msg00246.html
POSIX.1-2001/SUSv3 требует accept(), bind(), connect(), listen(), socket(), send(), recv() и т.д., чтобы быть потокобезопасными функциями.возможно, что в стандарте имеются некоторые двусмысленности в отношении их взаимодействие с потоками, но предполагается, что их поведение в многопоточных программах определяется стандартом.
- Если мы используем один и тот же один сокет из многих потоков, тогда производительность будет низкой, потому что сокет защищен блокировкой для потокобезопасного доступа из многих потоков: https://blog.cloudflare.com/how-to-receive-a-million-packets/
Производительность приема меньше по сравнению с однопоточной программа. Это , вызванное конфликтом блокировки в буфере приема UDPбоковая сторона. Поскольку оба потока используют один и тот же дескриптор сокета, они тратить непропорциональное количество времени на борьбу за блокировку вокруг UDP. В этой статье более подробно описывается проблема.
- Подробнее о spin-lock, когда приложение пытается прочитать данные из сокета - "Анализ параллельной производительности UDP-сокетов Linux": http://www.jcc2014.ucm.cl/jornadas/WORKSHOP/WSDP%202014/WSDP-4.pdf
V. K ERNEL ISOLATION
....
С другой стороны, , когда приложение пытается прочитать данные из сокета, он выполняет аналогичный процесс, который описан ниже и представлен на рисунке 3 справа налево:
1) Удалите один или несколько пакетов из очереди приема, , используя соответствующая спин-блокировка (зеленая).
2) Скопируйте информацию в память пользовательского пространства.
3) Отпустите память, используемую пакетом. Эта потенциально изменяет состояние сокета, поэтому два способа блокировки сокет может происходить: быстро и медленно. В обоих случаях пакет несвязанные из сокета, статистика учета памяти обновляется и сокет выдается в соответствии с принятым каналом блокировки.
т.е. когда многие потоки обращаются к одному и тому же сокету, производительность ухудшается из-за ожидания на одной блокировке спина.
У нас есть 2 x Xeon 32 HT-Cores сервер с 64-мя всеми HT-ядрами и две 10-гигабитные Ethernet-карты и Linux (ядро 3.9).
Мы используем RFS и XPS - то есть для одного и того же соединения обработанного TCP/IP-стека (пространство ядра) на одном CPU-Core в качестве прикладного потока (пользовательского пространства).
Существует как минимум 3 способа приема подключений к процессам во многих потоках:
- Используйте один акцепторный сокет, общий для многих потоков, и каждый поток принимает соединения и обрабатывает его
- Используйте один акцепторный сокет в 1 потоке, и этот поток нажимает на дескрипторы дескрипторов подключений к другим потоковым рабочим с помощью потоковой безопасности
- Используйте много сокетов-приемников, которые прослушивают один и тот же
ip:port
, 1 отдельный акцепторный сокет в каждом потоке, и поток, который получает соединение, затем обрабатывает его (recv/отправить)
Чем эффективнее, если мы принимаем много новых TCP-соединений?