Стройте ответ вокруг владения состоянием, лимитов, отказа и восстановления. Определение становится инженерным ответом только в конкретном production-сценарии.
Вопросы и ответы
29 подробных ответов
01Шаг 1. Прояснить требования
junior
Никогда не начинайте проектировать сразу. Сначала уточните, что именно строим. Разделите требования на две группы.
Функциональные требования — что система делает (фичи):
- Какие основные сценарии? (например: «сократить ссылку» и «перейти по короткой ссылке»).
- Кто пользователи? Сколько их? Гео-распределение?
- Что НЕ входит в скоуп? (аналитика, аутентификация, биллинг — часто можно отбросить, явно проговорив это).
Нефункциональные требования (NFR) — какими свойствами обладает:
- Масштаб: сколько пользователей / запросов / данных.
- Доступность (availability): важнее ли «всегда отвечать» или «отвечать корректно»? (CAP).
- Latency: жёсткие требования (p99 < 100 мс) или нет.
- Консистентность: допустима ли eventual consistency, или нужна строгая.
- Durability: можно ли терять данные (логи vs платежи).
- Read/Write ratio: преобладает чтение или запись.
💡 Задавайте вопросы вслух и фиксируйте ответы — это уже половина оценки. Пример: «Нам нужна аналитика по кликам? Если да — это меняет модель данных. Предположу, что в скоупе только базовый счётчик».
02Шаг 2. Оценить масштаб (back-of-the-envelope)
junior
Грубые прикидки, которые определят архитектуру. Считайте вслух, округляйте агрессивно.
Что оценить:
- QPS (запросов в секунду): средний и пиковый (пик ≈ 2-3× среднего).
- Объём данных: размер одной записи × число записей × горизонт хранения.
- Соотношение чтение/запись (read:write) — определяет, нужны ли реплики чтения и кэш.
- Пропускная способность (bandwidth): QPS × размер ответа.
- Хранилище (storage): рост в год.
Полезные числа для счёта в уме:
| Величина | Значение |
|---|---|
| Секунд в сутки | ~86 400 ≈ 10⁵ |
| Месяц | ~2.5 млн сек |
| 1M запросов/сутки | ≈ 12 QPS в среднем |
| 1B запросов/сутки | ≈ 12 000 QPS |
Пример расчёта (URL shortener): 100M новых ссылок/мес → 100M / 2.5M сек ≈ 40 записей/сек. Read:write = 100:1 → 4000 чтений/сек. Размер записи ~500 байт → 100M × 500B = 50 ГБ/мес → 600 ГБ/год → за 10 лет ~6 ТБ.
💡 Вывод из чисел сразу формулируйте: «40 записей/сек — это легко для одной БД. 4000 чтений/сек — добавим кэш и реплики чтения».
03Шаг 3. Определить API
junior
Опишите контракт сервиса — это фиксирует функциональность и помогает интервьюеру понять модель. REST/gRPC, основные эндпоинты:
POST /api/v1/urls {long_url, custom_alias?, ttl?} -> {short_url}
GET /{short_key} -> 301/302 redirect
Проговорите: методы, параметры, идемпотентность (POST идемпотентен?), пагинацию для списков (cursor-based, не offset на больших данных), авторизацию (api_key / токен).
Контракт должен включать не только happy path: коды ошибок, лимиты размера, таймауты и поведение повторного запроса. Для создания ресурса клиент может прислать Idempotency-Key; сервер сохраняет ключ вместе с результатом и возвращает тот же ответ при ретрае. Так API становится проверяемой границей системы, а не списком случайных URL.
request → validate/auth → domain operation → stable response
│ │
4xx contract retry-safe effect
04Шаг 4. Модель данных и выбор БД
junior
- Опишите ключевые сущности и связи (таблицы / коллекции).
- Выберите тип БД и обоснуйте:
- SQL (Postgres, MySQL): сложные связи, транзакции, строгая консистентность, аналитические запросы, JOIN. Берите по умолчанию, если нет причины не брать.
- NoSQL key-value / wide-column (DynamoDB, Cassandra): огромный масштаб записи, простой паттерн доступа по ключу, горизонтальное шардирование «из коробки», eventual consistency.
- Документная (MongoDB): гибкая схема, вложенные документы.
- In-memory (Redis): кэш, счётчики, rate limiting, очереди, leaderboard.
- Сразу подумайте о ключе шардирования (по чему партиционируем).
05Шаг 5. High-level архитектура
junior
Опишите словами (или схемой) компоненты и поток запроса:
[Client] -> [DNS] -> [Load Balancer] -> [API / App Servers (stateless)]
|-> [Cache (Redis)]
|-> [Database (+ read replicas)]
|-> [Message Queue] -> [Workers]
[CDN] -> [Blob Storage (S3)]
Проведите типичный запрос по этому пути от клиента до БД и обратно.
Не рисуйте каждый известный компонент. Начните с минимального пути, который выполняет требования, и добавляйте кэш, очередь или реплики только когда расчёты показывают необходимость. Для каждого ребра назовите протокол, синхронность, таймаут и источник истины; для каждого stateful-компонента — владельца данных, репликацию и восстановление.
Затем проведите два сценария: обычный запрос и отказ. Например: cache miss идёт в БД и заполняет кэш; при недоступной БД запрос быстро завершается по таймауту, circuit breaker ограничивает каскад, а метрики показывают деградацию. Схема ценна только вместе с поведением потоков.
06Шаг 6. Детализация ключевых компонентов
middle
Углубитесь в 1–2 части, которые определяют корректность или масштаб именно этой задачи: генерацию ключей, ранжирование ленты, доставку сообщений либо схему кэширования. Не пересказывайте весь стек одинаково поверхностно.
Для выбранной части зафиксируйте данные и инварианты, пройдите алгоритм на конкретном запросе, оцените сложность и разберите хотя бы один сбой. Например, для резервирования билета инвариант — sold + held ≤ capacity; одной диаграммы сервисов недостаточно, нужно показать атомарный UPDATE ... WHERE available > 0, TTL удержания и идемпотентный платёжный ретрай.
требование → инвариант → алгоритм → состояние → отказ → восстановление
Хорошая детализация заканчивается измеримым trade-off: что выигрываем, чем платим и при каком изменении нагрузки решение придётся пересмотреть.
07Шаг 7. Узкие места, масштабирование, отказоустойчивость, trade-offs
middle
- Где bottleneck? (обычно БД на чтение/запись, или один компонент).
- Как масштабировать каждый слой (горизонтально предпочтительнее).
- Где single point of failure? Как продублировать.
- Какие компромиссы приняли и почему (консистентность vs доступность, стоимость vs latency).
💡 Финал хорошего ответа всегда звучит как: «Здесь я выбрал X, пожертвовав Y, потому что для этого сценария важнее Z. Альтернатива — W, она лучше при другом профиле нагрузки».
08Load Balancer и Reverse Proxy
junior
- LB распределяет трафик между инстансами (round-robin, least-connections, consistent hashing). Даёт горизонтальное масштабирование и отказоустойчивость (убирает мёртвые ноды через health checks).
- L4 (TCP) — быстрее; L7 (HTTP) — умеет роутинг по URL/заголовкам, TLS-терминацию.
- Reverse proxy (Nginx, Envoy): TLS-терминация, сжатие, кэш, защита бэкенда, единая точка входа.
- 💡 Приложения держите stateless — тогда LB может слать запрос на любой инстанс. Сессии — в Redis, не в памяти процесса.
09Кэширование
junior
- Где: на клиенте, CDN, на уровне приложения (Redis/Memcached), в БД.
- Паттерны:
- Cache-aside (lazy): приложение читает кэш, при промахе — БД, затем кладёт в кэш. Самый частый.
- Write-through: пишем в кэш и БД синхронно (консистентно, но медленнее запись).
- Write-back: пишем в кэш, в БД асинхронно (быстро, риск потери).
- Инвалидация (одна из двух сложнейших проблем в CS): TTL, явное удаление при записи, версионирование ключей.
- Проблемы: cache stampede (множество промахов одновременно → блокировки/early recompute), hot keys, согласованность кэша и БД.
- При отказе кэша защищайте источник: ограничивайте конкурентные промахи, добавляйте jitter к TTL и не позволяйте всем инстансам одновременно прогревать один hot key.
10CDN
junior
- Кэширует статику (картинки, видео, JS/CSS) ближе к пользователю (edge). Снижает latency и нагрузку на origin.
- Применять для любого тяжёлого статического / редко меняющегося контента и для географически распределённой аудитории.
- Ключ кэша строится из URL и выбранных заголовков; версионированные имена файлов позволяют долгий
Cache-Control: immutable, а изменяемый HTML требует короткого TTL или purge. - Динамический запрос тоже можно кэшировать, если ответ публичный и ключ учитывает все варианты. Персональные ответы и cookie нельзя случайно разделить между пользователями.
client ─► nearest edge ──HIT──► response
│
MISS ─► origin ─► cache at edge
⚠️ CDN не исправляет медленный origin на первом промахе; защищайте origin от stampede и наблюдайте hit ratio отдельно по типам контента.
11Базы: репликация, шардирование, партиционирование
middle
- Репликация (read replicas): копии БД для масштабирования чтения и отказоустойчивости. Запись идёт в primary, чтение — с реплик. Trade-off: replication lag → eventual consistency на чтении.
- Шардирование (sharding): горизонтальное разбиение данных по нескольким БД по ключу. Масштабирует запись и объём. Стратегии: по хэшу ключа, по диапазону, по гео. Минусы: сложные JOIN/транзакции между шардами, ребалансировка, hot shards.
- Партиционирование — то же внутри одной БД (по диапазону дат, по списку). Упрощает работу с большими таблицами.
- Выбор ключа шардирования критичен: должен равномерно распределять и совпадать с паттерном доступа.
12Очереди и асинхронная обработка
middle
- Message Queue (Kafka, RabbitMQ, SQS) развязывает producer и consumer. Применять для: тяжёлых задач не в запросе пользователя (отправка email, обработка видео, fan-out ленты), сглаживания пиков нагрузки (буфер), повторов при сбоях.
- Гарантии доставки: at-most-once / at-least-once / exactly-once. Чаще всего at-least-once + идемпотентные consumer'ы.
- Продюсер пишет событие через transactional outbox, а консюмер дедуплицирует стабильный
message_idв одной транзакции с бизнес-эффектом. Ошибки уходят в retry-топик с backoff, затем в DLQ для разбора. - Очередь не убирает нагрузку, а переносит её во времени. Следите за lag, возрастом самого старого сообщения и пропускной способностью; при переполнении нужны backpressure и явная политика отбрасывания или деградации.
13Индексы и денормализация
middle
- Индексы ускоряют чтение ценой замедления записи и доп. места. Создавайте под реальные запросы (WHERE/JOIN/ORDER BY).
- Денормализация: дублирование данных, чтобы избежать JOIN при чтении. Применять в read-heavy системах (лента, профили). Trade-off: сложнее поддерживать консистентность при записи.
- Начинайте с access pattern: индекс
(tenant_id, created_at DESC)полезен для конкретного фильтра и сортировки, а «индексировать всё» увеличивает write amplification и время vacuum/rebuild. - Для денормализованной копии назовите источник истины и механизм обновления: синхронная транзакция, outbox/CDC или периодическая пересборка. Также определите допустимое окно stale-данных и способ repair после пропущенного события.
write → source of truth → outbox/CDC → read model
↘ retry + reconciliation
14CAP-теорема и консистентность
middle
- При сетевом разделении (P, неизбежно в распределённых системах) выбираем между C (consistency) и A (availability):
- CP: при разрыве жертвуем доступностью ради корректности (банки, инвентарь). Пример: HBase, etcd.
- AP: продолжаем отвечать, допуская расхождение (лента, лайки). Пример: Cassandra, DynamoDB.
- На практике важнее PACELC: даже без разрыва (Else) есть выбор Latency vs Consistency.
CAP рассматривает только период сетевого разделения, а не классифицирует систему навсегда как «CA». Во время partition CP-система отклоняет часть операций, чтобы не допустить противоречивых записей; AP принимает их и позже разрешает конфликты. Выбор делают по операции: перевод денег требует строгого инварианта, а счётчик лайков может временно расходиться.
partition? ─ yes ─► consistency or availability
└ no ─► PACELC: latency or consistency
⚠️ Репликация сама по себе не даёт strong consistency: важны лидерство, кворумы, правила чтения и поведение при failover.
15Согласованность: strong vs eventual, кворумы
middle
- Strong consistency: любое чтение видит последнюю запись. Дороже, выше latency. Нужна для денег, остатков, уникальности.
- Eventual consistency: реплики сходятся со временем. Дёшево и доступно. Ок для лайков, счётчиков просмотров, ленты.
- Кворумы: при N репликах требуем подтверждения от W при записи и R при чтении. Если W + R > N — гарантируется чтение свежих данных (например N=3, W=2, R=2). Настройка W/R балансирует консистентность, latency и доступность.
16Идемпотентность и dedup
middle
- Идемпотентность: повтор операции даёт тот же результат (повторный POST платежа не списывает дважды).
- Реализация: idempotency key от клиента → сервер хранит результат по ключу и при повторе возвращает сохранённый, не выполняя операцию снова. Хранить в Redis/БД с TTL.
- Dedup сообщений в очередях — по message_id.
- Проверка ключа и бизнес-эффект должны быть атомарны. Надёжный вариант — строка
idempotency_keyсUNIQUEв той же БД и транзакции; отдельная проверка Redis перед записью оставляет окно гонки. - Сохраняйте статус и сериализованный ответ, проверяйте, что повтор пришёл с тем же payload, и выбирайте TTL длиннее максимального окна клиентских ретраев.
FAILEDс неизвестным исходом нельзя бездумно запускать заново.
same key + same payload → same stored result
same key + other payload → 409 conflict
17Rate Limiting
middle
- Rate limiting защищает ёмкость системы и ограничивает злоупотребления. Fixed window прост, но допускает двойной всплеск на границе окна; sliding log/window точнее, но хранит больше состояния; token bucket разрешает контролируемый burst и затем восстанавливает токены с постоянной скоростью.
- Ключ выбирают по пользователю, API key, tenant или IP; ответ
429должен содержатьRetry-After. Лимит размещают до дорогой работы, но после достаточной идентификации клиента. - В распределённой системе состояние держат в Redis и обновляют атомарным Lua-скриптом. При отказе Redis заранее выбирают fail-open для некритичного API либо fail-closed для дорогой/чувствительной операции.
request → identify key → consume token? ─ yes → handler
└ no → 429 + Retry-After
18Микросервисы vs монолит
middle
- Монолит: проще разрабатывать, деплоить, дебажить; одна транзакция. Берите по умолчанию для нового продукта.
- Микросервисы: независимое масштабирование и деплой команд, изоляция отказов. Цена: сетевые вызовы, распределённые транзакции (saga), сложный observability, eventual consistency между сервисами.
- 💡 На собесе: «Начал бы с модульного монолита, выделил бы в сервисы то, что нужно масштабировать независимо или что владеет отдельная команда».
- Границу сервиса проводят вокруг бизнес-владения и данных, а не вокруг технического слоя. Выделение оправдано, когда модуль требует независимого релизного цикла, профиля масштабирования или команды; иначе сеть превращает простой вызов функции в контракт с таймаутами, ретраями и частичными отказами.
19API Gateway
middle
- Единая точка входа: маршрутизация, аутентификация, rate limiting, агрегация ответов, версионирование, TLS. Снимает кросс-задачи с сервисов. Риск — стать SPOF/узким местом, поэтому масштабируется и дублируется.
- Gateway не должен превращаться в новый монолит бизнес-логики. Он проверяет токен и общие политики, но авторизация конкретного ресурса остаётся в сервисе-владельце.
- Разворачивайте несколько stateless-инстансов за балансировщиком, задавайте короткие downstream timeouts, ограничивайте тело запроса и наблюдайте latency/error rate по каждому маршруту. Агрегация ответов экономит клиентские round trips, но требует частичного ответа и fallback при отказе одного downstream.
client → gateway ┬→ users
├→ orders
└→ catalog
20Blob Storage (S3)
junior
- Объектное хранилище для файлов, картинок, видео, бэкапов. Дёшево, durable (репликация внутри), бесконечно масштабируемо.
- Паттерн: метаданные в БД, сам файл — в S3, отдача через CDN, загрузка через pre-signed URL (клиент пишет в S3 напрямую, минуя бэкенд).
- Объект адресуется ключом и обычно заменяется целиком; это не POSIX-файловая система и не место для частых точечных обновлений. Версионирование и lifecycle rules переводят старые объекты в дешёвый storage или удаляют их.
- Pre-signed URL должен иметь короткий срок, ограниченный метод и непредсказуемый ключ. После загрузки сервис проверяет размер, MIME/сигнатуру и запускает асинхронное сканирование, прежде чем сделать объект публичным.
client ──metadata──► API ──signed URL──► client
client ─────────────upload──────────────► object storage
21Поиск (Elasticsearch)
middle
- Полнотекстовый поиск, фасеты, агрегации (инвертированный индекс). Не основное хранилище — данные приходят из основной БД через CDC/очередь. Eventual consistency допустима.
- Инвертированный индекс сопоставляет терм со списком документов; analyzer выполняет tokenization, нормализацию и stemming. Mapping нужно проектировать заранее:
textподходит для полнотекстового ранжирования,keyword— для точных фильтров и агрегаций. - Поток записи обычно выглядит как
DB → outbox/CDC → indexer → search index. События должны быть идемпотентны и версионированы, а периодический reconciliation восстанавливает пропуски. При падении поиска основной CRUD продолжает работать, возможно с упрощённым поиском.
⚠️ Не используйте Elasticsearch как единственный источник истины для платежей или остатков: refresh/replication дают окно устаревания, а переиндексация — обычная эксплуатационная операция.
22Геораспределение и latency
middle
- Размещайте сервисы и данные ближе к пользователям (мульти-регион), используйте CDN и GeoDNS / Anycast.
- Скорость света даёт нижнюю границу latency: межконтинентальный round-trip ~100-200 мс. Внутри ДЦ — < 1 мс.
- Trade-off: мульти-регион усложняет консистентность (геораспределённые записи → конфликты).
- Сначала разделите чтения и записи: статические ответы и read replicas можно приблизить к пользователю, а записи оставить в одном home region, чтобы сохранить простой порядок и транзакции.
- Active-active запись требует стратегии владения ключом или разрешения конфликтов; last-write-wins может потерять корректную запись из-за рассинхронизации часов. Для денег и уникального инвентаря лучше направить ключ к одному лидеру, приняв межрегиональную задержку.
user → nearest edge/read replica
write → home region leader → async replicas
23SPOF и отказоустойчивость
middle
- Single Point of Failure — компонент, отказ которого валит систему. Устраняется дублированием: несколько инстансов за LB, репликация БД с failover, multi-AZ.
- Паттерны устойчивости: health checks, retries с backoff, circuit breaker, graceful degradation (отдать кэш/частичный ответ), timeouts, bulkheads.
- Проверяйте не только количество инстансов, но и общие зависимости: два приложения в одной AZ, один DNS, общий connection pool или ручной failover всё ещё создают SPOF.
- Ретраи разрешены только для безопасных/идемпотентных операций и должны иметь exponential backoff, jitter и общий deadline. Circuit breaker ограничивает вызовы к больной зависимости, bulkhead не даёт ей занять все воркеры, а graceful degradation сохраняет критический путь без необязательной функции.
⚠️ Реплика без регулярно проверяемого failover — это копия, а не отказоустойчивость. Проводите game day и измеряйте реальные RTO/RPO.
24Метрики масштаба (порядки величин)
concept
Грубые ориентиры для оценок (порядок, не точные числа):
| Компонент | Примерная ёмкость |
|---|---|
| 1 app-сервер | ~1 000–10 000 QPS (зависит от логики) |
| 1 БД (запись) | ~1 000–10 000 writes/сек |
| 1 read-реплика | десятки тысяч простых чтений/сек |
| Redis (один инстанс) | ~100 000+ ops/сек |
| Строка реляц. БД | сотни байт – единицы КБ |
| Сетевой round-trip в ДЦ | < 1 мс |
| Чтение с SSD | ~100–200 мкс |
| Межрегиональный RTT | ~50–150 мс |
253.1 URL Shortener (TinyURL)
1. Требования. Функциональные: сократить длинный URL в короткий; редирект по короткому ключу на оригинал; (опц.) кастомный alias, TTL. Вне скоупа: аналитика кликов, аккаунты. NFR: очень read-heavy (редиректов гораздо больше создания); низкая latency редиректа; высокая доступность; короткие ссылки уникальны и не угадываемы; eventual consistency допустима для редиректа.
2. Масштаб. 100M новых ссылок/мес → ~40 writes/сек. Read:write ≈ 100:1 → ~4000 reads/сек, пик ~10k. Хранение: 100M × ~500 B = 50 ГБ/мес → ~6 ТБ за 10 лет. Длина ключа: алфавит base62 (a-zA-Z0-9). 62⁷ ≈ 3.5 триллиона — 7 символов хватает надолго.
3. API.
POST /api/v1/shorten {long_url, custom_alias?, ttl?} -> {short_url}
GET /{key} -> 301/302 -> long_url
301 (permanent) кэшируется браузером навсегда → меньше нагрузки, но теряем аналитику; 302 (temporary) → каждый клик идёт на сервер. Выбор обсудить.
4. Данные.
Таблица urls(key PK, long_url, created_at, expires_at, creator_id). Паттерн доступа — точечный lookup по ключу → отлично ложится на key-value NoSQL (DynamoDB/Cassandra) для масштаба, либо Postgres с индексом по key для начала. Шардирование по key (хэш) — равномерно.
5. Генерация ключа (ключевая часть 🟡):
- Вариант A — хэш (MD5/SHA от URL, взять первые 7 символов base62): просто, но коллизии → проверять и при коллизии добавлять соль/перехэшировать. Одинаковый URL даёт один ключ (можно считать плюсом или минусом).
- Вариант B — счётчик + base62: глобальный автоинкремент → кодируем в base62. Гарантированно без коллизий, короткие ключи. Минус: единый счётчик — bottleneck/SPOF. Решение — диапазоны ID (ticket/range server раздаёт блоки по 1000 каждому app-серверу) или распределённый генератор (Snowflake/ZooKeeper). Ключи предсказуемы → можно перемешивать.
- 💡 Обычно предлагаю B с раздачей диапазонов — масштабируется и без коллизий.
6. Архитектура.
Client -> LB -> App Servers -> [Redis cache] -> [KV DB (sharded)]
-> [ID/range service] (для записи)
Редирект: app смотрит в Redis (cache-aside, TTL), при промахе — БД, кладёт в кэш, отдаёт 302.
7. Узкие места.
- Чтение — главный объём. Решается кэшем (горячие ссылки) + read-репликами/широким шардированием. Hit rate высокий, т.к. распределение кликов следует степенному закону (популярные ссылки).
- Запись — раздача ID-диапазонов убирает контеншн.
- SPOF — дублировать LB, ID-сервис, реплицировать БД.
- Очистка expired-ссылок — фоновый job / TTL в БД.
263.2 Лента новостей (News Feed)
1. Требования. Функциональные: пользователь видит ленту постов от тех, на кого подписан, отсортированную (по времени/релевантности); публикация поста; подписки. NFR: read-heavy; низкая latency загрузки ленты (это hot path); eventual consistency ок (задержка появления поста в пару секунд приемлема); высокая доступность.
2. Масштаб. Допустим 300M DAU, каждый открывает ленту ~10 раз/день → 3B чтений/день ≈ 35k QPS, пик ~70k. Постов: 100M/день. Среднее число подписок ~ сотни; у звёзд — десятки миллионов фолловеров (важный спецслучай).
3. API.
GET /v1/feed?cursor=...&limit=20 -> [posts]
POST /v1/posts {content, media_ids} -> {post_id}
POST /v1/follow {target_user_id}
Пагинация — cursor-based (по post_id/timestamp), не offset.
4. Данные.
posts(post_id, author_id, content, media, created_at)follows(follower_id, followee_id)feed_cache(precomputed): per-user список post_id (Redis list / sorted set). SQL для графа подписок и постов; Redis для материализованных лент.
5. Fan-out — ключевое решение 🔴:
- Fan-out on write (push): при публикации поста сразу записываем его id в ленты (Redis) всех подписчиков. Чтение ленты → быстрый GET готового списка. Плюс: молниеносное чтение. Минус: дорогая запись для популярных авторов («fan-out problem»: пост звезды с 50M фолловеров = 50M записей).
- Fan-out on read (pull): ленту собираем в момент запроса — берём посты всех, на кого подписан, мержим. Плюс: дешёвая запись. Минус: дорогое и медленное чтение, особенно при многих подписках.
- Гибрид (правильный ответ): обычные пользователи — push; знаменитости (порог по числу фолловеров) — pull. При чтении мержим precomputed-ленту с «живыми» постами звёзд. 💡 Так делают реальные соцсети.
6. Архитектура.
Post -> API -> posts DB -> [Queue] -> Fan-out workers -> Redis feed lists
Read feed -> API -> Redis (готовая лента) + pull посты celebrity -> merge -> rank
Fan-out — асинхронно через очередь, чтобы не блокировать публикацию.
7. Ранжирование и узкие места.
- Ранжирование: хронология просто; ML-ранжирование (релевантность) — отдельный сервис со скорами; на собесе достаточно упомянуть.
- Узкие места: fan-out звёзд (→ гибрид), память Redis (хранить только N последних post_id на пользователя, остальное — pull из БД), горячие ключи.
- Отказоустойчивость: feed-кэш можно перестроить из БД; реплики Redis.
273.3 Чат / мессенджер (WhatsApp-like)
1. Требования. Функциональные: 1-на-1 сообщения, групповые чаты, статусы доставки (sent/delivered/read), presence (онлайн/был в сети), история. NFR: низкая latency доставки; высокая доступность; durability сообщений (не терять); порядок сообщений в чате; масштаб подключений.
2. Масштаб. 500M DAU, 40 сообщений/день → 20B сообщений/день ≈ 230k msg/сек. Одновременных подключений — сотни миллионов. Хранение: 20B × ~200 B ≈ 4 ТБ/день.
3. API / протокол. Не классический REST для доставки — нужна двусторонняя связь: WebSocket (или MQTT) для live, плюс HTTP для истории и регистрации.
WS: connect; send {chat_id, msg}; receive {msg}; ack {msg_id}
GET /v1/chats/{chat_id}/messages?cursor=...
4. Данные.
messages(chat_id, msg_id, sender_id, content, created_at, status)— партиционирование/шард поchat_id, сортировка по времени. Wide-column (Cassandra) идеально: огромная запись, доступ по ключу чата, time-series. msg_id — Snowflake (монотонный → порядок).chats,chat_members,user_presence.
5. Доставка — ключевая часть 🔴:
- Пользователи держат постоянное WebSocket-соединение с gateway-серверами (stateful). Нужен реестр: какой пользователь на каком gateway → хранить в Redis (
user_id -> gateway_id). - Отправка: A пишет в свой gateway → сервис сообщений сохраняет в БД (durability) → ищет gateway получателя B → пушит по его WS. B шлёт ack → статус delivered.
- B оффлайн: сообщение лежит в БД/очереди; при подключении B запрашивает недоставленные (по последнему seen msg_id). Можно push-нотификация (APNs/FCM).
- Группы: fan-out сообщения членам (для больших групп — как fan-out ленты).
6. Presence.
Heartbeat по WS каждые N секунд → обновляем last_seen в Redis с TTL. Нет heartbeat → offline. Не пушить presence всем подряд — только активным наблюдателям.
7. Узкие места.
- Миллионы постоянных соединений: много gateway-серверов, connection LB (L4, sticky). Каждый сервер держит ~десятки-сотни тысяч соединений.
- Маршрутизация между gateway → Redis-реестр + pub/sub.
- Порядок и идемпотентность: client-generated msg_id для dedup при ретраях.
- Durability: писать в БД до подтверждения отправителю.
283.4 Rate Limiter
1. Требования. Ограничить число запросов на клиента (user/IP/API-key) за период: например 100 req/min. NFR: низкие накладные расходы и latency; точность; работа в распределённой среде (много инстансов); fail-open или fail-closed при сбое стораджа (обсудить).
2. Масштаб. Срабатывает на каждый запрос → должен быть очень быстрым (< 1 мс). При 100k QPS — 100k проверок/сек.
3. API.
Обычно middleware/часть API Gateway. allow(key) -> bool. При отказе → HTTP 429 + заголовки X-RateLimit-Remaining, Retry-After.
4. Алгоритмы (ключевая часть 🟡):
- Fixed window counter: счётчик на окно (минуту). Просто, но всплеск на границе окон (до 2× лимита).
- Sliding window log: храним timestamp каждого запроса, считаем за последние 60 сек. Точно, но дорого по памяти.
- Sliding window counter: интерполяция между текущим и предыдущим окном. Хороший баланс точности и памяти. Частый выбор.
- Token bucket: бакет наполняется N токенов/сек, запрос берёт токен; пустой → отказ. Допускает burst до размера бакета. Очень популярен (гибкий).
- Leaky bucket: запросы в очередь, обрабатываются с постоянной скоростью. Сглаживает трафик.
5. Распределённая реализация 🔴:
- Состояние общее для всех инстансов → Redis (быстро, атомарно). Счётчик/токены по ключу
rl:{user}:{window}с TTL. - Атомарность — Lua-скрипт (read-check-write одной операцией) или
INCR+EXPIRE. Без атомарности — гонки и перерасход лимита. - Trade-off латентности: сетевой вызов в Redis на каждый запрос. Оптимизация — локальный кэш + периодическая синхронизация (немного жертвуем точностью).
- При недоступности Redis: fail-open (пропускать, чтобы не уронить сервис) vs fail-closed (блокировать) — решение по требованиям.
6/7. Узкие места. Redis как горячая точка → шардировать по ключу клиента; реплики. Hot key одного крупного клиента — отдельный шард / local limiting.
293.5 Хранение и раздача файлов / видео
1. Требования. Функциональные: загрузка файла/видео, хранение, скачивание/стриминг, (для видео) транскодирование в разные качества. NFR: durability (не терять файлы); высокая пропускная способность; низкая latency отдачи глобально; большой объём.
2. Масштаб. Допустим 1M загрузок/день, средний файл 5 МБ → 5 ТБ/день нового контента; чтений в десятки раз больше → терабиты bandwidth → CDN обязателен.
3. API.
POST /v1/files/initiate {filename, size, content_type} -> {upload_id, presigned_url}
PUT <presigned_url> (клиент грузит напрямую в S3, multipart для больших)
POST /v1/files/complete {upload_id} -> {file_id, url}
GET /v1/files/{file_id} -> 302 -> CDN URL
4. Данные.
- Метаданные в БД:
files(file_id, owner, name, size, s3_key, status, created_at)— реляционная БД ок. - Сам контент — blob storage (S3), не БД. Большие файлы — multipart upload (чанки, параллельно, докачка).
5. Архитектура (ключевая часть 🟡):
Client --presigned PUT--> S3 (origin)
<--metadata-- App Server -- DB
S3 --event--> Queue --> Transcoding Workers --> S3 (variants 240p/720p/1080p)
Client --GET--> CDN (edge) --miss--> S3
- Pre-signed URL: бэкенд выдаёт временный URL, клиент пишет прямо в S3, минуя app-серверы (не гоняем терабайты через бэкенд).
- Транскодирование видео: загрузка триггерит событие → очередь → пул воркеров режет на сегменты и кодирует в несколько качеств/битрейтов (для адаптивного стриминга HLS/DASH). Асинхронно, идемпотентно.
- CDN раздаёт и статику, и видеосегменты — главный слой чтения.
6/7. Узкие места и устойчивость.
- Bandwidth чтения → CDN снимает основную нагрузку с origin.
- Durability → S3 реплицирует объекты по AZ; критичное — кросс-регион репликация.
- Транскодирование — CPU-тяжёлое → масштабируем воркеры по длине очереди (autoscaling); ретраи на сбоях; статус задачи в БД.
- Большие загрузки → multipart + докачка + проверка контрольной суммы.
- Дедупликация по хэшу содержимого (одинаковые файлы хранить раз).
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.