Если ты когда‑то бывал на собеседовании по system design, то в курсе про все особенности данного этапа. Если же нет, то вкратце:

  1. Тебе даются функциональные требования к системе

  2. Ты должен дособрать функциональные и нефункциональные требования

  3. Далее спроектировать общий каркас системы

  4. После этого углубиться в corner cases

  5. Также круто, если проговоришь мониторинги, deployment

Существуют разные системы со своими нюансами:

  • Где‑то акцент на большое число пользователей и необходимость преобработки данных (как в Ленте)

  • В каких‑то системах акцент на геоданные (сервисы Доставки)

  • В ряде систем акцент на write‑heavy нагрузку и важность работы с БД (web‑crawler)

В этой статье рассмотрим как раз один из типов таких систем: доставка котировок от биржи до клиента. Здесь акцент на отказоустойчивость и скорость доставки данных.

Будем идти поэтапно: от сбора требований и базовой конструкции до нюансов работы с данными.

Функциональные требования

Сырое описание, которое ты можешь получить на собесе:

  • Спроектировать систему реального времени для доставки данных фондового рынка, способную обрабатывать 1 миллион котировок и 1 миллион клиентов, каждый из которых подписывается на watchlist с максимум 1000 акций. Требуется минимальная задержка при доставке обновлений от биржи.

Если структурировать:

  1. Доставка котировок акций от биржи до нашей системы

  2. Котировка — текущая цена акции на биржи

  3. Ticker — 1 акция: GOOG, AAPL etc

  4. Биржа — внешняя система, которая посылает нам обновление котировки по каждой акции

  5. Клиент — пользователь нашей системы

  6. Минимальная задержка от момента изменения данных на бирже до появления в нашей системе

НФР (нефункциональные требования)

  1. DAU = 1 million

  2. Объем данных — 1 million tickers

    1. 12 байт на котировку

    2. У каждого клиента максимум 1000 tickers

  3. Необходимо реализовать ultra‑low latency систему. То есть сократить момент, когда биржа сделала event в нашу сторону и мы показали его клиенту

    1. Под ultra‑low‑latency мы закладываем SLO отдачи информации от момента публикации у Брокера до момента получения инфы клиентом. У нас это будет до 200 мс. Почему такая цифра? → Больше уже глаз человека начинает замечать различия.

  4. Прикинем, что для каждого ticker будет 10 обновлений в секунду

  5. Также наша система будет вычитывать и хранить все tickers (1 million), чтобы минимизировать задержку получения новых тикеров, а также упростить логику

  6. Про геораспределенность: так как наша система получает данные от Биржы, то нам не нужно синковать данные между нашими кластерами.

Также необходимо допом сказать про особенности системы:

  1. Потенциально кластера должны находиться в стране, где мы работаем. Так как иначе есть риск, что нам не разрешат работать в ряде стран.

  2. В реальности нужно продумать, а как мы будем обеспечивать синхронность/одинаковость данных между разными странами. Условно, в Аргентине сервера упали и клиенты там не получат данные, а в Таиланде клиенты будут получать эту инфу.

  3. В рамках Неделя 6 будет сказано про Надежность и как ее можно посчитать. На реальном собесе стоит сказать, что нам в реальной системе нужно учитывать Uptime и поэтому понадобятся доп расчеты.

Важно: так как нагрузка большая, то здесь мы будем считать помимо стандартных параметров еще и нагрузку на сеть

  • Но важный момент: на реальном интервью не нужно всегда все считать. Нужно делать только те расчеты, которые помогут тебе спроектировать систему. Если бы ты делал эту систему на собесе, то можно кратко обсудить с интервьюером, что нагрузка большая и примерно накинуть расчеты, но не разбирать до винтика.

  • Ниже будет подробный расчет сети, чтобы ты понимал, а как это делается.

1. Расчет нагрузки на сеть (Bandwidth)

1.1. Исходящий трафик (от нашей системы к клиенту)

Объем данных на одного клиента в секунду:

Кол‑во обновлений в секунду: 1 000 тикеров × 10 обновлений = 10 000 обновлений/сек

Объем данных: 10 000 обновлений/сек × 12 байт = 120 000 байт/сек

Перевод в КБ: 120 000 байт/сек ≈ 117.19 КБ/сек

Общая нагрузка на исходящий трафик (для всех клиентов):

Всего клиентов: 1 000 000

Общий объем данных: 1 000 000 клиентов × 120 000 байт/сек = 120 000 000 000 байт/сек

Перевод в ГБ: 120 000 000 000 байт/сек = 120 ГБ/сек

1.2. Входящий трафик (от биржи в нашу систему)

Запросов в секунду (RPS): 1 000 000 тикеров × 10 обновлений = 10 000 000 RPS

Объем данных: 10 000 000 RPS × 12 байт = 120 000 000 байт/сек

Перевод в более крупные единицы:

≈ 120 МБ/сек

≈ 0.12 ГБ/сек

2. Расчет хранилища (Storage)

2.1. Real‑time данные (в оперативной памяти)

Общее количество тикеров: 1 000 000

Размер одного тикера: 12 байт

Теоретический объем активных данных: 1 000 000 × 12 байт = 12 000 000 байт = 12 МБ

? Комментарий: Данный расчет является теоретическим минимумом для «сырых» данных. На практике к ним добавляются служебные данные, ключи, индексы и метаданные системы. Реальный объем рекомендуется закладывать с запасом.

Ориентировочный объем с запасом (x5): 12 МБ × 5 = 60 МБ

2.2. State данные (снимок состояния на момент времени)

Ориентировочный объем: ~ 60 МБ (активные данные с запасом)

2.3. Требования к производительности Redis

Необходимая производительность на запись: 10 обновлений/сек × 1 000 000 тикеров = 10 млн операций записи в секунду (write ops)

Как мы видим, входящая нагрузка не такая большая, а вот исходящая — серьезная.

Основные компоненты архитектуры

Упрощенная схема
Упрощенная схема

Для начала давай верхнеуровнево посмотрим на все модули, а потом погрузимся в каждый кусок по отдельности.

GeoDNS

Данный элемент позволит нашей системе устанавливать соединение с ближайшим сервером биржи

Биржа

Внешняя система, которая высылает котировки по тикерам (акциям)

Data Receiver

Система, которая принимает данные от Биржи, обрабатывает и отправляет дальше в нашу систему

Pub/Sub cluster

Инструмент для агрегации тикеров. Выполняет сразу 2 функции:

  • Быстро передает данные от Биржи к Клиенту (так как нет записи на Диск)

  • Сохраняет время от времени state, чтобы при подключении/переподключении клиента можно было накатить последний state у tickers

API streaming

Сервис для вычитки инфы из Pub/Sub cluster и доставки к клиенту

Api Gw

В данном случае выполняет несколько функций:

  • auth для проверки, что клиент имеет право читать инфу с Биржи

  • Перевод с gRPC‑Web на gRPC streaming (об этом подробнее поговорим в блоке про API Gw)

LB

Классический LB, который занимается роутингом соединений от клиента до Api Gw

Детально про каждый модуль

Слой приёма данных — Data Receiver

Функции:

  • Приём высокочастотных данных с биржи

  • Обеспечение непрерывного потока данных для системы

  • Запись данных в Pub/Sub

Соединение будет по Websocket, так как polling/webhook недостаточно быстрые по скорости.

Можно было бы рассмотреть SSE (server‑site event), но:

  • Он чуть медленнее WebSocket

  • Также он требует reconnect время от времени

Как идет процесс (если система распределенная):

  1. Наша компания говорит IP адреса наших серверов в ДЦ Бирже

  2. Биржа добавляет их в whitelist

  3. При запросе от нашей системы запрос попадает на GeoDNS. Далее DNS распределит запрос на ближайший сервер биржи

  4. Устанавливается Websocket connection

?Но, если бы у нас было ограниченное кол‑во ДЦ и у биржи было мало серверов, то можно спокойно сделать хардкод IP биржи на нашей стороне

В финальной версии будет несколько Data Receiver для отказоустойчивости.

In-Memory хранилище — Pub/Sub

Нам нужно решение для агрегации tickers с минимальной задержкой. Будем использовать решение Redis Pub/Sub (альтернативы разберем далее в этом блоке).

  • Важно: на самом интервью можешь не говорить, что обязательно Redis Pub/Sub, а скорее концепцию из данной технологии. Так как часто в Big Tech либо делают свои решению, либо дорабатывают имеющиеся. Это избавит тебя от дополнительных вопросов «А точно ли так можно делать в Redis»

Важно: когда ты говоришь «Мы сделаем 10 шардов, 15 реплик» — нужно объяснение данного решения. Без объяснения это выглядит непрофессионально.

Конфигурация:

  • Используем одну ноду для записи и чтения с нее. В рамках этой ноды Redis сделает логические очереди для subscribers (так как каждый клиент подписан на разные tickers). По сути это аналог topic в Kafka.

  • В то же время делаем async replication на наши другие ноды. Из‑за частого обновления тикеров даже если будет какая‑то потеря, то не критично.

    • Как вариант, можно обсудить semi‑sync вариант репликации

  • В случае отказа основного инстанса Redis Sentinel (watchdog, который мониторит состояние Redis) делает failover на более подходящую реплику

  • Для redundancy нам нужно поставить доп реплики

  • Сразу возникает вопрос: а если не будем держать нагрузку по connections к Redis инстансу?

    • Более валидные варианты:

      • Провести load testing и понять реальное ограничение. До определенного уровня вертикальное масштабирование может помочь.

      • Вообще поменять решение с Redis Pub/Sub на Kafka (разберем в отдельном блоке про падение Redis).

    • Менее валидные варианты:

      • Возможен вариант с вычитки из реплик, но есть риск потери данных и задержек.

      • Можно на уровне publisher сразу писать в несколько реплик (то есть данные будут консистентны) и держать больше нагрузки. Но тут сразу момент с отслеживанием конфликтов между репликами.

Механизм Pub/Sub:

Redis используется для подписки стриминговых API на обновления в реальном времени, что позволяет эффективно рассылать изменения клиентам.

Redis будет хранить state, чтобы сразу выдавать последнюю инфу для клиентов, которые были не в онлайне, а потом подключились. То есть при подключении клиента происходит вычитка по нужным ключам. А далее клиент уже начинает слушать очередь и получать новые данные.

В Redis используется термин snapshot, поэтому далее будем указывать этот термин. Но помни, что это «снимок» данных на какой‑то момент времени и мы будем его обновлять время от времени.

Даже если клиент долго не был подключен, то snapshot будет всегда обновляться и клиент получит свежие данные.

?Как система понимает, что сначала нужно отдать snapshot, а уже потом поключиться слушать очередь?

  • Стриминг сервис получает от клиента список тикеров

  • Далее он сходит в Redis за snapshot

  • А уже потом подписка на прослушивание очереди

Сценарий

Как система работает

Клиент подключён и слушает Pub/Sub

Получает все новые обновления в реальном времени

Клиент не подключён в момент публикации

Пропускает эти события

Клиент подключается заново

Получает snapshot из Redis (актуальные данные) и затем подписывается на Pub/Sub для новых обновлений

Почему именно Pub/Sub + snapshot, а не полноценная Kafka/RabbitMQ?

Критерий

Redis Pub/Sub + Snapshot

Kafka / RabbitMQ

Гарантия доставки

Нет гарантии: если подписчик не подключён, сообщение теряется

Kafka/RabbitMQ сохраняют сообщения, можно получить позже

Персистентность

Нет хранения сообщений, только актуальное состояние в Redis

Сообщения хранятся на диске, можно настраивать срок хранения

Задержка (latency)

Минимальная, ultra‑low latency

Kafka — низкая, но чуть выше из‑за записи на диск; RabbitMQ — низкая

Надёжность при сбоях

Возможна потеря сообщений при падении/отключении

Сообщения не теряются, можно восстановить после сбоя

Использование памяти

Всё в памяти, быстро, но ограничено объёмом RAM

Kafka/RabbitMQ используют диск, могут работать с большими объёмами

Поддержка snapshot

Нужно реализовывать отдельно

В Kafka можно читать с любого offset, реализуя snapshot/историю

Когда Redis Pub/Sub + snapshot лучше?

  • Минимальная задержка критична (например, для мгновенной реакции на изменение котировки).

  • Можно мириться с потерей данных (например, если важна только последняя цена, а не вся история изменений).

Когда Kafka или RabbitMQ лучше?

  • Нельзя терять сообщения — критична гарантия доставки (например, для финансовых транзакций, аудита, аналитики).

  • Нужна история изменений — клиенты могут читать не только последние данные, но и всю последовательность событий.

  • Возможна временная недоступность клиентов — сообщения сохраняются и могут быть доставлены позже.

А как мы будем хранить snapshot?

  • Он будет храниться на каждой реплике

  • При подключении клиента в онлайн Redis вытащит snapshot из БД

Итого получается примерно такая картина:

Стриминговые API серверы

Каждый клиент будет получать тот набор tickers, который необходим ему. В этом нам поможет механизм Pub/Sub и библиотека Redis

Если вдруг появляется новый клиент с новыми tickers (или же отключается текущий и часть tickers пропадает)

  • Для нового клиента сначала произойдет вычитка snapshot из Redis

  • Добавление/изменение каналов может происходить через отдельный поток, чтобы оптимизировать latency

Обработка клиентских подключений до API серверов

Важно: здесь мы разбираем только кейс с получением tickers. Мы не разбираем доп API с /show‑tickers и тд.

Технологии:

  • gRPC стриминг для эффективной передачи данных

  • Около 100 серверов API рассчитано по пропускной способности сети (120 КБ/с на клиента)

То есть именно этот сервис будет subscriber«ом на redis и он делают агрегацию по tickers для клиентов. Также они держат соединения между клиентами и нашей системой.

Далее разберем формат подключения от клиентов до API сервисов

  1. GeoDNS нужен для выбора ближайшего LB к клиенту

  2. API Gw выполняет несколько ролей:

    1. auth

    2. Для браузеров будет менять формат соединения с gRPC‑Web (об этом далее) на gRPC. Или чисто WebSocket

    3. Распределение нагрузки на какой‑то из сервисов (так как L7, то можно задействовать Api GW, а можно поставить до LB L7)

Немного про виды gRPC:

В gRPC есть несколько режимов взаимодействия между клиентом и сервером:

  • Unary (однонаправленный вызов): клиент отправляет один запрос, сервер отвечает одним ответом.

  • Server streaming: клиент отправляет один запрос, сервер возвращает поток сообщений.

  • Client streaming: клиент отправляет поток сообщений, сервер отвечает одним ответом.

  • Bidirectional streaming: и клиент, и сервер обмениваются потоками сообщений одновременно.

gRPC streaming — это режимы server streaming или bidirectional streaming, позволяющие передавать данные непрерывно без необходимости открывать новое соединение. Именно эти режимы используем.

  • Почему именно такие виды? Так как клиенту нужно иногда делать запросы, если он хочет поменять тикеры. В остальное время клиент «слушает» изменения от системы

Для браузеров будем использовать gRPC‑Web, чтобы также использовать все плюсы HTTP 2.0. Но если в рамках тестирования мы увидем, что latency на переупаковку слишком большой — для Web клиентов делаем WebSocket.

Финальная картина

Разбор кейса с падением Redis

А что если наш кластер Redis упадет? Это будет означать конец для всей системы.

Существует много вариантов, как сделать данное решение более надежным.

?В примерах ниже везде будет circuit breaker, который активируется если от Redis пришло n ошибок за m времени.

?Подход для обработки ошибок называется fallback: это может быть как выброс exception клиенту, так и переход на запасной вариант обработки

?Подход, когда мы не сразу падаем в случае отказа какого‑то модуля, а пытаемся сделать это незаметно для клиента или с минимальными проблемами — graceful degradation

Плохой вариант:

  1. Вообще не использовать Redis и заменить на Kafka. Но тут нужно думать про а) Пропускную способность (так как у нас ultra low latency) б) Удаление данных, так как поток tickers очень большой. То есть вариант с Kafka потребует очень тонкой настройки.

Рабочие варианты:

  1. Поставить Kafka рядом. Запись будет идти как в Redis, так и в Kafka.

    1. Если включается circuit breaker, то начинаем читать из Kafka. Это медленнее, но зато получаем хоть какой‑то поток данных. Плюс Kafka можно оптимизировать для более быстрой вычитки.

    2. Также есть риск, что запишем данные в redis, а в Kafka не запишем (ошибки сети, упал broker). Можно подумать про transactional outbox.

  2. В общем такой подход называется Dual Message Broker Pattern. В данном случае система мониторит latency или кол‑во ошибок от Redis. Если какое‑то из условий выполняется, то делается fallback на Kafka.

    Также можно слать event Клиентам, что у нас проблемы, поэтому доставка данных будет чуть дольше.

  3. Поставить еще один Redis, в которых переливать данные регулярно. Если отрубается основной кластер, то идем на запасной

    1. В данном случае будет больше нагрузка на основной кластер, так как будет репликация

    2. Доп расходы по обеспечению кластера

    3. То есть можно настроить след механизмы и переливать их с одного кластера на другой

    • RDB (snapshotting): периодически сохраняет полную копию данных на диск (dump.rdb)

    • AOF (Append‑Only File): логирует каждую команду изменения данных, чтобы можно было восстановить состояние после сбоя

Как можно упростить связь клиента и нашей системы

Из‑за того, что браузеры нативно не поддерживают gRPC streaming, приходится использовать gRPC‑web и затем на Api Gw делать переключение на gRPC‑streaming. Это также может негативно сказываться на response time. И таким образом клиенты в мобильном приложении получат преимущество перед теми, кто в браузере.

Как вариант для браузеров можно заиспользовать WebSocket соединение, чтобы не приходилось менять протокол на Api Gw.

Но сразу учитываем проблемe: гораздо бОльшая нагрузка на нашу систему из‑за постоянно открытого connection

Как обезопасить систему от наплыва

В текущей архитектуре если какой‑то из клиентов начнет нагружать систему сильнее, то другие клиенты также пострадают.

Для повышения надежности можно использовать паттерн bulkhead. Прочитай ниже, что это.

В нашем случае мы можем разделить потоки для клиентов с браузера и клиентов с мобильного приложения. И как результат — если какая‑то из систем будет не справляться → слать alert, чтобы клиенты могли переключаться с телефона или же браузера.

А как же Rate Limiter

В нашем случае Rate Limiter будет на Api Gateway. Ситуация, когда клиент может начать нас заваливать: постоянное добавление и удаление тикеров, а также постоянное переподлючение к нашей системе.

В базовом формате будем использовать token bucket, так как:

  • Фиксированное потребление токенов клиентом

  • Если происходит какое‑то событие на бирже и нужно держать больше клиентов, то накидываем больше tokens

Не забываем про bursts: короткие периоды, когда мы позволяем клиентам делать чуть больше запросов. Например, 30 секунд делать на 30% больше

Observarbility

Технические метрики (примерно)

Latency

  1. Exchange → Data Receiver: <0.1ms (P99)

  2. Data Receiver → Redis: <0.2ms (P99)

  3. Redis → API Services: <0.3ms (P99)

  4. API Services → Client: <0.4ms (P99)

Resource utilization

  1. Redis Memory Usage: <75% per shard

  2. CPU Utilization: <70% P95 по всем сервисам

  3. Network I/O: <90% of interface capacity

Продуктовые метрики

  • Client Session Duration: среднее >4 часа

  • Client Churn Rate

Service Level Indicators (SLIs) & Objectives (SLOs)

Service

SLI

SLO

Error Budget

Quote Delivery

Availability

99.99%

4.3 min/month

API Response

Latency P99

<5ms

5% requests

Data Accuracy

Error Rate

<0.001%

10 errors/million

Connection Success

Success Rate

>99.9%

0.1% failure rate

А также не забываем про Chaos Engineering.

Заключение

Поток данных в системе

  1. Приём данных: Data Receiver получает поток с биржи.

  2. Хранение и публикация: Данные записываются в Redis, который публикует обновления.

  3. Подписка клиентов: API серверы подписываются на нужные каналы Redis в зависимости от подписок клиентов.

  4. Доставка клиентам: API серверы сразу передают обновления клиентам через gRPC стримы.

  5. Синхронизация: При подключении клиента ему сначала отправляется актуальный снимок данных из Redis, затем начинаются стриминговые обновления.

Взаимодействие компонентов

Архитектура реализует паттерн fan‑out:

  • Один источник данных (биржа)

  • Несколько Data Receiver«ов»

  • Несколько реплик Redis

  • Множество стриминговых API серверов

  • Миллионы клиентов

Так обеспечивается масштабируемость и низкая задержка доставки.

Отказоустойчивость и мониторинг

  • Репликация Redis для сохранности данных

  • Множество API серверов для отказоустойчивости клиентских подключений

  • Снимки (snapshot) Redis для восстановления после сбоев

  • Кластер LB для отказоустойчивости балансировки

Полезные ресурсы

  1. Проблемы Redis и как их можно решить с Dragonfly


Что дальше?

Паттерны микросервисной архитектуры

OTUS совместно с экспертами платформы algocode проведёт открытый онлайн-урок, посвящённый паттернам микросервисной архитектуры.

Дата и время: 28 октября 2025 года, 19:00 (МСК)
Формат: онлайн
Спикер: Даниил — Team Lead в крупной технологической компании, имеет более 5 лет опыта и провёл свыше 50 технических собеседований.

Темы урока:

  • основные паттерны микросервисной архитектуры: service discovery, retries, API Composition и другие;

  • практические примеры применения паттернов (bulkhead для кеширования, CQRS с MongoDB и Kafka, rate limiter при масштабировании);

  • проектирование надёжной архитектуры и подготовка систем к высоким нагрузкам;
    взаимодействие между сервисами и работа с брокерами сообщений;

  • распространённые ошибки при проектировании микросервисов;

  • методы отслеживания и анализа проблем в системе.

    Подробности доступны на странице урока.

Справочная информация:
Материалы algocode включены в программу курса OTUS
«Golang Developer. Professional» и нацелены на освоение паттернов микросервисной архитектуры.

Комментарии (1)


  1. mrfloony
    24.10.2025 08:14

    А чем вам FAST / FIX не угодил?