Апстримы и 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. Это база для следующего раздела: распределения нагрузки между бэкендами.