Этот вопрос проверяет системное мышление: как вы обеспечите высокую пропускную способность, предсказуемую задержку и работу с “тяжёлыми” данными без перегрузки сервиса и хранилищ.
Нужно разделить “онлайн-обработку” запросов и “тяжёлую обработку” данных: в запросе делаем только быстрые операции, а всё тяжёлое уносим в фоновые процессы. Масштабирование обычно горизонтальное: несколько экземпляров сервиса за балансировщиком, плюс отдельные компоненты для очередей, кеша и БД. Для больших данных важно заранее продумать модель хранения (партиции, индексы, горячие/холодные данные) и ограничить нагрузку на хранилище (кеш, батчи, асинхронщина). Обязательно закладываются таймауты, лимиты, деградация и наблюдаемость, иначе система будет “умирать” под пиками.
В таких системах главный принцип — пользовательский запрос не должен запускать тяжёлые вычисления и долгие походы по зависимостям. Архитектура обычно строится так, чтобы “быстро ответить” и “правильно посчитать” были разными потоками работы.
Определение: Online-path — действия в рамках пользовательского запроса с жёстким SLA по времени ответа. Offline/async-path — фоновые задачи без жёсткой привязки к конкретному запросу.
Практика:
В online-path:
чтение подготовленных данных (кеш/быстрое хранилище)
простая агрегация “на лету”, но только если она гарантированно быстрая
В offline-path:
предрасчёты, тяжёлые агрегации
подготовка витрин/индексов
сбор статистики, обновление рекомендаций
Чтобы работать с большими объёмами, обычно вводят несколько уровней:
“горячий” слой для быстрого чтения (часто кеш или оптимизированная БД/таблица)
“основной” слой для транзакционных данных
“аналитический/агрегационный” слой для отчётов и тяжёлых запросов
Ключевые приёмы:
Партиционирование (по времени/тенанту/региону) для ускорения выборок и упрощения очистки
Индексы под реальные запросы (а не “на всякий случай”)
Денормализация там, где важнее чтение (но с контролем консистентности)
Важно проектировать не “быстрее на стенде”, а “стабильно под нагрузкой”.
Таймауты на все внешние вызовы и операции с БД через context
Ограничение параллелизма (чтобы не убить БД/сети)
Деградация: если компонент перегружен, возвращаем упрощённый ответ или используем “последние известные данные”
Пример ограничителя параллелизма (Go), чтобы не устроить шторм в БД:
var sem = make(chan struct{}, 50) // максимум 50 одновременных запросов в БД
func withLimit(ctx context.Context, fn func(context.Context) error) error {
select {
case sem <- struct{}{}:
defer func() { <-sem }()
return fn(ctx)
case <-ctx.Done():
return ctx.Err()
}
}
Определение: Backpressure — механизм, который замедляет/ограничивает входной поток, когда система близка к перегрузке.
Практика:
лимиты на вход (rate limit)
очереди для фоновых задач
ретраи с “джиттером” и верхней границей, иначе будет лавина
Кеш нужен не “чтобы было быстрее”, а чтобы:
снять пики с БД
стабилизировать latency
переживать краткие деградации зависимостей
Подходы:
Кеш на чтение часто запрашиваемых сущностей
Кеш “срезов” (готовые ответы или их части)
TTL + инвалидация по событиям (если есть события/стрим изменений)
Без измерений вы не узнаете, почему система медленная.
Минимальный набор:
метрики RPS, latency (p50/p95/p99), ошибки по типам
метрики БД: время запросов, количество соединений, очереди
трассировка запросов (чтобы видеть “где время”)
нагрузочное тестирование до релиза, а не после инцидента
Для высокого RPS и больших данных обычно выигрывает архитектура с разделением “быстрого ответа” и “тяжёлой обработки”, предрасчётами, кешем и строгим контролем параллелизма/таймаутов. Такой подход даёт предсказуемую задержку и снижает риск перегрузки хранилищ.