← Назад к вопросам

Почему важно держать распределенные системы в Stateless?

2.7 Senior🔥 111 комментариев
#REST API и микросервисы#SOLID и паттерны проектирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему распределённые системы должны быть Stateless

Краткий ответ

Stateless архитектура критична для распределённых систем, потому что:

  • Масштабируемость — легко добавлять новые серверы
  • Надёжность — падение сервера не теряет данные
  • Балансировка нагрузки — запросы идут к любому серверу
  • Упрощение развёртывания — сервер можно заменить мгновенно

Что означает Stateless

Stateless (без состояния) означает, что сервер:

  • НЕ хранит информацию о клиенте
  • НЕ запоминает предыдущие запросы
  • Каждый запрос обрабатывает независимо
  • Все необходимые данные передаются в самом запросе
Stateless архитектура:

Клиент                    Серверы
  │                    ┌────────┬────────┬────────┐
  ├─ Запрос 1 ────────→│Server 1│Server 2│Server 3│
  │  (с auth token)     └────────┴────────┴────────┘
  │
  ├─ Запрос 2 ────────→│ Может обработать ЛЮБОЙ сервер!
  │  (с auth token)     │ Сервер не помнит Запрос 1
  │
  └─ Запрос 3 ────────→│ Полная информация в самом запросе
     (с auth token)

Статефул (Плохо) vs Stateless (Хорошо)

❌ СТАТЕФУЛ (с состоянием) — проблемы:

public class StatefulServer {
    // Сервер хранит состояние
    private Map<String, UserSession> sessions = new HashMap<>();
    
    public Response handleLogin(String username, String password) {
        // Проверяем пароль
        if (authenticate(username, password)) {
            // Создаём сессию НА СЕРВЕРЕ
            UserSession session = new UserSession(username);
            sessions.put(username, session);  // ← Состояние хранится
            
            return new Response("Login successful");
        }
        return new Response("Login failed");
    }
    
    public Response handleGetProfile(String username) {
        // Проверяем, залогинен ли пользователь
        if (sessions.containsKey(username)) {  // ← Ищем состояние
            UserSession session = sessions.get(username);
            return new Response(session.getUserData());
        }
        return new Response("Not logged in");
    }
}

Проблемы статефул архитектуры:

Сценарий 1: Server 1 упал

  Клиент                   Серверы
    │                   ┌──────────┐
    ├─ Login ──────────→│ Server 1 │ ← Залогинены на Server 1
    │                   └──────────┘
    │                       ⚠️ УПАЛ
    │
    ├─ GetProfile ──────────┌──────────┐
    │                   │ Server 2 │ ← "Кто ты? Я не знаю!"
    │                   └──────────┘
    │                       ❌ ERROR: Not logged in
    │
    Клиент потерял сессию!
Сценарий 2: Масштабирование

  Берём 10 серверов
  Каждый хранит свои сессии
  Проблемы синхронизации:
  - Какой сервер истинный источник?
  - Как синхронизировать состояние?
  - Нужна репликация, кэши, очереди
  - Очень сложно! Дорого! Медленно!

✅ STATELESS (без состояния) — решение:

public class StatelessServer {
    // Сервер НЕ хранит состояние
    // Всё необходимое передаётся в запросе
    
    public Response handleLogin(String username, String password) {
        if (authenticate(username, password)) {
            // Вместо хранения сессии на сервере
            // Выдаём JWT token клиенту
            String token = generateJWT(username);
            
            return new Response("Login successful", token);
        }
        return new Response("Login failed");
    }
    
    public Response handleGetProfile(String token) {
        // Проверяем token (не ищем в памяти)
        try {
            String username = verifyJWT(token);
            // JWT содержит всю информацию (username, expiry, etc)
            
            return new Response(getUserData(username));
        } catch (InvalidTokenException e) {
            return new Response("Invalid token");
        }
    }
}

Преимущества stateless архитектуры:

Сценарий 1: Server 1 упал

  Клиент                   Серверы
    │                   ┌──────────┐
    ├─ Login ──────────→│ Server 1 │ ← Получил JWT token
    │ (получил token)   └──────────┘
    │                       ⚠️ УПАЛ
    │
    ├─ GetProfile ──────────┌──────────┐
    │ (с token)          │ Server 2 │ ← Проверяет token
    │                   └──────────┘      ✅ "Ты кто?" "вот мой token!"
    │                       ✅ OK
    │
    Сессия сохранена!
Сценарий 2: Масштабирование

  Берём 100 серверов
  Каждый использует ОДИН и тот же JWT secret
  Каждый может проверить любой token
  ✅ Просто! Дёшево! Быстро!

Реальный пример: JWT (JSON Web Token)

// Выдача токена
public String generateJWT(String username) {
    return Jwts.builder()
        .setSubject(username)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + 3600000))  // 1 час
        .signWith(SignatureAlgorithm.HS256, secret)
        .compact();
}

// Проверка токена (работает на ЛЮБОМ сервере)
public String verifyJWT(String token) {
    try {
        Claims claims = Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
        
        return claims.getSubject();  // username
    } catch (JwtException e) {
        throw new InvalidTokenException();
    }
}

// Пример запроса
GET /api/profile
Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
//        ^^^^^^^^^^ Все данные в токене, сервер не помнит клиента

Архитектурное сравнение

Статефул (с состоянием):

Client ──→ Load Balancer ──→ Server 1 (хранит сессию Alice)
                          ──→ Server 2 (хранит сессию Bob)
                          ──→ Server 3 (пусто)

Проблемы:
- Sticky sessions (клиент привязан к серверу)
- При добавлении сервера нужна миграция сессий
- Отказоустойчивость: падение сервера = потеря сессий
- Масштабирование сложное и дорогое

Stateless (без состояния):

Client (token: eyJ...) ──→ Load Balancer ──→ Server 1 (проверяет token)
                                          ──→ Server 2 (проверяет token)
                                          ──→ Server 3 (проверяет token)
                                          ──→ Server 4 (добавили новый!)
                                          ──→ Server 5 (добавили новый!)

Преимущества:
- Load Balancer может распределять запросы ЛЮБОМУ серверу
- Добавление сервера тривиально
- Отказоустойчивость: сервер падает, клиент запрашивает другой
- Масштабирование просто и дёшево

Практические примеры Stateless систем

1. REST API (правильно):

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    // Stateless: каждый запрос содержит всю информацию
    @GetMapping("/{id}")
    public Order getOrder(
        @PathVariable String id,
        @RequestHeader("Authorization") String token  // ← Token в запросе
    ) {
        String userId = verifyToken(token);  // Проверяем, не ищем в памяти
        
        Order order = orderService.getOrder(id);
        
        // Авторизация: может ли этот пользователь видеть этот заказ?
        if (!order.getUserId().equals(userId)) {
            throw new AccessDeniedException();
        }
        
        return order;  // Ответ содержит все необходимые данные
    }
}

2. Микросервисная архитектура:

API Gateway (stateless)
    ├─→ User Service (stateless, хранит users в БД)
    ├─→ Order Service (stateless, хранит orders в БД)
    ├─→ Payment Service (stateless, хранит payments в БД)
    └─→ Notification Service (stateless, отправляет письма)

Всё состояние в БД, не в памяти сервера!

3. Kubernetes deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 10  # ← 10 реплик
  template:
    spec:
      containers:
      - name: api
        image: myapp:latest
        # Сервер stateless → можем запустить сколько угодно
        # Все данные в БД/Cache, не в памяти сервера

Где хранить состояние в Stateless системах

Если не на сервере, то ГДЕ?

// 1. БД (источник истины)
Database: PostgreSQL, MySQL

public class User {
    private Long id;
    private String username;
    private String email;
    // Сохраняется в БД
}

// 2. Кэш (для скорости)
Cache: Redis, Memcached

user = cache.get("user:123");
if (user == null) {
    user = database.getUser(123);  // Fallback
    cache.set("user:123", user, 3600);  // Кэшируем на 1 час
}

// 3. Клиент (в очень редких случаях)
Client: localStorage, cookies

token = localStorage.getItem("auth_token");
// Клиент хранит token, отправляет в каждом запросе

Правила Stateless архитектуры

ДЕЛАЙТЕ:

  • Всё состояние в БД или кэше
  • Каждый запрос содержит все необходимые данные
  • Используйте token'ы (JWT) для авторизации
  • Сервер может быть заменён в любой момент
  • Логируйте в централизованное хранилище (ELK, Splunk)

НЕ ДЕЛАЙТЕ:

  • НЕ храните сессии в памяти сервера
  • НЕ предполагайте, что клиент вернётся на тот же сервер
  • НЕ используйте глобальные переменные
  • НЕ полагайтесь на локальные файлы
  • НЕ используйте sticky sessions как основу архитектуры

Заключение

Stateless архитектура критична для распределённых систем:

Масштабируемость — добавляйте серверы как захотите ✅ Надёжность — падение сервера не теряет данные ✅ Простота — не нужна сложная синхронизация состояния ✅ Производительность — load balancer может оптимально распределять нагрузку ✅ Облачные вычисления — Kubernetes, Docker, AWS S3 — всё предполагает stateless

В современном мире cloud-native приложений stateless — это не вариант, это требование!

Почему важно держать распределенные системы в Stateless? | PrepBro