На 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):
- Приложение читает из кэша.
- Cache miss → читает из БД, пишет в кэш.
- Запись обновляет БД, инвалидирует кэш (или ждёт TTL).
Плюсы: кэшируются только реально запрашиваемые данные, отказ кэша не ломает систему.
Минусы: cache stampede при массовом промахе (thundering herd), данные в кэше могут устареть до TTL.
Thundering herd решают через: mutex lock на populate, probabilistic early expiration (XFetch алгоритм), или background refresh.
Write-through:
- Запись всегда идёт через кэш → кэш синхронно пишет в БД.
Плюсы: кэш всегда актуален для записанных данных.
Минусы: 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 = Xvsbalance += 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.