Session affinity на load-balancer level: cookie-based routing, IP-hash NAT bug, hot-spot проблема при backend death, и stateless альтернатива через externalize state в Redis. 4 сценария показывают когда sticky нужны (WebSocket, in-memory session) и почему вредны для horizontal scale.
Sticky sessions, session affinity или session pinning появляются там, где backend хранит состояние пользователя локально: in-memory session, shopping cart в RAM, WebSocket connection, streaming upload, game match state. Load balancer должен отправлять одного и того же пользователя на одну и ту же ноду, иначе следующий запрос попадет на backend, который ничего не знает о предыдущем.
Паттерн полезен как временная опора для legacy и как техническая необходимость для долгоживущих соединений. Но это плохая стратегия масштабирования по умолчанию. Sticky превращает stateless pool в набор маленьких stateful islands. Упала нода: ее пользователи потеряли session. Добавили новые replicas: старые пользователи все равно сидят на старых replicas. Один большой корпоративный клиент: одна нода перегружена, остальные idle.
Знать sticky важно, чтобы осознанно выбрать между удобством сейчас и операционной ценой завтра. Во многих системах правильное решение не в том, чтобы настроить affinity хитрее, а в том, чтобы вынести состояние в Redis, database, pub/sub adapter или durable actor/object. Sticky стоит использовать там, где физическая привязка к connection неизбежна, и проектировать вокруг нее drain, TTL, failover и наблюдаемость.
::concept{slug="load-balancing"} ::concept{slug="websocket"} ::concept{slug="cache-aside"}
Обычный load balancer распределяет requests между healthy backends. Sticky balancer добавляет память: user A должен ходить в app-1, user B в app-2. Это похоже на ресторан, где постоянного посетителя всегда сажают за один стол, потому что его заказ записан на салфетке именно там. Пока стол свободен, все удобно. Если стол сломался, запись исчезла.
Есть два основных механизма. Cookie-based affinity: load balancer ставит cookie вроде AWSALB или route=app-1, потом читает ее и выбирает backend. Это предпочтительнее, потому что ключ привязки контролируемый и может иметь TTL. IP-hash: backend выбирается как hash от source IP. Это проще, но опасно из-за NAT: тысячи пользователей за одним corporate или mobile NAT выглядят как один IP и попадают в одну ноду.
ADR-style framing: sticky sessions это не feature scalability, а concession к локальному состоянию. Default decision для web/API: stateless backend плюс externalized state. Exception decision: WebSocket/SSE/gRPC streaming, legacy in-memory sessions, streaming uploads, WebRTC signaling и real-time game rooms, где connection или match state физически связан с процессом.
Диаграмма показывает двух клиентов, load balancer с cookie/IP-hash режимами, три backend instances с in-memory sessions и отдельный Redis как preferred externalized state. Backends связаны с Redis, чтобы показать альтернативный путь: любая нода может прочитать session из общего store.
Первый важный элемент: app-1, app-2, app-3 выглядят одинаково по capacity, но sticky делает load неравномерным. Если пользователь закреплен за app-1, round-robin уже не помогает для последующих requests.
Второй элемент: Redis не является частью sticky path, а показывает выход из него. Когда session хранится в Redis, load balancer может снова быть обычным балансировщиком: любой request идет на любую healthy ноду, а состояние читается по session_id.
Третий элемент: client-b показывает NAT проблему. Для IP-hash balancer пользовательский identity заменяется сетевым артефактом. Один source IP не равен один пользователь. Это особенно болезненно для mobile operators, offices, universities и enterprise VPN.
cookie-sticky-happy показывает happy path. User A приходит впервые, LB выбирает app-1, ставит cookie, следующие requests идут туда же, in-memory cart найден. Урок: sticky действительно решает проблему локальной session без изменения приложения. Поэтому его любят при быстрых миграциях legacy.
backend-death-loses-session показывает цену. User A закреплен за app-1, app-1 падает, LB отправляет retry на app-2, но session в RAM исчезла. Пользователь видит login screen или пустую корзину. Урок: sticky не дает durability. Он только повторно выбирает тот же volatile process.
ip-hash-nat-bug показывает skew. 1000 сотрудников за NAT имеют один external IP, hash всегда выбирает app-2, эта нода перегружается. Урок: source IP плохой ключ для user affinity. Даже consistent hashing не исправит проблему, если ключ сам по себе низкой cardinality.
stateless-redis-preferred показывает recommended architecture. LB отправляет request на любую ноду, backend читает session из Redis. Если app-1 падает, следующий request идет на app-2 и получает тот же state. Урок: externalized state возвращает horizontal scaling, failover и rolling deploy.
ADR-001: cookie affinity против IP-hash. Cookie дает control: можно выставить TTL, обновить route, invalidated cookie при death backend, разделить tenants или users. Цена: нужен client с cookie support, а для некоторых native protocols это неудобно. IP-hash не требует cookie и работает на L4/L7, но ломается на NAT, VPN, IPv6 privacy addresses и proxy chains. Production default: cookie-based для browser HTTP, explicit key/header hash для API, избегать source IP affinity кроме простых внутренних сценариев.
ADR-002: sticky против Redis session store. Sticky дешевле внедрить: не надо менять app session code, не нужен Redis cluster. Цена приходит позже: session loss, hot spots, сложный rollout, broken autoscaling. Redis требует отдельной надежной системы, TTL policy, serialization, security и мониторинг, зато делает backend stateless. Для большинства web apps Redis/Spring Session/express-session store выигрывает.
ADR-003: WebSocket sticky против pub/sub adapter. WebSocket connection всегда привязан к конкретной ноде, потому что TCP connection нельзя переместить. Но события для пользователя могут приходить на любую ноду. Sticky удерживает initial connection, а Redis pub/sub, NATS, Kafka или Socket.IO adapter доставляют message туда, где реально сидит connection. Для маленькой системы достаточно sticky; для большого chat/realtime нужно routing layer или broker.
ADR-004: long TTL против fast rebalance. Длинный affinity TTL уменьшает churn и помогает долгим sessions, но закрепляет hot spots и переживает scale-down. Короткий TTL быстрее перераспределяет load, но может ломать legacy in-memory state. Практическое решение: TTL минимально достаточный, connection draining на deploy и metrics по per-backend load, а не только average CPU.
AWS ALB поддерживает stickiness через load balancer generated cookie или application cookie. Это удобно для legacy web apps, но AWS прямо требует учитывать duration и behavior при target removal.
NGINX Plus поддерживает sticky cookie, sticky route, sticky learn. Open-source NGINX часто решает affinity через ip_hash, что надо применять очень осторожно.
HAProxy использует cookie insertion или source balance. Он хорош для explicit server cookie и drain, но все равно не сохраняет in-memory session при death backend.
Envoy умеет hash policy по cookie, header, source IP, query parameter и ring hash. Это мощно для controlled routing, canary и tenant affinity.
Kubernetes Service имеет sessionAffinity: ClientIP. Это простой L4 вариант и хороший пример компромисса: удобно, но NAT skew и ограниченный control.
Socket.IO Redis adapter, Centrifugo, Pusher-like brokers и Cloudflare Durable Objects показывают альтернативы: connection может быть local, но routing state вынесен в отдельный слой.
Sticky как способ не внедрять shared session store. Экономия на Redis быстро превращается в невозможность нормально rollout-ить и scale-ить.
IP-hash для mobile или enterprise traffic. Carrier-grade NAT и corporate proxy создают огромные hot spots.
Sticky без connection draining. При deploy pod удаляется, LB еще отправляет traffic или ломает long-lived connections, users получают 502.
Autoscaling по average CPU. При sticky средняя CPU может быть 40%, но одна нода на 95%, и именно она определяет user experience.
Очень длинный cookie TTL. Пользователь продолжает возвращаться к старому route после topology changes, а balancer тратит время на fallback.
Sticky на gRPC unary или обычный REST без локального state. Affinity включили на всякий случай и ухудшили balance без причины.
Хранение critical state только в RAM. Если state нельзя потерять, sticky не является durability mechanism.
Не используй sticky для обычного REST API, если backend может быть stateless. Session, cart, preferences, CSRF state и rate limits обычно выносятся в Redis, database или signed token.
Не используй sticky для financial, order или payment state. Любое состояние, потеря которого нарушает бизнес-инвариант, должно быть durable до ack клиенту.
Не используй IP affinity для публичного интернета, если у тебя нет доказательств по source IP distribution. NAT сделает распределение хуже, чем round-robin.
Не используй sticky как замену message routing в realtime. Для chat, notifications и multiplayer обычно нужен pub/sub или room coordinator, иначе событие не найдет connection на другой ноде.
Не используй sticky, если rollout должен быть быстрым и частым, а приложение не умеет graceful session migration. Ты заплатишь downtime или странными user-visible сбоями.
Сначала закрой load-balancing: sticky это частный режим балансировки. Затем прочитай service-discovery, потому что balancer выбирает только среди healthy targets. Для realtime переходи к websocket и pub/sub. Для externalized state полезны cache-aside, Redis session stores и patterns вокруг consistency.
Связанные CloudArch материалы: ::concept{slug="load-balancing"} ::concept{slug="service-discovery"} ::concept{slug="timeout-deadline"} ::concept{slug="write-through-write-behind"}
Внешние источники: AWS ALB Target group sticky sessions, NGINX sticky sessions documentation, Envoy hash-based load balancing, Socket.IO multiple nodes guide, Cloudflare Durable Objects documentation.