Sticky-сессии и хеширование: ip_hash и hash

Иногда клиента нужно «приклеить» к одному и тому же бэкенду — например, если там лежит его сессия в памяти. Для этого есть ip_hash и hash.
«Липкая сессия лечит симптом, а не болезнь. Настоящее лекарство — хранить состояние не в памяти процесса, а в общем хранилище.»

Round-robin и least_conn распределяют запросы свободно, и один клиент в разных запросах попадает на разные серверы. Обычно это хорошо. Но если приложение хранит сессию в памяти конкретного процесса, клиента нужно стабильно направлять туда же. Это sticky-сессии (липкие сессии).

Метод ip_hash

upstream backend {
    ip_hash;
    server 10.0.0.1;
    server 10.0.0.2;
    server 10.0.0.3;
}

ip_hash вычисляет хеш от IP-адреса клиента и по нему всегда выбирает один и тот же сервер. Один IP — один бэкенд (пока тот жив).

   client 203.0.113.7  -- hash -->  всегда сервер B
   client 198.51.100.4 -- hash -->  всегда сервер A
   client 192.0.2.50   -- hash -->  всегда сервер C

Гибкий hash по ключу

upstream backend {
    hash $request_uri consistent;   # привязка по URI, а не по IP
    server 10.0.0.1;
    server 10.0.0.2;
}

hash позволяет выбрать ключ привязки самому: $request_uri, $arg_userid и т.п. Параметр consistent включает консистентное хеширование: при добавлении/удалении сервера переедет лишь малая доля ключей, а не все.

Смоделируем хеш-привязку на Python

servers = ["A", "B", "C"]

def pick(client_ip):
    # упрощённый аналог ip_hash: стабильный выбор по хешу
    h = sum(int(p) for p in client_ip.split("."))
    return servers[h % len(servers)]

clients = ["203.0.113.7", "198.51.100.4", "192.0.2.50", "203.0.113.7"]
for ip in clients:
    print(f"{ip:16} -> сервер {pick(ip)}")
print("Заметь: один и тот же IP всегда даёт один и тот же сервер")

Попробуй сам ▶ Один IP детерминированно отображается в один сервер — это и есть суть липкой сессии. Если убрать сервер из списка, привязки массово поедут (поэтому в Nginx и придумали consistent).

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

ip_hash берёт первые октеты IPv4 (или полный IPv6) и считает хеш, отображая его на список серверов. При обычном хешировании удаление одного сервера меняет результат почти для всех ключей — сессии массово теряются. consistent (алгоритм ketama) минимизирует это: при изменении состава апстрима переезжает примерно 1/N ключей. Если клиенты сидят за общим NAT (один внешний IP на офис), ip_hash свалит их всех на один сервер — балансировка перекосится.

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

  • ip_hash при клиентах за NAT/CDN. Сотни пользователей под одним IP уедут на один бэкенд — перекос нагрузки.
  • Лечить sticky тем, что должно жить в общем хранилище. Сессии в Redis/БД убирают саму потребность в липкости.
  • hash без consistent. Любое изменение апстрима обнулит почти все привязки разом.

Best practices

  • Сначала вынеси состояние из памяти процесса (Redis, БД, JWT) — тогда липкость не нужна и масштабирование проще.
  • Если sticky неизбежна, предпочитай hash ... consistent ради плавных переездов.
  • Помни про NAT: ip_hash честен только при уникальных клиентских IP.

Почему индустрия уходит от липких сессий

Sticky-сессии — наследие эпохи, когда состояние пользователя (его корзина, авторизация, временные данные) хранилось прямо в оперативной памяти конкретного процесса приложения. Тогда привязать клиента к «его» серверу было необходимостью. Но у этого подхода есть фундаментальный изъян: как только этот сервер падает или его выводят на деплой, все привязанные к нему пользователи теряют сессию — их разлогинивает, корзина пустеет. Масштабировать и обновлять такую систему больно, потому что каждый сервер несёт уникальное незаменимое состояние.

Современный подход — stateless-архитектура: вынести состояние из памяти процесса в общее хранилище. Сессии кладут в Redis или базу данных, к которым имеют доступ все бэкенды одинаково; или вообще уходят от серверных сессий к самодостаточным токенам (JWT), где всё нужное зашито в самом токене у клиента. Тогда любой запрос можно отдать любому серверу, балансировка снова свободная (round-robin или least_conn), а падение или деплой одного бэкенда никто не замечает. Именно поэтому в новых проектах sticky-сессии стараются не закладывать вовсе. Если же ты унаследовал систему, где они нужны, относись к этому как к временному компромиссу и постепенно выноси состояние наружу — это окупится при первом же масштабировании или обновлении без простоя.

Итоги

Sticky-сессии (ip_hash, hash) привязывают клиента к одному бэкенду — нужны, когда состояние лежит в памяти процесса. Но это костыль: лучше хранить сессии в общем хранилище и балансировать свободно. consistent смягчает переезды при изменении состава. Дальше — проверки здоровья и отказоустойчивость апстрима.

Проверьте себя
1. Зачем нужны sticky-сессии (ip_hash)?
AДля сжатия
BЧтобы стабильно направлять одного клиента на тот же бэкенд, например когда сессия хранится в памяти процесса
CДля HTTPS
DЧтобы ускорить статику
2. В чём опасность ip_hash при клиентах за общим NAT?
AПадает HTTPS
BМножество пользователей с одним внешним IP попадут на один сервер — нагрузка перекосится
CПерестаёт работать gzip
DСоединения не закрываются