Апстримы и keepalive: пул соединений к бэкенду

Блок upstream — это адресная книга бэкендов под одним именем. А keepalive — это привычка не звонить заново каждый раз, а держать линию открытой.
«Открыть TCP-соединение — дорого. Держать его открытым и переиспользовать — вот секрет низкой задержки прокси.»

Пока мы писали адрес бэкенда прямо в proxy_pass. Это работает для одного сервера, но не масштабируется. Правильный способ — блок upstream: именованная группа бэкендов, на которую ссылается proxy_pass.

Блок upstream

upstream backend {
    server 127.0.0.1:8000;
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
}

server {
    location / {
        proxy_pass http://backend;   # ссылаемся на пул по имени
    }
}

Теперь backend — это группа из трёх процессов приложения. Nginx распределяет запросы между ними (по умолчанию round-robin, см. следующий раздел). Добавить или убрать сервер — это правка одного места.

Keepalive: переиспользование соединений

По умолчанию Nginx открывает новое TCP-соединение к бэкенду на каждый запрос и закрывает после ответа. Установка соединения — это сетевые «рукопожатия», лишняя задержка и нагрузка. keepalive заставляет держать пул уже открытых соединений наготове:

upstream backend {
    server 127.0.0.1:8000;
    keepalive 32;                  # держать до 32 idle-соединений на воркер
}

server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;            # обязательно для keepalive
        proxy_set_header Connection "";    # очистить заголовок Connection
    }
}

Два условия для работы keepalive: proxy_http_version 1.1; (в HTTP/1.0 keep-alive не работает как надо) и пустой заголовок Connection (иначе соединение закроется после ответа).

Схема: с keepalive и без

   БЕЗ keepalive:                  С keepalive:
   запрос-1: connect..ответ..close   запрос-1: connect..ответ
   запрос-2: connect..ответ..close   запрос-2: (готовое).ответ
   запрос-3: connect..ответ..close   запрос-3: (готовое).ответ
   (каждый раз рукопожатие)          (соединение переиспользуется)

Как работает под капотом

Nginx держит пул простаивающих соединений к каждому бэкенду. Когда приходит запрос, воркер берёт готовое соединение из пула вместо нового connect(). Это убирает накладные расходы TCP-рукопожатия (а при TLS к бэкенду — и TLS-рукопожатия) с горячего пути. Число в keepalive 32 — это лимит idle-соединений на воркер, а не общий предел; при пике их может быть больше.

Частые ошибки

  • keepalive без proxy_http_version 1.1. Соединения всё равно будут пересоздаваться — настройка не сработает.
  • Забыть очистить Connection. Клиент мог прислать Connection: close, и без сброса бэкенд закроет соединение.
  • Слишком большой keepalive. Тысячи idle-соединений на бэкенд напрасно жрут его ресурсы; держи число умеренным.

Best practices

  • Используй upstream-блок даже для одного бэкенда — проще добавить второй позже.
  • Включай keepalive с обязательной парой proxy_http_version 1.1 + пустой Connection.
  • Размер пула подбирай по числу воркеров и нагрузке, начиная с 16–32.

Сколько это стоит в миллисекундах

Чтобы прочувствовать ценность keepalive, прикинем цену соединения. Установка TCP-соединения — это трёхстороннее рукопожатие (SYN, SYN-ACK, ACK), то есть полтора round-trip по сети ещё до того, как уйдёт хоть один байт запроса. В пределах localhost это микросекунды, но если бэкенд за сетью — уже миллисекунды, и они умножаются на каждый запрос. А если соединение к бэкенду ещё и по TLS, добавляется TLS-рукопожатие с асимметричной криптографией — это уже десятки миллисекунд и заметная нагрузка на CPU. Keepalive убирает всё это с горячего пути, переиспользуя готовое соединение.

Важно правильно соотнести настройки на обеих сторонах. Лимит keepalive 32 в Nginx должен согласовываться с тем, как долго бэкенд готов держать простаивающее соединение открытым (его keep-alive timeout). Если бэкенд закрывает соединения быстрее, чем Nginx ожидает, ты будешь периодически ловить ошибки на переиспользованном «протухшем» соединении — Nginx их обрабатывает, но это лишняя суета. На очень высоконагруженных системах пул keepalive — один из первых параметров, который тюнят, наблюдая за метриками upstream_connect_time: если оно стабильно около нуля, значит соединения переиспользуются и пул работает. Это классический пример того, как одна-две директивы дают измеримый прирост к задержке всей системы.

Итоги

Блок upstream объединяет бэкенды под одним именем, на которое ссылается proxy_pass. keepalive переиспользует уже открытые соединения, убирая дорогие TCP/TLS-рукопожатия — но требует HTTP/1.1 и сброса Connection. Это база для следующего раздела: распределения нагрузки между бэкендами.

Проверьте себя
1. Что такое блок upstream в Nginx?
AКаталог со статикой
BИменованная группа серверов-бэкендов, на которую ссылается proxy_pass
CКеш ответов
DСписок заблокированных IP
2. Какие две настройки обязательны, чтобы keepalive к бэкенду заработал?
Agzip on и sendfile on
Bproxy_http_version 1.1 и очистка заголовка Connection (proxy_set_header Connection "")
Cssl on и HSTS
Dtcp_nodelay и tcp_nopush