B12 · Кейсы#
Блок, ради которого написаны остальные. Здесь workflow из 00-workflow.md применяется к конкретным задачам: три подробных прохода, девять коротких разборов и обзор реальных архитектур.
Подробные кейсы идут по шагам W0–W7 с числами. Цель не в том, чтобы запомнить решение, а в том, чтобы увидеть, как оценки на втором шаге определяют выбор на пятом. Короткие кейсы устроены иначе: ключевая трудность, решение и главный компромисс.
Навигация: CASE-01 · CASE-02 · CASE-03 · CASE-04 · CASE-05 · CASE-06 · CASE-07 · CASE-08 · CASE-09 · CASE-10 · CASE-11 · CASE-12 · CASE-13
CASE-01 · Сокращатель ссылок#
Подробный проход
Почему этот кейс первый. Он кажется тривиальным и при этом задействует генерацию идентификаторов, соотношение чтения и записи, кэш, выбор хранилища и аналитику. Как раз на нём хороший разбор отличается от плохого.
W0 · Требования. Создать короткую ссылку по длинной; переход по короткой ведёт на исходную; ссылка может иметь срок жизни; нужна статистика переходов. Вне рамок: пользовательские аккаунты, редактирование ссылок. Нефункциональные:
- переход — p99 ≤ 50 мс: это часть пути пользователя к чужому сайту, задержка заметна;
- доступность чтения 99.99 % — недоступность ломает чужие страницы;
- создание ссылки может быть медленнее и менее доступным.
W1 · Оценки. Пусть 100 млн новых ссылок в месяц: ≈ 40 записей/с, пик ×10 = 400/с. Соотношение чтений к записям для таких сервисов порядка 100:1 → ≈ 4 000 чтений/с, пик 40 000/с. Хранение: 100 млн × ≈ 500 байт ≈ 50 ГБ в месяц, за 5 лет ≈ 3 ТБ. Это с запасом помещается в одну реляционную БД, шардирование на старте не нужно. Ключевой вывод: это система чтения, и весь дизайн строится вокруг чтения.
W2 · Контракты. POST /links с длинным URL и необязательными сроком жизни и желаемым алиасом → короткий код. Идемпотентность по ключу (NET-16), иначе повтор при таймауте создаст вторую ссылку. Переход — GET /{code} → 301 или 302: выбор здесь важнее, чем кажется. Код 301 кэшируется браузером навсегда, поэтому переходы быстрые, но статистика перестаёт собираться. Код 302 даёт полную статистику ценой обращения на каждый переход. Ответ: 302 плюс запрет кэширования, если статистика — требование.
W3 · Данные. Таблица: код (первичный ключ), исходный URL, время создания, срок жизни, владелец. Паттерн доступа один: точечное чтение по коду. Значит, подходит любое KV-хранилище, а реляционная БД избыточна, но удобна и достаточна.
Генерация кода — центральное решение:
| Вариант | Плюс | Минус |
|---|---|---|
| Хэш от URL, первые N символов | Один URL → один код | Коллизии требуют проверки и повторов |
| Случайный код с проверкой уникальности | Простота | Обращение к БД при генерации, риск повторов |
| Счётчик в 62-ричной записи | Без коллизий, короткий код | Последовательные коды перебираемы и раскрывают объём |
| Диапазоны счётчика на узел | Без коллизий и без координации на запрос | Дыры в нумерации |
Практический выбор — диапазоны с перемешиванием значения перед кодированием: сохраняется отсутствие коллизий, теряется предсказуемость. Семь символов по 62 знака дают ≈ 3.5 трлн вариантов — с запасом (DIST-09).
W4–W5 · Архитектура и масштаб. Путь чтения: CDN/edge → кэш → БД. Популярность ссылок распределена сильно неравномерно: небольшая доля даёт основную часть переходов. Поэтому hit ratio кэша высокий, и БД видит малую часть нагрузки (CACHE-01). При росте — реплики чтения, затем шардирование по коду (хэш-распределение, точечные чтения, кросс-шардовых запросов нет — редкий случай, когда шардирование безболезненно).
W6 · Согласованность. Ссылка неизменяема после создания, поэтому инвалидация кэша не нужна вовсе — только срок жизни. Это резко упрощает систему: большинство сложностей кэширования здесь просто отсутствует.
W7 · Отказы и злоупотребления. Недоступность БД при живом кэше — продолжаем обслуживать популярные ссылки. Переполнение памяти кэша — вытеснение по частоте использования (LFU). Отдельно: сокращатель — инструмент фишинга, поэтому нужны проверка URL по спискам угроз, rate limiting на создание (EDGE-06) и возможность заблокировать ссылку.
Аналитика. Запись каждого перехода синхронно убила бы путь чтения — события уходят в лог (MSG-02) и агрегируются отдельно (MSG-11).
Что проверяет этот кейс.
- понимание того, что соотношение чтения и записи определяет архитектуру;
- неочевидный выбор кода перенаправления;
- генерацию идентификаторов без координации;
- неизменяемость как способ убрать целый класс проблем.
Связано: DIST-09 уникальные ID · CACHE-01 кэш · DATA-10 шардирование
Источники: Alex Xu — System Design Interview, vol. 1 · RFC 9110 — Redirection 3xx
CASE-02 · Лента новостей#
Подробный проход
Почему этот кейс. Он строится вокруг одного решения — когда собирать ленту, при записи или при чтении, — и это решение меняет всю систему. Плюс здесь живёт классическая проблема неравномерности: пользователи с миллионами подписчиков ломают любую симметричную схему.
W0 · Требования. Лента из постов тех, на кого подписан пользователь, обратно хронологическая (ранжирование вынесем за рамки); публикация поста; бесконечная прокрутка. Нефункциональные:
- открытие ленты p99 ≤ 200 мс;
- отставание появления поста у подписчиков до 10 секунд приемлемо — это ключевая уступка, открывающая асинхронный путь;
- доступность чтения важнее доступности публикации.
W1 · Оценки. 10 млн активных пользователей в сутки, каждый открывает ленту 10 раз → 100 млн чтений/сут ≈ 1 200/с, пик 12 000/с. Постов 1 млн/сут ≈ 12/с — чтений в сто раз больше записей. Средняя подписка — сотни аккаунтов.
W3–W4 · Центральное решение.
| Подход | Что делает | Кому подходит |
|---|---|---|
| Сборка при чтении (pull, fan-out on read) | Открыл ленту — собрали посты от всех подписок и слили | Читают редко, подписок много |
| Раздача при записи (push, fan-out on write) | Опубликовал — разложили по спискам всех подписчиков | Читают часто, подписок умеренно |
| Гибрид | Push для обычных, pull для аккаунтов с огромной аудиторией | Реальный выбор |
Из оценок следует push: сто чтений на одну запись — значит, работу надо переносить на запись. Но у аккаунта с 10 млн подписчиков одна публикация порождает 10 млн вставок, что даёт всплеск и задержку. Отсюда гибрид: посты обычных авторов раскладываются заранее, посты «звёзд» подмешиваются при чтении из отдельного небольшого списка. Порог переключения — число подписчиков, и это настраиваемый параметр, а не константа в коде.
Данные. Список ленты на пользователя — упорядоченный набор идентификаторов постов, ограниченный длиной (хранить бесконечную ленту не нужно; глубже — досборка из источника). Сами посты — в отдельном хранилище, лента ссылается на них: так изменение или удаление поста не требует правки миллионов лент. Пагинация только курсорная (NET-14) : при offset новые посты сдвигают выдачу, и пользователь видит дубли.
W5 · Масштаб. Раздача — асинхронная, через очередь (MSG-02): публикация возвращает ответ сразу, разложение идёт в фоне. Списки лент — в хранилище в памяти (CACHE-05) с ограничением длины; посты — в шардированном хранилище с кэшем.
W6 · Согласованность. У ленты по построению отложенная согласованность (eventual consistency). Но своё действие пользователь должен видеть немедленно: собственный пост подмешивается локально, не дожидаясь раздачи (DIST-03). Удаление поста не вычищается из миллионов лент — фильтруется при чтении по признаку в хранилище постов.
W7 · Отказы. Отставание очереди раздачи — деградация, а не отказ: лента просто старше. Обрыв раздачи на середине оставляет часть подписчиков без поста — нужна идемпотентная повторная обработка (MSG-06). Всплеск публикаций от популярного аккаунта ограничивается темпом раздачи, чтобы не вытеснить обычный трафик.
Что проверяет этот кейс.
- умение вывести push/pull из соотношения чтения и записи;
- распознавание неравномерности как отдельной проблемы;
- понимание, что уступка по свежести (10 секунд) — это решение, открывающее целый класс архитектур.
Связано: MSG-02 очередь · CACHE-05 Redis · NET-14 пагинация · DATA-10 шардирование
Источники: Alex Xu — System Design Interview, vol. 1 · Twitter Engineering — Timelines at Scale
CASE-03 · Чат и мессенджер#
Подробный проход
Почему этот кейс. Здесь всё, чего нет в двух предыдущих: постоянные соединения, доставка в реальном времени, порядок сообщений, состояние присутствия и хранение со специфичным профилем нагрузки.
W0 · Требования. Личные и групповые чаты; доставка в реальном времени; история; статусы «доставлено/прочитано»; работа на нестабильной мобильной сети. Нефункциональные: задержка доставки p99 ≤ 500 мс; ни одно принятое сообщение не теряется; порядок в пределах чата сохраняется; работа при обрывах соединения.
W1 · Оценки. 50 млн активных пользователей, 40 сообщений в день → 2 млрд/сут ≈ 23 000/с, пик ×5 ≈ 115 000/с. Одновременных соединений — миллионы. Хранение при 200 байтах на сообщение — ≈ 400 ГБ/сут, что за годы даёт сотни терабайт: шардирование обязательно с самого начала (DATA-10).
W2 · Транспорт. Нужен двунаправленный канал — WebSocket (NET-12). Отсюда главное архитектурное следствие: соединение закреплено за конкретным сервером, значит нужен реестр «пользователь → сервер» и способ доставить сообщение на нужный узел. Обычно это внутренняя шина: сервер, принявший сообщение, публикует его, а сервер, который держит соединение адресата, доставляет.
Идентификатор сообщения генерируется клиентом — это позволяет мгновенно показать сообщение в интерфейсе, безопасно повторить отправку при обрыве и связать подтверждение с локальной записью (NET-16). Порядок в чате задаётся не временем клиента (часы врут), а монотонным номером, присваиваемым сервером в пределах чата.
W3 · Данные. Профиль экстремальный: запись огромная и постоянная, чтение — в основном последние N сообщений одного чата, старое читается редко. Это профиль для LSM-хранилища (DATA-05): дешёвая запись, эффективные диапазонные чтения по ключу «чат + номер». Шардирование по идентификатору чата — все сообщения одного чата рядом, кросс-шардовых запросов нет. Ровно так устроено хранилище Discord.
W4–W5 · Доставка. Онлайн-адресат получает сообщение через своё соединение. Офлайн — сообщение остаётся в хранилище, а клиент при подключении синхронизируется «с последнего известного номера». Это же решает и обрывы: клиент не «слушает поток», а догоняет состояние, что на мобильной сети принципиально надёжнее. Push-уведомления — отдельный канал для офлайн-адресатов (CASE-05).
Группы. Небольшие — раздача каждому участнику. Крупные (тысячи и больше) — та же проблема неравномерности, что и в ленте: раздача становится дороже сообщения, поэтому участники подписываются на поток чата и вытягивают обновления.
W6 · Согласованность и статусы. Статусы доставки — отдельные события с собственной идемпотентностью. «Прочитано» в групповом чате порождает поток на порядок больше самих сообщений, поэтому статусы агрегируются и отправляются пачками, а не по одному.
W7 · Отказы. Падение узла с соединениями — клиенты переподключаются, и нужен jitter (разброс задержки) переподключения, иначе миллион клиентов одновременно устроит атаку на собственный кластер (REL-01). Выкатка — та же проблема (OPS-02). Отставание шины доставки — сообщения не теряются, но приходят с задержкой; клиент всё равно догонит их синхронизацией.
Что проверяет этот кейс.
- понимание цены постоянных соединений;
- идентификатор от клиента как решение проблемы повторов;
- синхронизация по номеру вместо надежды на непрерывный поток;
- распознавание профиля нагрузки, требующего LSM.
Связано: NET-12 realtime-транспорт · DATA-05 индексы · DATA-10 шардирование · CASE-05 уведомления
Источники: How Discord Stores Trillions of Messages · Alex Xu — System Design Interview, vol. 2 · ByteByteGo — How Discord Stores TRILLIONS of Messages
CASE-04 · Rate limiter#
Короткий разбор
Трудность. Лимит должен соблюдаться суммарно по всем экземплярам сервиса, но проверка выполняется на горячем пути каждого запроса. Строгий общий счётчик — сетевое обращение к общему хранилищу на каждый запрос; локальные счётчики дешевле, но каждый экземпляр видит лишь свою часть трафика.
Решение. Счётчик в хранилище в памяти с атомарным инкрементом и алгоритмом token bucket (токенного ведра, EDGE-06). Операция выполняется одним скриптом на стороне хранилища, чтобы проверка и инкремент были атомарны. Если частота слишком высока и для этого — локальные счётчики с периодической сверкой: лимит соблюдается приблизительно, зато без сетевого обращения. Ответ 429 с Retry-After и заголовками остатка квоты (NET-05).
Компромисс. Точность против задержки и доступности. И отдельное решение, принимаемое заранее: что делать при недоступности хранилища счётчиков — пропускать всё (fail open: доступность важнее) или отклонять (fail closed: защита важнее). Для публичного API обычно первое.
Порядок величин. Типовая конфигурация публичного API — тысяча запросов в минуту на ключ с накопленным всплеском (burst) в сотню. Проверка через общее хранилище стоит один сетевой обмен: около 0.5 мс при времени обработки запроса в десятки миллисекунд, то есть проценты накладных расходов. Это и есть аргумент в пользу строгого счётчика. Локальная схема при двадцати экземплярах и сверке раз в десять секунд даёт погрешность в единицы процентов: приемлемо для защиты от злоупотреблений и неприемлемо для тарификации.
Связано: EDGE-06 rate limiting · CACHE-05 Redis · REL-03 load shedding
Источники: Stripe — Scaling your API with rate limiters · Cloudflare — What is rate limiting? · RFC 6585 — 429 Too Many Requests
CASE-05 · Сервис уведомлений#
Короткий разбор
Трудность. Один сервис отправляет через несколько каналов — push, email, SMS, в приложении, — и у каждого свои лимиты, форматы и вероятность отказа. При этом дубликат уведомления раздражает пользователя, а потеря критичного (код подтверждения) недопустима.
Решение. Событие о факте («заказ отправлен») отделено от решения, кому и по какому каналу слать: правила и настройки пользователя — отдельный слой. Очередь на канал (MSG-02) с собственным темпом и повторами (MSG-08). Дедупликация по ключу «событие + получатель + канал» (MSG-06). Шаблоны версионируются и локализуются отдельно от кода. Нужны также отписка, тихие часы и объединение однотипных уведомлений в одно.
Компромисс. At-least-once (как минимум один раз) плюс дедупликация даёт редкие дубликаты; at-most-once (не более одного раза) — редкие потери. Для критичных каналов выбирают первое, для маркетинговых — второе. Отдельная ловушка: внешние провайдеры доставки — самая частая причина отказов, поэтому нужны запасной провайдер и circuit breaker (REL-02).
Порядок величин. Миллион пользователей и три уведомления в день дают около 35 отправок в секунду в среднем. Но массовая рассылка сжимает суточный объём в минуты, и пик оказывается в десятки раз выше. Отсюда два следствия: очередь на канал должна сглаживать всплеск, а отправка идёт пачками (мобильные провайдеры принимают сотни адресатов за один вызов). Окно дедупликации берут с запасом относительно максимального времени повторов — обычно сутки.
Связано: MSG-02 очередь vs лог · MSG-10 webhooks · CASE-03 чат
Источники: Firebase Cloud Messaging — Documentation · Apple — Sending notification requests to APNs · RFC 8030 — Web Push
CASE-06 · Автодополнение поиска#
Короткий разбор
Трудность. Подсказки должны появляться за десятки миллисекунд, запрос отправляется почти на каждое нажатие клавиши, а количество запросов на порядок больше, чем у самого поиска.
Решение. Trie (префиксное дерево) с предвычисленным топом подсказок в каждом узле: ответ — это чтение готового списка, а не поиск. Структура целиком в памяти и реплицируется на чтение; обновляется пакетно из статистики запросов, а не в реальном времени. На клиенте — debounce (задержка перед отправкой), отмена устаревших запросов и локальный кэш префиксов. Персонализация и свежие тренды подмешиваются к предвычисленному топу отдельным слоем.
Компромисс. Свежесть против стоимости: подсказки, обновляемые раз в час, дёшевы и обычно достаточны; реальное время требует потоковой обработки (MSG-11). Отдельно — фильтрация: подсказки формируются из пользовательских запросов, поэтому нужны списки запрещённого и защита от накрутки.
Порядок величин. Запрос уходит на каждое нажатие: десять символов на поиск превращают миллион поисков в сутки в десять миллионов запросов подсказок. Задержка отправки в 100–150 мс на клиенте отсекает больше половины из них ещё до сети — самая дешёвая оптимизация в этом кейсе. Дерево на десять миллионов запросов с топом из пяти подсказок в узле укладывается в единицы гигабайт памяти. Значит, оно помещается на один узел и реплицируется на чтение.
Связано: DATA-13 полнотекстовый поиск · PERF-03 структуры данных · CACHE-01 кэш
Источники: Elasticsearch — Suggesters · Introduction to Information Retrieval — Dictionaries and tolerant retrieval · Redis — Sorted sets
CASE-07 · Загрузка и раздача видео#
Короткий разбор
Трудность. Файлы огромны, загрузка идёт по ненадёжной сети, обработка занимает минуты, а раздача должна работать на любой скорости соединения.
Решение. Загрузка — напрямую в объектное хранилище по подписанной ссылке, частями с независимым повтором каждой (DATA-12); приложение не пропускает байты через себя. Обработка — асинхронная операция (NET-13): транскодирование в несколько качеств, нарезка на сегменты, генерация превью; конвейер строится как очередь задач с приоритетами. Раздача — adaptive bitrate (адаптивный битрейт): плейлист сегментов, клиент сам выбирает качество по фактической скорости; всё это через CDN (EDGE-05), где и лежит основная стоимость.
Компромисс. Число вариантов качества против стоимости хранения и обработки: каждое дополнительное качество — это ещё одна копия всего каталога. Отсюда практика транскодировать популярное сразу, а редкое — по запросу.
Порядок величин. Час исходника в 1080p — единицы гигабайт; транскодирование в пять качеств увеличивает хранение примерно вдвое, потому что нижние качества дешевле верхних. Сегменты режут по 2–6 секунд: короче — быстрее переключение качества и больше запросов, длиннее — наоборот. Основная статья расходов здесь не хранение и не обработка, а исходящий трафик: он обычно на порядок дороже хранения того же объёма.
Связано: DATA-12 object storage · EDGE-05 CDN · NET-13 асинхронные операции
Источники: RFC 8216 — HTTP Live Streaming · DASH Industry Forum · Netflix — Per-Title Encode Optimization
CASE-08 · Бронирование билетов#
Короткий разбор
Трудность. Место — дефицитный ресурс: продать его дважды нельзя. При этом в момент старта продаж тысячи пользователей одновременно борются за одни и те же места, а между выбором места и оплатой проходят минуты.
Решение. Двухфазная модель: резерв с ограниченным сроком, затем подтверждение оплатой. Резерв — атомарная операция в БД: UPDATE ... WHERE status = 'free' либо вставка с уникальным индексом по месту. Проверка «свободно ли» отдельным запросом с последующей записью создаёт гонку (race condition) (DATA-08). Истёкшие резервы освобождаются фоновой задачей (ARCH-08) — и она тоже должна быть идемпотентной. Всплеск на старте продаж сглаживается очередью ожидания (waiting room) и rate limiting (EDGE-06).
Компромисс. Долгий резерв удобен пользователю и снижает доступность мест для остальных; короткий — наоборот. Оптимистическая блокировка (optimistic locking) даёт лучшую пропускную способность и требует обработки отказа в интерфейсе («место только что заняли»). Пессимистическая проще, но создаёт очередь на популярных местах.
Порядок величин. Старт продаж на популярное событие — десятки тысяч человек в первую минуту на тысячи мест, то есть десяток попыток на место. При такой конкуренции конфликт при оптимистической блокировке — не исключение, а норма: интерфейс должен обрабатывать «место только что заняли» как штатный исход, а не как ошибку. Срок резерва подбирают по фактическому времени оплаты: если девяносто процентов платят за три минуты, десятиминутный резерв держит места впустую втрое дольше нужного.
Связано: DATA-08 уровни изоляции · DIST-07 распределённые блокировки · CASE-09 платёжный поток
Источники: PostgreSQL — SELECT FOR UPDATE и SKIP LOCKED · Kleppmann, DDIA, гл. 7 · Cloudflare — Waiting Room
CASE-09 · Платёжный поток#
Короткий разбор
Трудность. Деньги нельзя списать дважды и нельзя потерять. При этом внешний платёжный провайдер отвечает медленно, иногда таймаутом, и его ответ «неизвестно» — штатное состояние, а не исключение.
Решение. Три механизма вместе:
- Идемпотентность (NET-16): ключ генерируется на попытку оплаты, повтор возвращает результат первой.
- Явная машина состояний платежа (
создан → авторизован → списан → возвращён, плюснеизвестен) с разрешёнными переходами. Состояние «неизвестен» должно существовать и разрешаться сверкой (ARCH-03). - Saga для процесса вокруг платежа (DIST-08): резерв товара, списание, отгрузка — с компенсациями; возврат денег — компенсация, а не откат транзакции.
Плюс: запись в БД и публикация события — только через outbox (MSG-07); ежедневная сверка с выпиской провайдера (DATA-16) — обязательная часть, а не опция.
Компромисс. Синхронное подтверждение оплаты удобно пользователю и делает систему зависимой от времени ответа провайдера; асинхронное надёжнее, но требует показывать промежуточное состояние и уведомлять о результате.
Порядок величин. Внешний платёжный провайдер отвечает за секунды, и хвост распределения уходит в десятки секунд. Таймаут короче этого хвоста не убирает неопределённость, а увеличивает долю операций в состоянии «неизвестно» — их придётся разрешать сверкой. Расхождение при ежедневной сверке на практике не бывает нулевым: доли процента операций требуют разбора. Поэтому процесс их обработки проектируется сразу, а не заводится после первого инцидента.
Связано: NET-16 идемпотентность · DIST-08 saga · DATA-16 сверка · DATA-04 деньги как тип данных
Источники: Stripe — Idempotent requests · Stripe — Payment Intents lifecycle · microservices.io — Saga
CASE-10 · Геопоиск ближайших объектов#
Короткий разбор
Трудность. Запрос «двадцать ближайших в радиусе» нельзя обслужить обычным индексом, а если объекты движутся (курьеры, такси), позиции обновляются чаще, чем читаются.
Решение. Пространство сводится к ключу ячейки (DATA-14): грубый отбор кандидатов по своей и соседним ячейкам, затем точное вычисление расстояния на небольшом наборе. Для статичных объектов — индекс в основной БД. Для движущихся — хранилище в памяти с коротким TTL (сроком жизни записи): позиция устаревает за секунды, и её долговечность не нужна. Шардирование по ячейке даёт локальность, но требует внимания к плотным районам.
Компромисс. Размер ячейки: мелкая — точнее отбор, больше ячеек для проверки; крупная — наоборот. Горячие ячейки (hot spots) в центре города создают перекос (DATA-10). И отдельно: расстояние по прямой не равно расстоянию по дорогам — если продукту нужно второе, требуется маршрутизатор, а геопоиск лишь отбирает кандидатов.
Порядок величин. Ячейка H3 восьмого разрешения — около 0.7 км². В городе с плотностью в сотню объектов на квадратный километр это порядка семидесяти кандидатов на ячейку. Проверка своей и шести соседних даёт примерно пятьсот кандидатов, на которых точный расчёт расстояния занимает миллисекунды. Если кандидатов получается тысячи — разрешение выбрано слишком грубо, если единицы — слишком мелко и приходится расширять поиск кольцами.
Связано: DATA-14 геоиндексы · CACHE-05 Redis · DATA-10 шардирование
Источники: Uber H3 — Hexagonal hierarchical spatial index · PostGIS — Spatial indexing · Redis — Geospatial indexes
CASE-11 · Дедупликация и топ-K#
Короткий разбор
Трудность. «Сколько уникальных пользователей за сутки» и «какие сто запросов самые частые» на потоке в миллионы событий в секунду точно не посчитать. Точный ответ требует хранить все ключи в памяти.
Решение. Приблизительные структуры, дающие ответ с известной погрешностью за фиксированную память. Подсчёт уникальных — вероятностный счётчик (HyperLogLog): доли процента ошибки при килобайтах памяти вместо гигабайтов, с возможностью объединять счётчики разных узлов. Проверка «видели ли раньше» — фильтр Блума: отвечает «точно нет» или «возможно да», что подходит для отсечения дорогих обращений (CACHE-04). Топ-K — потоковые алгоритмы подсчёта частот с ограниченной памятью, обычно двухуровнево: приблизительный отбор кандидатов на потоке, точный подсчёт по короткому списку.
Компромисс. Точность против памяти — и это осознанный размен: для аналитики и антифрода погрешность в доли процента несущественна, для биллинга неприемлема. Правило: приблизительные структуры для наблюдения и решений о ранжировании, точный подсчёт — для денег.
Порядок величин. Вероятностный счётчик уникальных занимает около 12 КБ и даёт погрешность меньше процента — против сотен мегабайт при точном подсчёте десяти миллионов идентификаторов. Фильтр Блума при десяти битах на элемент даёт примерно один процент ложных срабатываний: миллион элементов помещается в полтора мегабайта. Эта разница на четыре порядка и делает приблизительные структуры единственным вариантом на потоке.
Связано: PERF-03 структуры данных · MSG-11 data pipeline · DATA-15 OLAP
Источники: Flajolet et al. — HyperLogLog (2007) · Cormode, Muthukrishnan — Count-Min Sketch · Redis — HyperLogLog
CASE-12 · Мультитенантный SaaS#
Короткий разбор
Трудность. Единственный кейс, который связывает в одном проходе изоляцию данных, авторизацию, лимиты и стоимость. Клиенты делят одну систему, но не должны видеть друг друга, мешать друг другу и платить за чужой аппетит.
Решение. Идентификатор tenant (арендатора) в каждой таблице и первым полем каждого индекса (DATA-18). Принудительная фильтрация на уровне БД или единой прослойки доступа: забытый фильтр в одном запросе означает утечку между клиентами (SEC-04). Квоты и лимиты на tenant (EDGE-06) против noisy neighbor (шумного соседа). Идентификатор tenant в логах, метриках и трассировках, иначе инцидент у одного клиента не диагностируется. Крупные клиенты и клиенты с требованиями регулятора выносятся в отдельные экземпляры или ячейки (REL-07) — отсюда естественный гибрид: общий пул для массовых, выделенные ячейки для крупных.
Компромисс. Общая модель дешевле и делает любую ошибку фильтрации инцидентом безопасности; выделенная безопаснее и превращает миграцию в распределённую операцию над сотнями экземпляров. Отдельная работа, которую закладывают заранее: экспорт и полное удаление данных одного tenant (DATA-19).
Порядок величин. Общая таблица с идентификатором tenant спокойно обслуживает десятки тысяч клиентов; отдельная БД на клиента упирается в эксплуатацию на сотнях. Разница видна на миграциях: пятьсот БД при одном проценте неуспешных применений дают несколько ручных разборов на каждый релиз. Это и есть цена сильной изоляции: её закладывают в план работ, а не обнаруживают в пятницу вечером.
Связано: DATA-18 мультитенантность · SEC-04 авторизация · REL-07 ячеистая архитектура · PERF-11 unit-экономика
Источники: AWS SaaS Lens — Well-Architected · PostgreSQL — Row Security Policies · Microsoft — Multi-tenant SaaS database tenancy patterns
CASE-13 · Разборы реальных архитектур#
Обзор
Зачем. Шесть разборов ниже ценны не решениями, а контекстом: каждое решение отвечает на конкретные числа и ограничения. Перенос решения без переноса контекста — самая дорогая ошибка, которую можно сделать, прочитав инженерный блог.
| Система | Решение | Контекст, без которого оно не работает | Карточка |
|---|---|---|---|
| Discord | Переезд на хранилище с LSM, шардирование по каналу | Триллионы сообщений, запись постоянна, читаются последние | DATA-05, CASE-03 |
| Netflix | Эволюция от общего API к слою на клиента и федерации | Сотни типов устройств с разными потребностями в данных | ARCH-10, NET-11 |
| Stack Overflow | Монолит на небольшом числе мощных серверов | Нагрузка на чтение, отличный кэш, одна команда | ARCH-01 |
| Disney+ Hotstar | Агрегация реакций на клиенте и в потоке, а не поштучно | Миллиард событий за минуты матча, точность не нужна | MSG-11, CASE-11 |
| Собственные распределённые хранилища и индексы | Масштаб, при котором готовых решений не существует | DATA-13 | |
| Prime Video | Отказ от конвейера функций в пользу единого процесса | Постоянная нагрузка, большой объём данных между шагами | ARCH-09, PERF-11 |
Как читать такие разборы. Сначала искать числа и ограничения, потом решение. Задавать вопрос «при каких условиях это перестало бы работать» — ответ обычно показывает, применимо ли решение к вашей системе. И помнить, что инженерные блоги описывают итог, а не путь. Промежуточные архитектуры, работавшие годами, в них почти не упоминаются, и создаётся впечатление, что сложное решение было принято сразу.
Связано: ARCH-01 монолит vs микросервисы · W10 trade-offs · PERF-11 стоимость
Источники: Discord Engineering Blog · Netflix Tech Blog · Prime Video Tech Blog · ByteByteGo — How Discord Stores TRILLIONS of Messages · ByteByteGo — Demystifying the Unusual Evolution of the Netflix API Architecture · ByteByteGo — Uncovering Stack Overflow's Shocking Architecture · ByteByteGo — How Disney Hotstar Captures One Billion Emojis! · ByteByteGo — Trillions of Web Pages: Where Does Google Store Them? · ByteByteGo — Amazon Prime Video Ditches AWS Serverless, Saves 90%