Что это
- 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 не скейлится "просто добавив ноду". Нужно:
- Sharding key: данные делим по ключу (userId, regionId).
- Partition map: знание, какая нода какой shard обслуживает.
- Replicas: каждый shard реплицируется на N нод.
- 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));
}
}
Антипаттерны
| Антипаттерн | Почему плохо |
|---|---|
| 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".