Настройка сетевого стека Linux для высоконагруженных систем

Автор оригинала: John H Patton
  • Перевод

Максимизируем количество входящих соединений

В рамках набора учащихся на курс "Administrator Linux. Professional" подготовили перевод полезного материала.

Приглашаем всех желающих посетить открытый демо-урок
«Практикум по написанию Ansible роли». На этом вебинаре участники вместе с экспертом будут писать, тестировать и отлаживать ansible роли. Это важно для тех, кто хочет автоматизировать настройку инфраструктуры, поскольку это один из инструментов, который это позволяет сделать. Сетевой стек — одна из самых запутанных вещей в Linux. И не только из-за сложности некоторых концепций и терминов, но и из-за изменения смысла некоторых параметров в разных версиях ядра. В этой статье приведена информация для ядра 2.2 и выше, а также, там где это возможно, указано различие между версиями вплоть до 5.5.

О том как изменять параметры ядра, описываемые здесь, можно прочитать в статье Linux Kernel Tuning for High Performance Networking: Configuring Kernel Settings.

Очередь приема и netdev_max_backlog

Каждое ядро процессора перед обработкой пакетов сетевым стеком может хранить их в кольцевом буфере. Если буфер заполняется быстрее, чем TCP-стек обрабатывает пакеты, то пакеты отбрасываются и увеличивается счетчик отброшенных пакетов (dropped packet counter). Для увеличения размера очереди следует использовать параметр net.core.netdev_max_backlog.

net.core.netdev_max_backlog — параметр задается на ядро процессора.

Очередь ожидающих запросов на соединение и tcp_max_syn_backlog

Соединения создаются для SYN-пакетов из очереди приема и перемещаются в очередь ожидания (SYN Backlog Queue). Также соединение помечается как "SYN_RECV" и клиенту отправляется "SYN+ACK". Эти соединения не перемещаются в очередь установленных соединений ожидающих обработки accept() (accept queue) до тех пор, пока не будет получен и обработан соответствующий ACK. Максимальное количество соединений в этой очереди устанавливается параметром net.ipv4.tcp_max_syn_backlog.

Для просмотра очереди приема используйте команду netstat. На правильно настроенном сервере при нормальной нагрузке значение не должно быть больше 1. При большой нагрузке значение должно быть меньше размера очереди ожидания (SYN Backlog):

# netstat -an | grep SYN_RECV | wc -l

Если в состоянии "SYN_RECV" находятся много соединений, то можно также подстроить продолжительность нахождения SYN-пакета в этой очереди.

SYN Cookie

Если SYN cookie отключены, то клиент просто повторяет отправку SYN-пакета. Если включены (net.ipv4.tcp_syncookies), то соединение не создается и не помещается в SYN backlog, но клиенту отправляется SYN+ACK так, как если бы это было сделано на самом деле. SYN cookie могут быть полезны при нормальной нагрузке, но при всплесках трафика некоторая информация о соединении может быть потеряна и клиент столкнется с проблемами, когда соединение будет установлено. Подробнее о SYN cookie можно прочитать в статье Грэма Коула (Graeme Cole) SYN cookies ate my dog (SYN cookie съели мою собаку), в которой подробно объясняется, почему включение SYN cookie на высоконагруженных серверах может привести к проблемам.

Повторы SYN+ACK

Что происходит, если SYN+ACK отправлен, но ответа ACK нет? В этом случае сетевой стек сервера повторит отправку SYN+ACK. Задержка между попытками вычисляется таким образом, чтобы обеспечить восстановление сервера. Если сервер получает SYN и отправляет SYN+ACK, но не получает ACK, то тайм-аут повторной передачи вычисляется по экспоненте (Exponental Backoff) и, следовательно, зависит от количества повторных попыток. Количество повторных попыток отправки SYN+ACK задается параметром ядра net.ipv4.tcp_synack_retries (по умолчанию равно 5). Повторные попытки будут через следующие интервалы: 1с, 3с, 7с, 15с, 31с. При шести попытках последняя будет примерно через 63 секунды после первой. Это позволяет удержать SYN-пакет в очереди ожидания более 60 секунд до истечения времени ожидания пакета. Если очередь SYN backlog мала, то не требуется большого количества соединений, чтобы возникла ситуация, когда полуоткрытые соединения никогда не завершатся и тогда никакие соединения не смогут быть установлены. Установите количество повторных попыток SYN+ACK равным 0 или 1, чтобы избежать такого поведения на высоконагруженных серверах.

Повторы SYN

Несмотря на то что повторные SYN-пакеты отправляются клиентом во время ожидания SYN+ACK, они могут влиять и на высоконагруженные серверы, работающие с прокси-соединениями. Например, сервер nginx, устанавливающий несколько десятков прокси-соединений к бэкенд-серверу, из-за всплесков трафика может на некоторое время перегрузить сетевой стек, а повторные попытки создадут дополнительную нагрузку на бэкэнд как в очереди приема, так и в очереди ожидания (SYN backlog). Это, в свою очередь, может повлиять на клиентские соединения. Повторные попытки SYN контролируются параметром net.ipv4.tcp_syn_retries (по умолчанию 5 или 6 в зависимости от дистрибутива). Ограничьте количество повторных попыток SYN до 0 или 1, чтобы не было долгих повторных попыток отправки в течение 63–130 с.

Более подробно о проблемах с клиентскими соединениями при обратном прокси-сервере читайте в статье Linux Kernel Tuning for High Performance Networking: Ephemeral Ports.

Очередь установленных соединений ожидающих принятия (accept queue) и somaxconn

Очередь запросов на соединение создает приложение, используя listen() и указывая размер очереди в параметре "backlog". Начиная с ядра 2.2 поведение этого параметра изменилось с максимального количества неоконченных запросов на соединение, которое может удерживать сокет, на максимальное количество полностью установленных соединений, ожидающих, пока они будут приняты. Как описано выше, максимальное количество неоконченных запросов на соединение теперь задается с помощью параметра ядра net.ipv4.tcp_max_syn_backlog.

somaxconn и параметр backlog в listen()

Хотя за размер очереди для каждого слушателя отвечает приложение, есть ограничение на количество соединений, которые могут находиться в очереди. Размером очереди управляют два параметра: 1) параметр backlog в функции listen() и 2) параметр ядра net.core.somaxconn, задающий максимальный размер очереди.

Значения по умолчанию для очереди

Значение по умолчанию для net.core.somaxconn берется из константы SOMAXCONN, которая в ядрах Linux вплоть до версии 5.3 имеет значение 128, но в 5.4 она была увеличена до 4096. Однако, на момент написания этой статьи, ядро 5.4 еще не очень распространено, поэтому в большинстве систем значение будет 128, если вы не модифицировали net.core.somaxconn.

Часто приложения для размера очереди по умолчанию используют константу SOMAXCONN, если этот размер не задается в конфигурации приложения. Хотя некоторые приложения устанавливают и свои значения по умолчанию. Например, в nginx размер очереди равен 511, который автоматически усекается до 128 в ядрах Linux до версии 5.3. 

Изменение размера очереди

Многие приложения позволяют указывать размер очереди в конфигурации, указывая значение параметра backlog для listen(). Если приложение вызывает listen() со значением backlog, превышающим net.core.somaxconn, то размер очереди будет автоматически усечен до значения SOMAXCONN.

Потоки

Если очередь большая, то подумайте также об увеличении количества потоков, которые обрабатывают запросы в приложении. Например, установка для nginx очереди ожидания в 20480 для HTTP-listener без достаточного количества worker_connections для управления очередью приведет к тому, что сервер будет отказываться отвечать на запросы на установку соединения.

Соединения и файловые дескрипторы

Системные ограничения

Любое сокетное соединение использует файловый дескриптор. Максимальное количество дескрипторов, которые могут быть созданы в системе, задается параметром ядра fs.file-max. Посмотреть количество используемых дескрипторов можно следующим образом:

# cat /proc/sys/fs/file-nr
1976      12       2048

Вывод показывает, что используется 1976 файловых дескрипторов. Выделено, но не используется 12 (для ядер 2.6+), а максимальное количество — 2048. В высоконагруженной системе значение должно быть достаточно большим, чтобы справиться как с большим количеством соединений, так и с потребностями в файловых дескрипторах других процессов.

Пользовательские ограничения 

Помимо системного ограничения количества файловых дескрипторов, у каждого пользователя есть свои лимиты. Они настраиваются в системном файле limits.conf (nofile) или, при запуске процесса под управлением systemd, в unit-файле systemd (LimitNOFILE). Чтобы увидеть значение по умолчанию запустите:

$ ulimit -n
1024

Для systemd (на примере nginx):

$ systemctl show nginx | grep LimitNOFILE
4096

Настройка

Для настройки системных ограничений установите параметр ядра fs.max-file в максимальное количество файловых дескрипторов, которое может быть в системе (с учетом некоторого буфера). Например:

fs.file-max = 3261780

Для настройки пользовательского лимита установите достаточно большое значение, чтобы хватило сокетам и файловым дескрипторам рабочих процессов (также с некоторым буфером). Пользовательские ограничения устанавливаются в /etc/security/limits.conf, в conf-файле в /etc/security/limits.d/ или в unit-файле systemd. Например:

# cat /etc/security/limits.d/nginx.conf
nginx soft nofile 64000
nginx hard nofile 64000
# cat /lib/systemd/system/nginx.service 
[Unit]
Description=OpenResty Nginx - high performance web server
Documentation=https://www.nginx.org/en/docs/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
LimitNOFILE=64000
PIDFile=/var/run/nginx.pid
ExecStart=/usr/local/openresty/nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
[Install]
WantedBy=multi-user.target

Количество worker'ов

Аналогично файловым дескрипторам, количество worker'ов или потоков, которые может создать процесс, ограничено как на уровне ядра, так и на уровне пользователя.

Системные ограничения

Процессы могут создавать рабочие потоки. Максимальное количество потоков, которые могут быть созданы, задается параметром ядра kernel.threads-max. Для просмотра максимального и текущего количества потоков, выполняющихся в системе, запустите следующее:

# cat /proc/sys/kernel/threads-max 
257083
# ps -eo nlwp | tail -n +2 | \
    awk '{ num_threads += $1 } END { print num_threads }'
576

Пользовательские ограничения

Есть свои ограничения и у каждого пользовательского процесса. Это также настраивается с помощью файла limits.conf (nproc) или unit-файла systemd (LimitNPROC). Для просмотра максимального количества потоков, которое может создать пользователь запустите:

$ ulimit -u
4096

Для systemd (на примере nginx):

$ systemctl show nginx | grep LimitNPROC
4096

Настройка

В большинстве случаев системные ограничения достаточно большие, чтобы справиться с высокой нагрузкой. Однако их можно настроить через параметр ядра kernel.threads-max. Установите его значение в максимальное количество потоков, необходимых системе, плюс некоторый буфер. Например:

kernel.threads-max = 3261780

Как и в случае с nofile, ограничения для пользователей (nproc) устанавливаются в /etc/security/limits.conf, в conf-файле в /etc/security/limits.d/ или в unit-файле systemd. Пример с nproc и nofile:

# cat /etc/security/limits.d/nginx.conf
nginx soft nofile 64000
nginx hard nofile 64000
nginx soft nproc 64000
nginx hard nproc 64000
# cat /lib/systemd/system/nginx.service 
[Unit]
Description=OpenResty Nginx - high performance web server
Documentation=https://www.nginx.org/en/docs/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
LimitNOFILE=64000
LimitNPROC=64000
PIDFile=/var/run/nginx.pid
ExecStart=/usr/local/openresty/nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
[Install]
WantedBy=multi-user.target

Обратный прокси и TIME_WAIT

При большом всплеске трафика прокси-соединения, застрявшие в "TIME_WAIT", суммарно могут потреблять много ресурсов при закрытии соединения. Это состояние говорит, что клиент получил последний FIN-пакет от сервера (или вышестоящего worker'а) и находится в ожидании для корректной обработки пакетов. Время нахождения соединения в состоянии "TIME_WAIT" по умолчанию составляет 2 x MSL (Maximum Segment Length — максимальная длина сегмента), что составляет 2 x 60 с. В большинстве случаев это нормальное и ожидаемое поведение, и значение по умолчанию в 120 с вполне приемлемо. Однако много соединений в состоянии "TIME_WAIT" может привести к тому, что приложение исчерпает эфемерные порты для соединений к клиентскому сокету. В этом случае следует уменьшить FIN тайм-аут.

Управляет этим тайм-аутом параметр net.ipv4.tcp_fin_timeout. Рекомендуемое значение  для высоконагруженных систем составляет от 5 до 7 секунд.

Собираем все вместе

Очередь приема (receive queue) должна быть рассчитана на обработку всех пакетов, полученных через сетевой интерфейс, не вызывая отбрасывания пакетов. Также необходимо учесть небольшой буфер на случай, если всплески будут немного выше, чем ожидалось. Для определения правильного значения следует отслеживать файл softnet_stat на предмет отброшенных пакетов. Эмпирическое правило — использовать значение tcp_max_syn_backlog, чтобы разрешить как минимум столько же SYN-пакетов, сколько может быть обработано для создания полуоткрытых соединений. Помните, что этот параметр задает количество пакетов, которое каждый процессор может иметь в своем буфере, поэтому разделите значение на количество процессоров.

Размер SYN очереди ожидания (SYN backlog queue) на высоконагруженном сервере должен быть рассчитан на большое количество полуоткрытых соединений для обработки редких всплесков трафика. Здесь эмпирическое правило заключается в том, чтобы установить это значение, по крайней мере, на максимальное количество установленных соединений, которое слушатель может иметь в очереди приема, но не выше, чем удвоенное количество установленных соединений. Также рекомендуется отключить SYN cookie, чтобы избежать потери данных при больших всплесках соединений от легитимных клиентов.

Очередь установленных соединений, ожидающих принятия (accept queue) должна быть рассчитана таким образом, чтобы в периоды сильного всплеска трафика ее можно было использовать в качестве временного буфера для установленных соединений. Эмпирическое правило — устанавливать это значение в пределах 20–25% от числа рабочих потоков.

Параметры

В этой статье были рассмотрены следующие параметры ядра:

# /etc/sysctl.d/00-network.conf
# Receive Queue Size per CPU Core, number of packets
# Example server: 8 cores
net.core.netdev_max_backlog = 4096
# SYN Backlog Queue, number of half-open connections
net.ipv4.tcp_max_syn_backlog = 32768
# Accept Queue Limit, maximum number of established
# connections waiting for accept() per listener.
net.core.somaxconn = 65535
# Maximum number of SYN and SYN+ACK retries before
# packet expires.
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_synack_retries = 1
# Timeout in seconds to close client connections in
# TIME_WAIT after receiving FIN packet.
net.ipv4.tcp_fin_timeout = 5
# Disable SYN cookie flood protection
net.ipv4.tcp_syncookies = 0
# Maximum number of threads system can have, total.
# Commented, may not be needed. See user limits.
#kernel.threads-max = 3261780
# Maximum number of file descriptors system can have, total.
# Commented, may not be needed. See user limits.
#fs.file-max = 3261780

И следующие пользовательские ограничения:

# /etc/security/limits.d/nginx.conf
nginx soft nofile 64000
nginx hard nofile 64000
nginx soft nproc 64000
nginx hard nproc 64000

Заключение

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


Узнать подробнее о курсе "Administrator Linux. Professional".

Смотреть вебинар «Практикум по написанию Ansible роли».

OTUS
Цифровые навыки от ведущих экспертов

Похожие публикации

Комментарии 6

    +2
    Думаю было бы интересно не просто увидеть настройку сетевого стека, но и побольше команд для аналитики узких мест.
    Лично мне было бы интересно это в контексте nginx.
    0

    Видимо, за счет именно этих параметров серверные версии отличаются от десктопных

      0

      Многие дефолтные значения даже для серверных ОС необходимо менять, так как они не соответствуют современным реалиям.

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

        Есть ли возможность вернуть былую функциональность без глубоких приседаний с ядром?
          0
          Спасибо за статью. У cloudflare про это тоже интересно написано: blog.cloudflare.com/syn-packet-handling-in-the-wild

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

          Самое читаемое