MidТеория7 min

Stateful vs Stateless

Где хранить состояние: Redis для сессий, sticky sessions, Kafka/Cassandra stateful, K8s Deployment vs StatefulSet

Что это

  • Stateless-сервис: между запросами не хранит ничего пользовательски-специфичного в памяти. Любой экземпляр может обработать любой запрос.
  • Stateful-сервис: держит данные в памяти или на локальном диске; разные экземпляры обслуживают разные куски состояния и не взаимозаменяемы.
STATELESS                         STATEFUL
   LB                                LB
   │                                 │ (sharded by key)
   ▼                                 │
 ┌──┐ ┌──┐ ┌──┐                 ┌──┐ ┌──┐ ┌──┐
 │A1│ │A2│ │A3│  (одинаковые)   │K1│ │K2│ │K3│  (каждый уникален)
 └──┘ └──┘ └──┘                 └──┘ └──┘ └──┘
    │                            │    │    │
    ▼                            ▼    ▼    ▼
  Shared state               Локальное состояние
  (БД, Redis)                (диск + реплики)

"Stateless" -- это не "нет состояния вообще", а "нет состояния в процессе; всё в shared-слое".

Когда Stateless лучше

  • Горизонтальное масштабирование. Добавь pod, LB раскинет трафик -- никакой ребалансировки, никаких миграций.
  • Простая устойчивость к падениям. Упал pod -- K8s поднял новый, ничего не потеряно.
  • Rolling updates тривиальны. Убил старый, запустил новый, никаких state migrations.
  • Auto-scaling реактивный. Scale up / down по CPU/RPS без задержек.
  • Отсутствие sticky sessions. LB может round-robin или least-connections без ограничений.

Типичные stateless-компоненты

  • HTTP API backend.
  • GraphQL / REST gateway.
  • Worker, читающий задачи из очереди.
  • Функция (lambda).
  • Transcoder, распакованный по jobs.

Как вынести состояние

Источник состояния Вынести куда
Сессия Redis / signed cookie
Кеш Redis / Memcached
Uploaded files S3 / MinIO
In-memory counters Redis / Prometheus counter
Job state DB / queue

Когда Stateful лучше

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

  • Сами БД, очереди, координаторы. Postgres, Cassandra, Kafka, Zookeeper, etcd -- они и есть shared state; им некуда "выносить".
  • In-memory compute на больших данных. Spark executors, Flink TaskManagers хранят shuffle-данные на диске worker'а.
  • Realtime state per-connection. Game server держит мир в RAM; WebSocket server хранит подключение.
  • Sticky-очень-специфичная обработка: видео-конференция, игровой матч.

Почему Kafka / Cassandra stateful

Они оптимизированы под то, чтобы писать sequentially на локальный диск и реплицировать peer-to-peer. Если бы они хранили данные во внешнем storage, они бы пересекали сеть на каждую запись -- потеряли смысл существования.

Session state: варианты

Классический пример выбора.

Вариант Как работает Плюсы Минусы
In-memory на ноде + sticky sessions LB держит юзера на одной ноде Просто Падение ноды = logout всех на ней; rebalance сложный
Redis централизованно Все ноды читают/пишут Redis Любая нода обслужит юзера Redis = SPOF если не HA
Signed cookie (JWT) Состояние у клиента, сервер stateless Нулевой бэкенд-state Нельзя инвалидировать без blacklist; размер cookie
Разделённая модель Короткоживущий JWT + Redis для revocation Скорость + возможность отозвать Сложнее

Production default: JWT + Redis либо Redis session store + stateless HTTP сервер.

Sticky sessions: когда нужны и когда нет

Sticky = LB привязывает пользователя к конкретной ноде (по cookie / IP hash).

Нужны:

  • Legacy app с in-memory сессией, которую не хотите переписывать.
  • Long-polling / WebSocket -- соединение живёт только на одной ноде.
  • Dev / тесты.

Не нужны и вредны:

  • Если состояние уже в Redis -- sticky лишь усложняет failover и балансировку.
  • Hot users могут перегрузить одну ноду.
  • Rolling deploy становится сложнее (сессии прерываются).

Правило: вынесите state -- и sticky перестанет быть нужен.

Scaling stateful: шардирование

Stateful не скейлится "просто добавив ноду". Нужно:

  1. Sharding key: данные делим по ключу (userId, regionId).
  2. Partition map: знание, какая нода какой shard обслуживает.
  3. Replicas: каждый shard реплицируется на N нод.
  4. Rebalance: при добавлении нод часть shard'ов переезжает.

Алгоритмы:

  • Consistent hashing (Cassandra, DynamoDB). Добавление ноды двигает ~1/N данных.
  • Range-partitioning (HBase, Bigtable). Легко range queries, но hot spots.
  • Fixed partitions + rebalance (Kafka, Elasticsearch). N партиций, ребаланс передвигает partition.

ASCII: ребалансировка при добавлении ноды в consistent hashing

До:               После добавления ноды D:
[A][B][C]          [A][B][C][D]
                    ↓     ↓
              Часть данных с C и B
              переезжает на D.

Kubernetes: Deployment vs StatefulSet

Ресурс Для чего Особенности
Deployment Stateless Interchangeable pods, случайные имена, scale up/down -- мгновенно
StatefulSet Stateful Stable имена (pod-0, pod-1), стабильные PVC, порядок запуска
DaemonSet По одному на ноду Агенты, логи, мониторинг
Job / CronJob One-off / по расписанию Не для постоянных нагрузок

StatefulSet даёт:

  • Пермантентное имя pod -- другие могут к нему обращаться (db-0.db.default.svc).
  • Привязанный PVC -- диск переживает рестарт pod'а.
  • Ordered rollout -- 0 запускается и становится Ready до 1.

Важно: StatefulSet не даёт автоматически репликацию данных. Это должно делать приложение (Postgres streaming replication, Cassandra gossip, Kafka ISR).

Trade-off таблица

Ось Stateless Stateful
Горизонтальное масштабирование Тривиально Требует sharding
Rolling update Без проблем Последовательно, с данными
Failover Новый pod Failover реплики, может быть RTO
Локальный кеш Нет (нельзя полагаться) Да (быстрее)
Сетевой трафик Каждый запрос в shared state Локальный доступ к диску/RAM
Диагностика Легко (лог one-pod) Сложнее (state per node)
Пример K8s Deployment StatefulSet

Код: session stateless vs sticky

<?php

declare(strict_types=1);

/**
 * Stateless HTTP handler: session lives in Redis, any node can serve any user.
 *
 * Trade-offs:
 * - Pros: horizontal scale, no sticky sessions, rolling deploy safe.
 * - Cons: every request = Redis round trip (~1ms); Redis must be HA.
 */
final class SessionMiddleware
{
    public function __construct(
        private readonly \Redis $redis,
        private readonly int $ttl = 3600,
    ) {}

    public function __invoke(Request $req, callable $next): Response
    {
        $sid = $req->cookies->get('sid') ?? $this->newSid();

        $data = $this->redis->get("sess:$sid");
        $session = $data ? unserialize($data) : [];

        $req->attributes->set('session', $session);
        $response = $next($req);

        // Save any mutations back to Redis.
        $this->redis->setex(
            "sess:$sid",
            $this->ttl,
            serialize($req->attributes->get('session'))
        );

        $response->headers->setCookie(Cookie::create('sid', $sid, time() + $this->ttl));
        return $response;
    }

    private function newSid(): string
    {
        return bin2hex(random_bytes(16));
    }
}
WS-хаб -- редкий случай, когда stateful оправдан: каждое сообщение маршрутизируется напрямую в память, без БД.

Антипаттерны

Антипаттерн Почему плохо
In-memory кеш в stateless API без инвалидации Разные ноды видят разное
StatefulSet без репликации на уровне приложения Падение одного pod = потеря данных
Stateful для того, что могло бы быть stateless Лишняя сложность ops
Sticky sessions "на всякий случай" Усложняют scaling и rolling updates
"БД в поде без persistent volume" Перезапуск = всё улетело

Матрица выбора

Задача Stateless Stateful
REST API, CRUD + -
Worker из очереди + -
WebSocket hub - +
Kafka broker - +
БД - +
Realtime game match - +
Прокси / gateway + -
ML inference + (обычно) -

Выводы

  • Stateless -- дефолт для application-слоя: легко скейлить, устойчиво к падениям, K8s-friendly.
  • State выносите в Redis / БД / S3 / очередь.
  • Stateful оправдан только там, где локальный доступ критичен: БД, брокеры, realtime hub'ы.
  • В K8s: Deployment для stateless, StatefulSet для stateful (но репликация -- на уровне приложения).
  • Sticky sessions -- пережиток; с Redis-сессией они не нужны.
  • Масштабирование stateful = sharding + replication + rebalance; это не "добавь pod".