Модуль: System Design · Уровень: Senior
TL;DR#
System design интервью оценивает не «знание решения», а способ мышления: как ты переводишь размытые требования в конкретные числа, как обосновываешь выбор компонентов и где честно проговариваешь trade-offs. Senior-кандидата отличает не количество названных технологий, а умение управлять временем, задавать уточняющие вопросы и связывать решение с бизнес-ограничениями.
Работающий фреймворк на 45 минут:
requirements → estimations → API → data model → high-level → deep dive → scale → trade-offs
(5') (5') (3') (3') (8') (12') (5') (4')Главные провалы кандидатов: сразу рисуют боксы без требований, не считают цифры (QPS/storage), не делают deep dive, перечисляют технологии без обоснования и молчат про trade-offs. Senior всегда говорит «зависит от…» и явно называет, от чего именно.
Теория#
Почему именно фреймворк#
Задача намеренно открытая («Спроектируйте Twitter»). Без структуры кандидат либо тонет в деталях одного компонента, либо растекается по верхам. Фреймворк — это контейнер для мысли: он гарантирует, что за 45 минут ты покроешь весь стек от требований до масштабирования и оставишь интервьюеру материал для оценки. Произноси шаги вслух — интервьюер должен видеть управляемый процесс, а не поток сознания.
Шаг 1. Requirements (≈5 мин)#
Разделяй два класса требований.
Functional requirements — что система делает (use cases):
- «Пользователь публикует твит», «читает ленту», «подписывается».
- Сужай scope. Скажи: «Я сфокусируюсь на трёх core-сценариях: post tweet, home timeline, follow. Notifications, search, DM — вне scope, верно?» Это сразу показывает senior-уровень: ты управляешь объёмом, а не пытаешься объять всё.
Non-functional requirements — каким система должна быть:
- Availability vs Consistency — что важнее? Для ленты соцсети допустима eventual consistency (твит появится через секунду — ок). Для баланса счёта — strong consistency обязательна.
- Latency — целевой p99. «Лента должна отдаваться за < 200 ms p99».
- Throughput — пиковый QPS.
- Durability — допустима ли потеря данных? Для аналитических событий — да, для платежей — нет.
- Read/Write ratio — критично для архитектуры. Соцсети обычно 100:1 … 1000:1 в пользу чтения.
- Consistency model — strong / eventual / read-your-writes / monotonic reads.
Senior-приём: спрашивай про масштаб бизнеса до того, как считать. «Сколько DAU? Это стартап или Twitter-scale?» Решение для 10k и 500M пользователей — принципиально разное.
Шаг 2. Estimations / back-of-the-envelope (≈5 мин)#
Цель — обосновать архитектурные решения числами, а не точная бухгалтерия. Считай в порядках величины.
Базовые константы для прикидок:
- В сутках ≈ 86 400 с ≈ 10^5 с (округляй для скорости).
- 1 млн запросов/сутки ≈ 12 QPS (1e6 / 86400).
- Пик обычно 2–3× среднего.
Пример: Twitter-like, 300M DAU.
QPS чтения ленты (предположим каждый юзер читает ленту 10 раз/день):
reads/day = 300e6 * 10 = 3e9
avg QPS = 3e9 / 1e5 = 30 000 QPS
peak QPS = ~90 000 QPS (×3)QPS записи твитов (каждый постит 2 раза/день):
writes/day = 300e6 * 2 = 6e8
avg QPS = 6e8 / 1e5 = 6 000 QPS
peak = ~18 000 QPSRead/Write ≈ 30000/6000 = 5:1 на уровне QPS, но если учитывать fan-out чтений (один твит читают тысячи подписчиков) — реальная асимметрия гораздо больше → это аргумент за read-оптимизированную архитектуру (precompute timeline).
Storage (текст твита ≈ 300 байт + метаданные ≈ 200 байт ≈ 0.5 KB):
твитов/день = 6e8
storage/day = 6e8 * 0.5 KB = 300 GB/day
за год = 300 GB * 365 ≈ 110 TB/year (только текст)Медиа меняет картину на порядки: 10% твитов с картинкой по 200 KB → 6e7 * 200 KB ≈ 12 TB/day → ~4.4 PB/year. Вывод: медиа в blob storage + CDN, текст в БД.
Bandwidth (egress на чтение ленты, 20 твитов × 0.5 KB = 10 KB на ответ):
peak read QPS * payload = 90 000 * 10 KB = 900 MB/s ≈ 7.2 GbpsMemory для кэша (горячая выборка — кэшируем активные timelines, правило 80/20):
храним последние 20 твитов на активного юзера, активных ~20% = 60M
60e6 * 20 * 0.5 KB = 600 GB → шардированный Redis-кластерLatency numbers every programmer should know (порядки, держи в голове):
L1 cache reference ~0.5 ns
Branch mispredict ~5 ns
L2 cache reference ~7 ns
Mutex lock/unlock ~25 ns
Main memory (RAM) reference ~100 ns
Compress 1 KB (Zippy/Snappy) ~3 µs
Send 1 KB over 1 Gbps network ~10 µs
Read 1 MB sequentially from RAM ~250 µs
Round trip within same datacenter ~500 µs
Read 1 MB sequentially from SSD ~1 ms
Disk seek (HDD) ~10 ms
Read 1 MB sequentially from HDD ~20 ms
Round trip CA ↔ Netherlands ↔ CA ~150 msПрактические выводы из этих чисел:
- RAM ≈ в 100 000× быстрее HDD-seek → кэш в памяти оправдан почти всегда.
- Cross-DC round-trip 150 ms → синхронная георепликация убивает latency; для глобальных систем — async-репликация + geo-DNS.
- SSD seek ≈ 100 µs (нет механики) против HDD 10 ms → для random access всегда SSD.
- Один RTT внутри DC = 0.5 ms → минимизируй число последовательных сетевых хопов (N+1 запросов).
Правило senior: каждое число должно вести к решению. Не считай ради счёта — связывай: «900 MB/s egress → нужен CDN и кэш, один сервер не вытянет».
Шаг 3. API design (≈3 мин)#
Опиши контракт между клиентом и системой. Это фиксирует функциональные требования в конкретные эндпоинты.
POST /v1/tweets
body: { text, media_ids[] }
auth: Bearer <token>
→ 201 { tweet_id, created_at }
GET /v1/timeline/home?cursor=<opaque>&limit=20
→ 200 { tweets[], next_cursor }
POST /v1/users/{id}/follow
→ 204Senior-нюансы:
- Пагинация курсором, а не offset. Offset-пагинация (
?page=5) ломается при вставках/удалениях и деградирует на больших offset (БД сканирует все пропущенные строки). Cursor (?after=<tweet_id|timestamp>) — стабильный и O(1) по индексу. - Идемпотентность записи.
POST /tweetsсIdempotency-Keyв заголовке → защита от двойной отправки при retry. Обязательно для платежей и заказов. - Versioning (
/v1/) с самого начала. - REST vs gRPC vs GraphQL — REST для публичных API, gRPC для internal service-to-service (бинарный protobuf, HTTP/2, стриминг, ниже latency), GraphQL когда клиентам нужна гибкая выборка полей (мобильные, под-/over-fetching).
Шаг 4. Data model (≈3 мин)#
Опиши ключевые сущности, связи и где они хранятся.
User (user_id PK, name, created_at, ...)
Tweet (tweet_id PK, author_id FK, text, created_at, media_ids[])
Follow (follower_id, followee_id) -- граф подписок
Timeline(user_id, tweet_id, score) -- precomputed (Redis/Cassandra)Решения, которые принимаются здесь:
- Выбор ключа партиционирования.
Tweetшардируем поtweet_id(равномерно) или поauthor_id(локальность чтения твитов автора, но риск hot-partition на знаменитостях). - Денормализация под паттерн чтения. В NoSQL дублируем
author_nameвTweet, чтобы не делать JOIN на каждом чтении ленты. - Граф подписок — отдельное хранилище (специализированная graph DB или просто индексированная таблица с двумя направлениями
followingиfollowers).
Шаг 5. High-level architecture (≈8 мин)#
Рисуй сверху вниз: клиент → edge → сервисы → данные. Начинай просто, усложняй по мере обсуждения.
┌─────────┐
Clients ──►│ CDN │ (статика, медиа)
└────┬────┘
│
┌────▼─────┐
│ DNS / │ (geo-routing)
│ GSLB │
└────┬─────┘
│
┌──────▼───────┐
│ Load Balancer│ (L7)
└──────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ API GW │ │ API GW │ │ API GW │ (auth, rate-limit)
└───┬────┘ └───┬────┘ └───┬────┘
└──────────┼──────────┘
┌───────┼────────┐
▼ ▼ ▼
┌─────────┐┌────────┐┌──────────┐
│ Tweet ││ Timeline││ User/Graph│ (stateless services)
│ Service ││ Service ││ Service │
└────┬────┘└───┬────┘└─────┬─────┘
│ │ │
┌────▼───┐ ┌──▼────┐ ┌────▼────┐
│ Tweet │ │ Redis │ │ Graph │
│ DB │ │ Cache │ │ DB │
│(shard) │ │(shard)│ │ │
└────────┘ └───────┘ └─────────┘
│
┌────▼─────────────────┐
│ Message Queue (Kafka) │ ──► fan-out workers ──► Timeline cache
└───────────────────────┘Опиши главный data flow словами: «При публикации твита Tweet Service пишет в БД, кладёт событие в Kafka. Fan-out workers разбирают событие и вставляют tweet_id в timeline-кэши подписчиков. При чтении ленты Timeline Service отдаёт готовый список из Redis — O(1), без вычислений на лету».
Fan-out on write vs on read — классический trade-off этой задачи:
- Fan-out on write (push): при публикации сразу раскладываем в кэши подписчиков. Чтение мгновенное. Но твит знаменитости с 100M подписчиков = 100M записей — «celebrity problem».
- Fan-out on read (pull): лента собирается в момент запроса из твитов тех, на кого подписан. Дёшево на запись, дорого на чтение.
- Гибрид (senior-ответ): push для обычных юзеров, pull для знаменитостей. На чтении мёржим precomputed timeline + свежие твиты celebrities.
Шаг 6. Deep dive (≈12 мин) — самая важная часть#
Здесь интервьюер оценивает глубину. Выбери 1–2 компонента и копай. Обычно интервьюер сам направит («давай подробнее про timeline»). Если нет — предложи сам: «Хочешь, углубимся в fan-out или в шардирование БД?»
Что демонстрировать в deep dive:
- Конкретный алгоритм (consistent hashing, fan-out, rate-limiting через token bucket).
- Обработку edge cases (hot partition, thundering herd, cache stampede).
- Конкретные технологии с обоснованием.
- Bottleneck identification: «Узкое место — fan-out write на знаменитостях. Решаем гибридом».
Шаг 7. Scale (≈5 мин)#
Найди узкие места и устрани:
- Stateless-сервисы → горизонтальное масштабирование за LB.
- БД → read replicas (для read-heavy), затем sharding (когда не помещается/упирается в write).
- Кэш → шардированный кластер, защита от stampede.
- Async → тяжёлые операции (fan-out, нотификации, обработка медиа) в очередь.
- CDN → весь статический/медиа egress.
- Multi-region → geo-DNS + локальные реплики для latency и DR.
Шаг 8. Trade-offs (≈4 мин)#
Резюмируй ключевые решения и их цену. Это то, что отделяет senior от middle.
- «Выбрал eventual consistency для ленты → выигрыш в availability и latency, цена — твит может появиться у подписчика с задержкой в 1–2 с. Для соцсети приемлемо».
- «Гибридный fan-out → сложнее код и merge на чтении, зато решает celebrity problem».
Строительные блоки#
Load Balancer#
L4 (transport, TCP/UDP) vs L7 (application, HTTP):
- L4 — балансирует по IP:port, не смотрит в содержимое. Очень быстрый, низкий overhead, не терминирует TLS. Не умеет content-based routing.
- L7 — видит HTTP: маршрутизация по path/header/cookie, sticky sessions, TLS-терминация, retry, rate-limit. Дороже по CPU. Это nginx/Envoy/ALB.
Алгоритмы:
- Round-robin — по кругу. Прост, не учитывает нагрузку.
- Weighted round-robin — учёт мощности нод.
- Least connections — на ноду с наименьшим числом активных соединений (хорош при разной длительности запросов).
- Least response time — least-conn + latency.
- IP hash / consistent hash — стабильная привязка клиента к ноде (sticky без cookie).
Health checks: active (LB периодически дёргает /healthz) и passive (LB наблюдает за реальными ответами, выкидывает ноду после N ошибок). Различай liveness (жив ли процесс) и readiness (готов ли принимать трафик — прогрет ли кэш, есть ли коннект к БД). Не отправляй трафик на ноду, которая live, но not ready.
Cache#
Стратегии:
- Cache-aside (lazy loading): приложение читает кэш; miss → читает БД → кладёт в кэш. Самая частая. Минус: первый запрос медленный, риск stale-данных. Запись идёт в БД + инвалидация ключа.
- Write-through: запись идёт в кэш, кэш синхронно пишет в БД. Кэш всегда консистентен, но запись медленнее. Хорош для read-heavy с требованием свежести.
- Write-back (write-behind): запись в кэш, асинхронный сброс в БД батчами. Очень быстрая запись, высокий риск потери данных при падении кэша. Для счётчиков/метрик.
- Read-through: кэш сам подгружает из БД при miss (логика в кэш-слое, а не в приложении).
Eviction:
- LRU — выселяем давно не используемое. Дефолт, хорош для temporal locality.
- LFU — выселяем редко используемое. Лучше когда есть стабильно «горячий» набор, но дороже по учёту.
- TTL — по времени жизни; комбинируется с LRU/LFU.
- FIFO / random — простые, реже подходят.
Инвалидация («одна из двух сложных задач в CS»):
- TTL — просто, но окно stale-данных.
- Write-through invalidation — на каждую запись инвалидируем/обновляем ключ.
- Event-based — БД/CDC публикует изменения, кэш-слой реагирует.
Проблемы и защита:
- Cache stampede / thundering herd: ключ протух → тысячи запросов одновременно бьют в БД. Лечат: (1) probabilistic early expiration, (2) request coalescing / single-flight (один поток грузит, остальные ждут — в Go
golang.org/x/sync/singleflight), (3) lock на ключ при перезагрузке. - Cache penetration: запросы несуществующих ключей минуют кэш. Лечат cache-of-misses (negative caching) или Bloom filter.
- Hot key: один ключ создаёт перегрев одной ноды. Лечат локальной репликой ключа / клиентским кэшем.
Redis — основной выбор: in-memory, структуры данных (string, hash, sorted set для лент/leaderboard, set для подписок), pub/sub, TTL, репликация, Cluster mode (хэш-слоты, 16384). Single-threaded на команды → атомарность простых операций без локов, но осторожно с O(N)-командами (KEYS, большие SMEMBERS).
CDN#
Кэширует статику и медиа на edge-узлах ближе к пользователю → снижает latency и разгружает origin. Pull (CDN сам тянет с origin при первом запросе) или push (заранее заливаем).
- Кэшировать: картинки, видео, JS/CSS, иногда API-ответы для анонимов.
- Управление:
Cache-Control,ETag, версионирование URL (app.a1b2c3.js) для cache busting. - Senior-нюанс: cache invalidation на CDN медленная (purge — это операция на тысячах edge); поэтому версионируй URL, а не инвалидируй.
Message Queue#
Развязывает producer и consumer: асинхронность, сглаживание пиков (buffering), отказоустойчивость, fan-out.
Kafka vs RabbitMQ:
| Kafka | RabbitMQ | |
|---|---|---|
| Модель | distributed log (партиции, offset) | broker с очередями (AMQP) |
| Потребление | consumer сам двигает offset, реплей возможен | сообщение удаляется после ack |
| Throughput | очень высокий (миллионы msg/s) | средний |
| Порядок | в пределах партиции | в пределах очереди |
| Routing | простой (топик/партиция) | богатый (exchanges: direct/topic/fanout/headers) |
| Use case | event streaming, логи, аналитика, event sourcing | task queues, сложный routing, RPC |
Senior-различие: Kafka — это persistent log, можно перечитать историю и иметь несколько независимых consumer groups; RabbitMQ — это очередь задач, сообщение «исчезает» после обработки.
Delivery semantics:
- At-most-once — может потеряться, не дублируется (fire-and-forget). Для метрик где потеря ок.
- At-least-once — не теряется, но возможны дубли при retry. Дефолт большинства систем. Требует идемпотентных consumers (дедуп по message_id / upsert).
- Exactly-once — без потерь и дублей. Дорого и узко применимо: Kafka даёт его только внутри Kafka (transactions + idempotent producer); end-to-end exactly-once на практике эмулируется через at-least-once + идемпотентность на стороне потребителя.
Прочее: DLQ (dead-letter queue) для «ядовитых» сообщений после N retry; backpressure и consumer lag как ключевая метрика мониторинга; ordering гарантируется только внутри партиции → ключ партиционирования = то, по чему нужен порядок (например, по user_id).
Database: SQL vs NoSQL#
SQL (Postgres, MySQL):
- Строгая схема, ACID, транзакции, JOIN, сложные запросы.
- Вертикальное масштабирование легко, горизонтальное (write) — сложно.
- Когда: финансы, заказы, любая предметка с реляционными инвариантами и транзакциями.
NoSQL:
- Key-value (Redis, DynamoDB) — простой доступ по ключу, огромный throughput.
- Document (MongoDB) — гибкая схема, вложенные документы.
- Wide-column (Cassandra, HBase) — write-heavy, линейное масштабирование, tunable consistency.
- Graph (Neo4j) — связи (соцграф, рекомендации).
- Обычно: горизонтальное масштабирование из коробки, денормализация, eventual consistency, query-first моделирование (схема под паттерн доступа).
- Когда: огромный объём, простые паттерны доступа, write-heavy, гибкая схема.
Senior-формулировка: «SQL — strong consistency и гибкие запросы ценой сложного горизонтального масштабирования; NoSQL — масштаб и доступность ценой ослабленной консистентности и ограниченных запросов. Выбор диктуется паттернами доступа и требованиями к консистентности, а не модой».
Репликация:
- Leader-follower (master-slave): запись в лидера, чтение с реплик. Масштабирует чтение. Реплики асинхронны → replication lag → возможно чтение устаревших данных (нарушение read-your-writes). Лечат: критичные чтения с лидера, либо semi-sync репликация.
- Multi-leader: запись в несколько лидеров (multi-region). Проблема — write conflicts (нужны LWW / vector clocks / CRDT).
- Leaderless (Dynamo-style): запись/чтение в N нод, кворумы. Консистентность через R + W > N (например N=3, W=2, R=2).
- Sync vs async: sync — durability/consistency ценой latency; async — быстро, но окно потери данных при падении лидера.
Sharding (партиционирование):
- Range-based: диапазоны ключей по шардам. Удобно для range-запросов, но риск hot-shard (например, шардирование по времени — весь свежий трафик в один шард).
- Hash-based:
hash(key) % N. Равномерно, но range-запросы невозможны, и resharding при изменении N перетряхивает почти все ключи. - Directory-based: lookup-таблица key→shard. Гибко, но сам lookup — единая точка отказа/узкое место.
Проблемы шардирования: cross-shard joins/transactions (дорого — нужен distributed transaction или денормализация), rebalancing, hotspots.
Consistent hashing — решает проблему resharding в hash-based. Ключи и ноды мапятся на кольцо (хэш-пространство). Ключ принадлежит первой ноде по часовой стрелке. При добавлении/удалении ноды перемещается только ~1/N ключей, а не все.
node A
/ \
key3 key1
| |
node C ---- node B
\ /
key2
Добавили node D между A и B:
переедут только ключи в дуге (A, D] — остальные на месте.Virtual nodes (vnodes): каждая физическая нода представлена множеством точек на кольце → равномернее распределение и плавный rebalance (без vnodes одна нода может получить непропорционально большую дугу). Используется в Cassandra, DynamoDB.
CAP и PACELC#
CAP-теорема: при сетевом разделении (Partition) распределённая система может сохранить либо Consistency (все узлы видят одни данные), либо Availability (каждый запрос получает ответ), но не оба.
- CP — при partition жертвуем доступностью: отказываем в ответе, чтобы не отдать неконсистентные данные (HBase, etcd, ZooKeeper, традиционный RDBMS в кластере).
- AP — при partition отвечаем, рискуя устаревшими данными (Cassandra, DynamoDB, Riak).
- «CA без P» — миф для распределённых систем: сеть может разорваться всегда, P нельзя «не выбрать». CA — это про single-node.
Важно: CAP касается только поведения во время partition. В нормальном режиме система не обязана жертвовать ничем — отсюда расширение:
PACELC: If Partition → choose between Availability and Consistency; Else (нормальный режим) → choose between Latency and Consistency.
- Даже без сбоев есть фундаментальный trade-off latency ↔ consistency: синхронная репликация на кворум = выше consistency, но выше latency.
- Cassandra/Dynamo: PA/EL (доступность при partition, latency в норме).
- Полностью консистентная БД: PC/EC.
Senior-вывод: реальные системы предлагают tunable consistency (Dynamo/Cassandra — выбор R/W/consistency level на запрос), и ты выбираешь точку на спектре per-use-case, а не для всей системы целиком.
Чеклист (тайм-бокс на 45 мин)#
[ 0–5 мин ] REQUIREMENTS
□ Уточнить functional (core use cases), сузить scope
□ Non-functional: consistency vs availability, latency p99, durability
□ Спросить масштаб бизнеса: DAU/MAU, read/write ratio
□ Зафиксировать вслух scope с интервьюером
[ 5–10 мин ] ESTIMATIONS
□ QPS (avg + peak ×3), отдельно read и write
□ Storage/day и /year (+ медиа отдельно)
□ Bandwidth (egress на пике)
□ Memory для кэша (80/20)
□ Каждое число → архитектурный вывод
[ 10–13 ] API
□ Ключевые эндпоинты, сигнатуры
□ Cursor-пагинация, идемпотентность записи, versioning
□ REST/gRPC/GraphQL — обосновать
[ 13–16 ] DATA MODEL
□ Сущности, связи, ключи
□ Partition key, денормализация под чтение
[ 16–24 ] HIGH-LEVEL
□ Диаграмма: client → CDN → LB → services → data
□ Stateless services
□ Главный data flow словами (read path + write path)
[ 24–36 ] DEEP DIVE (главное!)
□ Выбрать 1–2 компонента, копать алгоритмами
□ Edge cases: hot partition, stampede, celebrity problem
□ Назвать bottleneck и решение
[ 36–41 ] SCALE
□ Replicas → sharding, кэш-кластер, async-очереди, CDN, multi-region
[ 41–45 ] TRADE-OFFS + WRAP-UP
□ Резюме ключевых решений и их цены
□ «Что бы улучшил с большим временем»Частые ошибки кандидатов#
- Прыгают к решению без требований. Рисуют боксы на первой минуте. Сначала — requirements и scope.
- Не считают цифры. «Будет много трафика» вместо «90k QPS на пике → нужен кэш + шардирование». Без оценок нельзя обосновать архитектуру.
- Считают ради счёта. Цифры не привязаны к решениям. Каждое число должно вести к выводу.
- Нет deep dive. Остаются на уровне коробок-стрелок. Senior обязан углубиться хотя бы в один компонент с алгоритмами.
- Buzzword-bingo. «Поставим Kafka, Redis, Cassandra, Kubernetes» без обоснования зачем. Технология должна следовать из требования.
- Молчание про trade-offs. Любое решение имеет цену. Не назвал цену — выглядишь как middle, который знает один правильный ответ.
- Over-engineering для несуществующего масштаба. Multi-region active-active для стартапа на 10k юзеров. Масштабируй под реальные числа.
- Игнорируют consistency. Не проговаривают модель консистентности и replication lag.
- Не управляют временем. 30 минут на API, ноль на масштабирование. Следи за тайм-боксами.
- Не слушают интервьюера. Игнорируют подсказки и направляющие вопросы. Интервью — это диалог.
- Забывают про single points of failure и про мониторинг/observability.
- Не обсуждают failure modes: что при падении ноды, сети, кэша, лидера БД.
Вопросы на собеседовании#
В: В чём разница между functional и non-functional requirements, и почему non-functional определяют архитектуру? О: Functional — что система делает (use cases: «опубликовать твит»). Non-functional — каким она должна быть: availability, consistency, latency, throughput, durability. Именно non-functional диктуют архитектуру: требование strong consistency исключает eventual-репликацию; целевой latency < 100 ms заставляет кэшировать и держать данные близко к пользователю; read/write ratio 1000:1 толкает к precompute и read-репликам. Функциональность можно реализовать множеством способов — non-functional сужают выбор до конкретных компонентов.
В: Прикиньте storage и QPS для системы на 100M DAU, каждый постит 1 раз/день, читает 20 раз/день. О: Writes/day = 100e6 → avg ≈ 100e6/86400 ≈ 1160 QPS, peak ×3 ≈ 3500. Reads/day = 2e9 → avg ≈ 23k QPS, peak ≈ 70k. Storage: пост ≈ 0.5 KB → 100e6 × 0.5 KB = 50 GB/day ≈ 18 TB/year (текст). Вывод: read-heavy (read/write ≈ 20:1) → кэш + read-реплики; 70k QPS чтения нельзя обслужить из одной БД → шардирование/кэш; медиа считаем отдельно и кладём в blob + CDN.
В: Fan-out on write vs on read для ленты — что выберете? О: Гибрид. Fan-out on write (push) даёт мгновенное чтение, но ломается на знаменитостях (100M записей на один твит — celebrity problem). Fan-out on read (pull) дёшев на запись, дорог на чтение. Решение: push для обычных пользователей (precompute timeline в Redis), pull для знаменитостей. На чтении мёржим precomputed timeline с свежими твитами тех celebrities, на кого подписан юзер. Trade-off: сложнее код и merge-логика на чтении, зато оба паттерна нагрузки обслуживаются эффективно.
В: Объясните CAP и PACELC. Почему «CA» — некорректный выбор для распределённой системы? О: CAP: при сетевом разделении выбираешь либо Consistency, либо Availability. P (возможность partition) нельзя «отключить» — сеть рвётся всегда, поэтому реальный выбор только между CP и AP; «CA» описывает лишь single-node систему. CAP описывает только режим partition. PACELC расширяет: при Partition — A vs C, иначе (Else) — Latency vs Consistency. То есть даже без сбоев синхронная репликация повышает консистентность ценой latency. Cassandra — PA/EL, строго консистентная БД — PC/EC.
В: Что такое cache stampede и как с ним бороться?
О: Популярный ключ протухает → лавина одновременных запросов бьёт в БД (thundering herd), которая может лечь. Защита: (1) request coalescing / single-flight — один поток грузит значение, остальные ждут результат (в Go — singleflight); (2) probabilistic early expiration — обновляем ключ чуть раньше TTL с вероятностью, растущей к моменту истечения; (3) distributed lock на перезагрузку ключа; (4) stale-while-revalidate — отдаём устаревшее значение, пока фоном обновляем.
В: Зачем consistent hashing и что добавляют virtual nodes?
О: При hash(key) % N изменение числа нод N перетряхивает почти все ключи (massive resharding). Consistent hashing мапит ключи и ноды на кольцо; ключ идёт к первой ноде по часовой стрелке. При добавлении/удалении ноды переезжает только ~1/N ключей. Проблема базового варианта — неравномерность: одна нода может получить большую дугу. Virtual nodes решают это: каждая физическая нода = много точек на кольце → равномерное распределение и плавный rebalance. Применяется в Cassandra, DynamoDB.
В: Kafka или RabbitMQ — когда что? О: Kafka — distributed log: высокий throughput, persistence с возможностью реплея, несколько независимых consumer groups, порядок в пределах партиции. Для event streaming, аналитики, event sourcing, логов. RabbitMQ — broker с богатым routing (exchanges) и моделью «сообщение исчезает после ack». Для task queues, сложной маршрутизации, RPC. Ключевое отличие: Kafka хранит историю и позволяет перечитать, RabbitMQ — это очередь задач, где сообщение удаляется после обработки.
В: At-least-once vs exactly-once delivery — что реально использовать? О: At-least-once — практический дефолт: сообщение не теряется, но возможны дубли при retry. Требует идемпотентных consumers — дедуп по message_id или upsert-операции, чтобы повтор не менял результат. Exactly-once дорого и узко: Kafka даёт его транзакционно только внутри Kafka; end-to-end exactly-once на практике строится как at-least-once + идемпотентность на стороне потребителя. То есть «exactly-once» обычно означает «at-least-once с дедупликацией».
В: Чем L4-балансировщик отличается от L7 и когда выбирать какой? О: L4 балансирует по IP:port, не заглядывая в payload — очень быстрый, низкий overhead, но без content-based routing и TLS-терминации. L7 видит HTTP: маршрутизация по path/header/cookie, TLS-терминация, retry, rate-limit, sticky sessions — гибче, но дороже по CPU. L4 — для предельного throughput и не-HTTP протоколов; L7 — когда нужна логика на уровне приложения (API gateway, canary по заголовку, terminating TLS).
В: Как обеспечить read-your-writes consistency при leader-follower репликации с lag? О: Проблема: записал в лидера, тут же читаешь с асинхронной реплики и не видишь своей записи. Решения: (1) читать критичные/собственные данные с лидера в течение окна после записи; (2) запоминать timestamp/LSN записи у клиента и читать только с реплики, догнавшей этот LSN (monotonic/consistent prefix read); (3) sticky-routing пользователя на одну реплику; (4) semi-synchronous репликация, где лидер ждёт ack хотя бы одной реплики перед подтверждением записи.
На что копают на senior+#
- Trade-offs, а не решения. Senior не отвечает «правильно», он называет варианты и их цену, и обосновывает выбор под конкретные требования. Фраза-маркер: «это зависит от X, поэтому я выбираю Y».
- Числа → решения. Оценки не для галочки: каждое число должно влечь архитектурный вывод. Умение быстро прикинуть порядки (QPS, storage, bandwidth) на back-of-the-envelope.
- Failure modes и устойчивость. Что при падении ноды/сети/лидера/кэша? SPOF, graceful degradation, retry с backoff и jitter, circuit breaker, idempotency, timeout budget.
- Consistency-нюансы. Не просто «strong vs eventual», а read-your-writes, monotonic reads, consistent prefix, replication lag, tunable R/W/N кворумы.
- Bottleneck identification. Умение показать пальцем на узкое место («fan-out на знаменитостях», «hot partition по времени») и предложить точечное лечение.
- Глубина в deep dive. Конкретные алгоритмы (consistent hashing с vnodes, token bucket для rate-limit, single-flight, fan-out-гибрид), а не названия технологий.
- Эволюция системы. Как решение масштабируется от 10k до 100M юзеров поэтапно; что менять и в каком порядке; когда НЕ масштабировать (избегать premature optimization).
- Data modeling под паттерн доступа. Query-first для NoSQL, выбор partition key с учётом hotspots и cross-shard операций.
- Observability и операционка. Метрики (p99 latency, consumer lag, cache hit rate, error rate), SLO/SLA, как деплоить без даунтайма, как делать миграции схемы на живой системе.
- Cost awareness. Senior+ учитывает стоимость: PB-хранилища, cross-region трафик, гигантский Redis-кластер — всё это деньги, и решение должно быть экономически разумным.
- Управление коммуникацией. Ведёт интервью как диалог, проговаривает предположения, реагирует на подсказки, держит тайм-боксы.