system-design

Проектирование распределённых систем — декомпозиция монолита на модули/сервисы (критерии границ), синхронное vs асинхронное взаимодействие (очереди, события), уровни масштабирования 100 → 10k RPS (кэш

SKILL.md

GitHub

Навык: System Design

Декомпозиция: монолит → модули → сервисы

Дефолт — модульный монолит (структура — скилл backend-architecture): модули с жёсткими границами внутри одного деплоя. Отдельный сервис выделяй, только когда у куска есть своя причина жить отдельно.

Критерии границы (модуля или сервиса):

  • Своя предметная область (bounded context): свой словарь, свои инварианты. «Биллинг» и «Уведомления» — разные контексты; «UserUtils» — не граница.
  • Свои данные: модуль владеет своими таблицами; чужие данные — только через его публичный API, не JOIN'ом в его схему.
  • Своя причина меняться: релизится/масштабируется независимо (рассылки идут пачками ночью, API — днём).
  • Свой профиль нагрузки или команда: CPU-тяжёлый рендер PDF рядом с латентно-чувствительным API — кандидат на вынос.

Пример: интернет-магазин → модули catalog (товары, поиск), orders (корзина, инварианты заказа), billing (платежи, вебхуки провайдера), notifications (email/push, ретраи). Первый кандидат в отдельный сервис — notifications: асинхронный, независимый ритм, безопасен при отставании. Плохая декомпозиция — по техслоям («сервис контроллеров», «сервис БД») или по CRUD-сущностям без инвариантов.

Синхронно vs асинхронно

Признак Синхронно (HTTP/gRPC) Асинхронно (очередь/событие)
Ответ нужен прямо в этом запросе ✅ —
Результат можно отдать позже (email, отчёт, вебхук) — ✅
Вызываемый может лежать, а мы должны жить — ✅ (очередь буферизует)
Пики нагрузки надо сглаживать — ✅
Несколько независимых потребителей одного факта — ✅ (событие, pub/sub)
«Проверь и сразу реши» в одной транзакции запроса ✅ —
  • Команда («сделай X», один получатель, важен результат) → очередь задач с подтверждением и ретраями (Redis Streams / RabbitMQ / SQS).
  • Событие («X случилось», реагирует кто подписан) → pub/sub; producer не знает потребителей.
  • Асинхронность тянет обязательства: идемпотентные потребители (at-least-once = дубли будут), DLQ для «ядовитых» сообщений, eventual consistency в UX («заказ обрабатывается»).

Масштабирование: 100 → 10k RPS

  1. ~100 RPS — один сервер приложения + одна БД. Хватает индексов и вменяемых запросов. Не усложняй.
  2. ~1k RPS — сначала кэш: Redis cache-aside для горячих чтений, CDN для статики — снимает большинство чтений с БД. Затем горизонтально масштабируй stateless-приложение за балансировщиком (состояние сессий — в Redis/JWT, не в памяти процесса).
  3. ~5k RPS — узкое место обычно БД → read-реплики (чтения на реплики, записи в primary; учитывай лаг репликации), пул соединений (pgbouncer), тяжёлые операции — в фоновые очереди.
  4. ~10k+ RPS — шардинг (партиционирование записей по ключу: user_id, tenant). Последний ход, дорог в поддержке (cross-shard-запросы, решардинг); прежде чем шардить, проверь, что не хватило кэша, реплик и выноса горячих таблиц.

Каждый уровень включай, только когда упёрся измеримо (метрики), а не «на вырост».

Отказоустойчивость (минимум)

Таймаут на каждый сетевой вызов → ретраи с экспонентой и jitter только для идемпотентных операций → circuit breaker к падающей зависимости → graceful degradation (рекомендации отвалились — каталог работает). Идемпотентность записи — ключи идемпотентности (скилл api-design).

Наблюдаемость

  • Логи — структурные (JSON), с traceId/requestId, сквозным через все сервисы.
  • Метрики — RED на каждый эндпоинт и зависимость: Rate, Errors, Duration (p50/p95/p99, не среднее).
  • Трейсинг — OpenTelemetry: один trace через сервисы и очереди; без него распределённую задержку не найти.

Что рисовать на архитектурной диаграмме

(Строить — скиллы mermaid-diagrams / drawio-diagrams.)

  • Уровень C4-container: по блоку на деплоймент-единицу — сервисы, БД, кэш, очереди, внешние API. Не по классу и не по функции.
  • Стрелки = вызовы, с протоколом и направлением инициации: API → Postgres (SQL), API → Billing (HTTP, sync), Orders → queue → Notifications (async). Sync и async визуально различай (сплошная/пунктир).
  • Границы доверия/сети (VPC, публичное/приватное) — рамками; точки входа (LB, gateway) — явно.
  • Не тащи всё в одну диаграмму: контекст (пользователи и внешние системы) — отдельно, контейнеры — отдельно, критичные потоки — sequence-диаграммой.