Модуль: 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 QPS

Read/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 Gbps

Memory для кэша (горячая выборка — кэшируем активные 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
  → 204

Senior-нюансы:

  • Пагинация курсором, а не 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:

KafkaRabbitMQ
Модельdistributed log (партиции, offset)broker с очередями (AMQP)
Потреблениеconsumer сам двигает offset, реплей возможенсообщение удаляется после ack
Throughputочень высокий (миллионы msg/s)средний
Порядокв пределах партициив пределах очереди
Routingпростой (топик/партиция)богатый (exchanges: direct/topic/fanout/headers)
Use caseevent streaming, логи, аналитика, event sourcingtask 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
  □ Резюме ключевых решений и их цены
  □ «Что бы улучшил с большим временем»

Частые ошибки кандидатов#

  1. Прыгают к решению без требований. Рисуют боксы на первой минуте. Сначала — requirements и scope.
  2. Не считают цифры. «Будет много трафика» вместо «90k QPS на пике → нужен кэш + шардирование». Без оценок нельзя обосновать архитектуру.
  3. Считают ради счёта. Цифры не привязаны к решениям. Каждое число должно вести к выводу.
  4. Нет deep dive. Остаются на уровне коробок-стрелок. Senior обязан углубиться хотя бы в один компонент с алгоритмами.
  5. Buzzword-bingo. «Поставим Kafka, Redis, Cassandra, Kubernetes» без обоснования зачем. Технология должна следовать из требования.
  6. Молчание про trade-offs. Любое решение имеет цену. Не назвал цену — выглядишь как middle, который знает один правильный ответ.
  7. Over-engineering для несуществующего масштаба. Multi-region active-active для стартапа на 10k юзеров. Масштабируй под реальные числа.
  8. Игнорируют consistency. Не проговаривают модель консистентности и replication lag.
  9. Не управляют временем. 30 минут на API, ноль на масштабирование. Следи за тайм-боксами.
  10. Не слушают интервьюера. Игнорируют подсказки и направляющие вопросы. Интервью — это диалог.
  11. Забывают про single points of failure и про мониторинг/observability.
  12. Не обсуждают 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-кластер — всё это деньги, и решение должно быть экономически разумным.
  • Управление коммуникацией. Ведёт интервью как диалог, проговаривает предположения, реагирует на подсказки, держит тайм-боксы.