← Back to notes

System Design для Principal DevOps:
ёмкость, согласованность, очереди, multi-region


На Principal-уровне system design — это не схема коробок с REST-стрелочками, а разговор о компромиссах под конкретные ограничения: бюджет, команда, SLO, регуляторика. Ниже — плотный разбор концепций.


Оценка нагрузки: back-of-envelope до диаграммы

Первый шаг — цифры. Без них разговор о шардировании и репликации бессмысленен.

Типовой расчёт RPS:

DAU = 10 млн пользователей
Среднее: 5 действий / пользователь / день
RPS = (10_000_000 × 5) / 86_400  578 RPS
Peak = ×3 = ~1 750 RPS

Это read-heavy трафик. Write RPS отдельно считай: если 10% действий — записи, получаешь ~58 write RPS в среднем, пик ~175. Важно разделять, потому что read и write масштабируются по-разному.

Latency budget:

Допустим, цель — p99 < 300 мс для user-facing запроса. Бюджет надо распределить:

Оставшиеся 50 мс — headroom для аномалий. Если в этой таблице нет места для downstream, значит синхронный вызов туда запрещён — нужен либо кэш, либо async fan-out.

Throughput storage:

1 млрд событий / день × 500 байт/событие = 500 ГБ/день
Год: ~180 ТБ
С репликацией ×3: ~540 ТБ

Это сразу ставит вопрос о tiered storage, TTL и archival policy — ещё до выбора базы данных.


CAP и PACELC: что реально означают эти теоремы

CAP theorem (Brewer, 2000): распределённая система при сетевом разделе (Partition) вынуждена выбирать между Consistency и Availability. В реальных системах сеть ненадёжна всегда, поэтому выбор — не абстрактный.

  • CP-системы (ZooKeeper, etcd, HBase): при партиции возвращают ошибку вместо stale-данных. Подходят для координации, distributed locks, leader election.
  • AP-системы (Cassandra, CouchDB, DynamoDB в default-конфиге): при партиции продолжают работать, но возможны расхождения реплик. Подходят для счётчиков, корзин, feed-данных.

PACELC расширяет картину: даже без партиций (нормальный режим) есть trade-off между Latency и Consistency. Cassandra с QUORUM — выше консистентность, выше latency. С ONE — низкая latency, читаешь возможно устаревшие данные.

На практике вопрос не «CP или AP», а: где конкретно допустима stale-data, а где нет?

  • Баланс счёта в банке: не допустима. Нужен strong consistency.
  • Счётчик лайков: допустима расхождение в несколько секунд.
  • Инвентарь на складе при списании: зависит от модели — optimistic lock или saga.

Если на интервью не называешь конкретные trade-offs для конкретного use-case — ответ неполный.


Согласованность данных: модели и паттерны

Strong consistency — все читают одно и то же сразу после записи. Достигается через quorum-записи, single-leader репликацию с синхронным follower или 2PC. Цена: latency растёт, availability снижается при потере узлов.

Eventual consistency — реплики сойдутся, но не гарантируется когда. Требует idempotent writes и conflict resolution (last-write-wins, vector clocks, CRDTs).

Read-your-writes consistency — клиент всегда видит свои собственные записи. Реализуется через sticky sessions на primary или routing writes/reads через один узел для пользователя. Важно для UX: пользователь обновил профиль и сразу должен видеть изменения.

Causal consistency — если событие B зависит от A, то все узлы видят A перед B. Используется в distributed databases с vector clocks (DynamoDB со сторонней логикой, YugabyteDB).

Saga pattern для распределённых транзакций: последовательность локальных транзакций с компенсирующими шагами при откате. Choreography-based (события между сервисами) vs Orchestration-based (центральный координатор). Оба подхода требуют idempotency и tidy failure handling.


Шардирование и репликация

Репликация решает availability и read scalability. Варианты:

  • Single-leader: простой failover, риск — split-brain при network partition без правильного quorum или STONITH.
  • Multi-leader: write locality для multi-region, сложный conflict resolution.
  • Leaderless (Dynamo-style): quorum reads/writes, где W + R > N повышает шанс чтения последней подтверждённой записи. В Cassandra это работает только при корректных consistency levels и без конфликтов, которые разрешаются timestamp/repair-механизмами; это не универсальная linearizability.

Шардирование решает write scalability и объём данных. Стратегии:

  • Hash sharding: равномерное распределение, resharding болезненный (consistent hashing смягчает).
  • Range sharding: удобен для range queries, риск hot shards при монотонных ключах (типичная ошибка с timestamp как ключом шарда).
  • Directory-based: lookup-таблица маппинга ключ → шард, гибко, но lookup сам становится узким местом.

Перед шардированием измеряют фактический предел одного инстанса на своём workload: размер транзакций, индексы, fsync, contention и replication lag часто важнее абстрактного TPS.

Resharding — самое болезненное в шардированных системах. Планируй заранее: consistent hashing или виртуальные шарды (Cassandra vnodes) уменьшают объём перемещаемых данных при добавлении узла.


Кэш-стратегии

Кэш — не «добавим Redis». Каждая стратегия несёт разные гарантии и failure modes.

Cache-aside (lazy loading):

  1. Приложение читает из кэша.
  2. Cache miss → читает из БД, пишет в кэш.
  3. Запись обновляет БД, инвалидирует кэш (или ждёт TTL).

Плюсы: кэшируются только реально запрашиваемые данные, отказ кэша не ломает систему.
Минусы: cache stampede при массовом промахе (thundering herd), данные в кэше могут устареть до TTL.

Thundering herd решают через: mutex lock на populate, probabilistic early expiration (XFetch алгоритм), или background refresh.

Write-through:

  1. Запись всегда идёт через кэш → кэш синхронно пишет в БД.

Плюсы: кэш всегда актуален для записанных данных.
Минусы: write latency выше, кэш заполняется данными которые могут никогда не читаться.

Write-behind (write-back): Запись в кэш, асинхронная flush в БД. Максимальная write performance, но риск потери данных при падении кэша до flush. Подходит для счётчиков, аналитики, не подходит для финансовых операций.

TTL: Каждый ключ должен иметь TTL. Без него кэш со временем заполняется stale-данными. TTL надо выбирать исходя из допустимой staleness: для курса валюты — 30 секунд, для профиля пользователя — 5 минут, для статичного контента — часы.

Hot keys — отдельная проблема. Ключ с топ-100 популярных продуктов читается миллиарды раз в день. Решения: local in-process cache (L1) перед Redis (L2), replication of hot keys across multiple Redis nodes, или разбивка одного ключа на несколько с рандомным суффиксом.


Очереди и event-driven: мифы и реальность

At-least-once delivery — стандарт большинства систем (Kafka, SQS, RabbitMQ). Сообщение доставляется минимум один раз, возможны дубли. Твой consumer обязан быть идемпотентным.

Exactly-once — узкая гарантия конкретной системы. Kafka transactions дают exactly-once processing между Kafka topics при корректной настройке producer/consumer. Внешняя БД или HTTP-вызов требуют отдельной идемпотентности и дедупликации.

Idempotency — фундамент надёжных систем. Паттерны:

  • Idempotency key: клиент генерирует UUID, сервер хранит в БД. Повторный запрос с тем же ключом возвращает кэшированный результат. Stripe использует этот паттерн для payment API.
  • Natural idempotency: операция по природе идемпотентна (SET balance = X vs balance += X). Первое безопасно, второе нет.
  • Deduplication window: SQS FIFO очереди используют MessageDeduplicationId или content-based deduplication в 5-минутном окне. Standard queues остаются at-least-once и могут отдавать дубли.

Poison messages — сообщения которые consumer не может обработать и они блокируют очередь. Обязательно: DLQ (Dead Letter Queue) с настроенным maxReceiveCount, алертинг на DLQ depth, процесс ручного или автоматического replay.

Ordering в очередях:

  • Kafka: порядок гарантирован в рамках партиции. Ключ партиционирования должен быть выбран так, чтобы связанные события попадали в одну партицию (например, user_id).
  • SQS FIFO: порядок гарантирован в рамках MessageGroupId; throughput зависит от режима FIFO, batching, региона и количества независимых message groups.

Consumer lag — сигнал о проблеме. Alert на Kafka consumer lag > 10k сообщений (или > 60 секунд по времени) обычно означает либо медленный consumer, либо спайк объёма. Нужен playbook: добавить partitions, scale consumers, или circuit-break upstream.

Schema evolution: при event-driven архитектуре схема события — это публичный контракт. Используй Avro или Protobuf с schema registry. Backward-compatible изменения (добавить поле с default) — безопасно. Удаление поля или переименование — breaking change, требует версионирования.


API Gateways и авторизация

API Gateway — единая точка входа: TLS termination, rate limiting, auth, routing, request/response transformation. Популярные: Kong, AWS API Gateway, Envoy + custom control plane.

Ключевой вопрос: что делает Gateway, а что делает сервис? Ошибка — переносить бизнес-логику в Gateway. Gateway должен быть stateless и инфраструктурным слоем, не application-слоем.

Rate limiting — реализуется на Gateway уровне. Алгоритмы:

  • Token bucket: позволяет burst, мягче для клиентов.
  • Fixed window counter: прост, но допускает double-burst на границе окна.
  • Sliding window: точнее, но дороже (Redis sorted sets).

Для distributed rate limiting нужен shared state (Redis). Lua-скрипты в Redis для атомарности. При Redis outage — fail-open (пропустить) или fail-closed (отклонить)? Зависит от риска.

AuthZ паттерны:

  • JWT (stateless): сервер не хранит сессии, масштабируется горизонтально. Проблема: revocation. Решение: короткий TTL (15 мин) + refresh tokens, или хранить blacklist в Redis.
  • RBAC (Role-Based Access Control): роли → permissions. Просто в реализации, плохо масштабируется при сложных условиях (ownership, контекст).
  • ABAC (Attribute-Based Access Control): политики на основе атрибутов субъекта, ресурса, контекста. Гибко, но сложно отлаживать. OPA (Open Policy Agent) — стандарт для externalized policy engine.
  • mTLS между сервисами: взаимная аутентификация на transport-уровне. Service mesh (Istio, Linkerd) автоматизирует выпуск сертификатов и ротацию. Overhead на CPU минимален (~1%), но операционная сложность высокая.

Zero-trust: не доверяй сети, проверяй каждый запрос независимо от источника. Реализуется через mTLS + SPIFFE/SPIRE для identity, OPA для authZ, audit logging.


Multi-region и DR

Multi-region — не бесплатная кнопка. Это операционное решение с конкретной стоимостью и сложностью.

Active-passive: трафик идёт в primary-регион, secondary — standby. Failover обычно занимает минуты из-за detection, health checks, DNS/traffic manager и прогрева зависимостей. RPO определяется лагом и durability cross-region репликации; синхронная cross-region запись снижает RPO, но добавляет latency и снижает доступность.

Active-active: трафик распределён между регионами. Каждый регион обрабатывает writes. Требует разрешения конфликтов при concurrent writes. Подходит для globally distributed пользователей, когда latency критична (разница между EU и US — ~100 мс).

Write locality: пользователь всегда пишет в ближайший регион, который является primary для его данных. Остальные регионы читают с репликационного лага. CockroachDB и YugabyteDB реализуют это через geo-partitioning.

Failover trigger: автоматический vs ручной. Автоматический быстрее, но риск split-brain и false positives. Ручной безопаснее, но требует людей в 3 ночи. Компромисс: автоматическое обнаружение + ручное подтверждение (или автоматический failover с immediate alert и возможностью ручного отката).

Failback часто сложнее failover: нужно убедиться что primary догнал secondary по данным, перенаправить трафик, проверить целостность. Без протестированного failback playbook multi-region — иллюзия.

Цифры для DR:

  • RTO (Recovery Time Objective): время до восстановления работоспособности. Active-passive: 5-30 мин. Active-active: секунды.
  • RPO (Recovery Point Objective): допустимая потеря данных. Синхронная репликация может дать RPO около 0 только пока доступен нужный quorum; асинхронная ограничена фактическим лагом и архивированием WAL/логов.

Global routing: Anycast (Cloudflare, AWS Global Accelerator), GeoDNS, или latency-based routing (Route 53). Anycast даёт наименьшую latency, GeoDNS зависит от TTL кэша (может направить не туда при failover до истечения TTL).


Observability в дизайне

Observability — не afterthought. Если не проектируешь её с первого дня, system design неполный.

Три столпа:

  • Metrics: агрегированные числа во времени. Prometheus/Grafana — стандарт де-факто. Правило: каждый критичный user journey должен иметь SLI (Service Level Indicator) метрику.
  • Logs: структурированные (JSON) с correlation ID для трассировки через сервисы. Без correlation ID debugging в микросервисах — боль.
  • Traces: распределённая трассировка запроса (Jaeger, Tempo, AWS X-Ray). Показывает где реально тратится время в цепочке вызовов.

SLI/SLO:

SLI: доля запросов с latency < 300 мс за 5-минутное окно
SLO: SLI >= 99.5% за скользящие 30 дней
Error budget: 0.5% * 30 * 24 * 60 = 216 минут в месяц

Когда error budget исчерпан — все новые фичи заморожены, работа только над reliability. Это делает SLO инструментом принятия решений, а не просто числом в вики.

Alerting policy:

  • Alert только на симптомы (SLO burn rate), не на причины (CPU > 80%).
  • Burn rate alert: если тратишь error budget в 14x быстрее нормы — это критический алерт (1 час до исчерпания). 3x быстрее — warning.

Что должно быть в каждом сервисе:

  • Health endpoint: /health (liveness) и /ready (readiness) с разной семантикой.
  • Request rate, error rate, latency (RED method).
  • Saturation: CPU, memory, connection pool utilization, queue depth.
  • Structured logs с trace_id, span_id, user_id (не PII), service, version.

Когда использовать

Синхронный запрос (REST/gRPC):

  • Результат нужен немедленно для ответа пользователю.
  • Downstream надёжен и имеет низкую latency (< 50 мс p99).
  • Объём не требует буферизации.

Очередь / async:

  • Результат не нужен немедленно (email, отчёт, расчёт).
  • Нужно сглаживать спайки: producer быстрее consumer.
  • Нужен retry без блокировки клиента.
  • Fan-out: одно событие обрабатывают несколько потребителей.

Шардирование:

  • Одна БД упирается в write throughput или объём данных (> 5 ТБ на инстанс начинает болеть на операционном уровне).
  • Read replicas уже добавлены и всё равно не хватает.

Кэш:

  • Read-heavy трафик с повторяющимися запросами.
  • БД latency приемлема, но нужно снизить нагрузку.
  • Данные относительно стабильны (staleness допустима).

Multi-region:

  • Регуляторные требования (data residency в EU).
  • Пользователи глобально распределены и latency критична.
  • Требования к availability превышают то, что даёт один регион (> 99.95% uptime).
  • RTO < 5 минут — один регион не справится при AZ outage, нужна cross-region redundancy.

Микросервисы:

  • Реальные bounded contexts с разными командами и скоростью изменений.
  • Разные требования к scale/isolation для разных компонентов.
  • Не «потому что модно».

Типичные ошибки

1. Схема без чисел. «Добавим кэш и будет быстро» без ответа на: какой hit rate ожидаешь? Какой TTL? Как измеришь эффект? — это не аргумент.

2. Autoscaling как замена capacity planning. Autoscaling реагирует с задержкой (2-5 минут на запуск нового инстанса). При внезапном 10x spike за 30 секунд — не поможет. Нужен headroom и load shedding.

3. Confusion между DR и backup. Backup — восстановление данных после их потери или повреждения. DR — восстановление работоспособности сервиса. Разные процессы, разные RTO/RPO, разные тесты.

4. Multi-region без модели записи. «Мы в двух регионах» без ответа на «кто primary для write, как разрешаем конфликты» — маркетинг, не архитектура.

5. Range sharding с монотонным ключом. Sharding по timestamp или auto-increment ID создаёт hot shard: все записи идут на один шард, остальные простаивают. Используй hash sharding или добавляй salt к ключу.

6. Exactly-once как цель по умолчанию. Достижимо только в очень узких условиях и с высокой стоимостью. Правильная цель: at-least-once + idempotent consumer. Дешевле и надёжнее.

7. JWT без стратегии revocation. Long-lived JWT (24h+) невозможно отозвать до истечения. Если токен скомпрометирован — нет способа заблокировать. Короткий TTL + refresh token rotation — обязательно.

8. Synchronous fan-out. Один запрос делает 20 downstream HTTP вызовов синхронно. Tail latency = max из всех 20. Один тормозящий сервис ломает весь запрос. Используй async fan-out или scatter-gather с timeout и partial results.

9. Игнорирование schema migration. Новая версия сервиса пишет новые поля, старая версия их не понимает. При rolling deploy это гарантированный инцидент. Expand-contract pattern: сначала добавь поле в read-код (expand), потом начни писать, потом удали старое.

10. Observability после факта. «Добавим мониторинг позже» означает что при первом инциденте тебе не хватит данных для RCA. Correlation IDs, structured logs и RED-метрики должны быть в день один.


Альтернативы

Вместо шардирования:

  • Вертикальное масштабирование (дешевле и проще до определённого предела — современный сервер с 128 core / 4 ТБ RAM держит огромную нагрузку).
  • Read replicas для read-heavy нагрузки.
  • CQRS: разделение read и write моделей, read-сторона масштабируется независимо.
  • Managed NewSQL (CockroachDB, Spanner, YugabyteDB) — шардирование прозрачно для приложения, SQL-интерфейс сохранён.

Вместо кастомного API Gateway:

  • Managed: AWS API Gateway, Kong Cloud, Apigee.
  • Service mesh (Istio, Linkerd) для east-west трафика между сервисами.
  • BFF (Backend for Frontend): отдельный aggregation layer для каждого типа клиента (web, mobile, partner API).

Вместо Kafka:

  • SQS/SNS для простых use cases без необходимости replay и stream processing.
  • Google Pub/Sub, Azure Service Bus — managed с меньшим операционным overhead.
  • Temporal.io для workflow orchestration с надёжностью и state management — заменяет очередь + custom retry logic + state storage.

Вместо Redis для кэша:

  • Memcached если нужен простой key-value без persistence и сложных структур данных.
  • In-process cache (Caffeine в JVM, functools.lru_cache в Python) — нулевая latency, без сетевых hop, ценой памяти и несогласованности между инстансами.
  • DynamoDB DAX для кэширования DynamoDB с прозрачным API.

Вместо микросервисов:

  • Monolith with modular boundaries (Majestic Monolith): хорошая архитектура внутри, простой деплой. Stripe долго работал на этой модели.
  • Modular monolith → selective extraction: декомпозиция только тех компонентов, которые реально требуют независимого масштабирования или деплоя.

Вместо active-active multi-region:

  • Multi-AZ в одном регионе покрывает 99% случаев availability требований (AWS SLA по регионам — несколько 9).
  • Read replicas в другом регионе для read-locality без сложности cross-region writes.
  • Pilot light / warm standby: минимальный footprint в DR-регионе, rapid scale-up при failover.
System Design для Principal DevOps: ёмкость, согласованность, очереди, multi-region | Aleksandr Suprun