Нагрузка и надёжность

Перед началом: весь функционал (Модули 1–10). Инструменты: k6, Prometheus-метрики, sysctl/ulimit. Трудоёмкость: ~7 ч.

🔥 Инцидент: «Шторм реконнектов положил базу»

Деплой в 12:00. Процесс перезапустился — и 10 000 клиентов одновременно пошли в reconnect. Каждый коннект делал sync по 20 диалогам отдельным запросом на диалог — 200 000 запросов за несколько секунд. Postgres лёг, sync не завершился ни у кого, клиенты пошли в повторный реконнект — цикл. Через день — второй инцидент, тише: «100% CPU на 500 коннектах». Виновник — горутина на каждое событие с неограниченным каналом: гора мусора и GC-шторм. В этом модуле: лимиты, бюджеты sync, метрики — и проверка нагрузкой, которая делает такие инциденты скучными.

📖 Теория

11.1 Экономика соединений

Ориентиры для одной ноды (учебные, но честные):

РесурсПрикидка
10k WS-коннектов~20–40 KB памяти на коннект (буферы чтения/записи, горутины)
Файловые дескрипторыконнект = сокет: нужен ulimit -n 65535 и запас
Пул БД10k коннектов ≠ 10k DB-коннектов: в БД ходит только sync/отправка, пул 20–50 на ноду
Goroutinesread-pump + write-loop на коннект — это ок; «горутина на событие» — нет

Правило: соединение — дешёвое, запрос в БД на соединение — дорогое. Всё, что можно, считается в памяти: подписки, буферы, агрегация.

11.2 Sync-бюджет: почему шторм убивает базу

Из инцидента: N коннектов × M диалогов отдельных запросов — квадратичный взрыв. Лечим тремя механизмами:

  1. Батч — догон одним запросом по всем диалогам (сделали в Модуле 6: unnest-пары). 200k запросов превращаются в 10k.
  2. Throttle — на соединение разрешён один sync не чаще, чем раз в ~5 секунд; повторные sync в окне — дропаем (клиент получит live).
  3. Jitter — клиентский реконнект с экспоненциальной паузой и случайным разбросом (иначе 10k клиентов синхронны как метроном):
ts
const delay = Math.min(30_000, 500 * 2 ** attempt) * (0.5 + Math.random())

Сервер при рестарте «включает свет» постепенно: принимая коннекты, держит sync-budget глобально (например, 500 sync/сек) и мягко отклоняет лишние (клиент повторит по jitter-паузе).

11.3 Backpressure: медленный клиент

Медленный потребитель (труп вкладки, слабый канал) не должен копить память. Уже заложено в бойлерплейте: буфер соединения ограничен, при переполнении события для этого соединения дропаются — клиент восстановит догоном по seq. Это лучшее свойство seq-модели: потеря в транспорте не потеря данных.

11.4 Метрики, по которым ловится всё это

Минимальный набор (Prometheus-формат, /metrics):

ws_connections_active            gauge    — текущие соединения
ws_connects_total{result}        counter  — connect/reject (лимит, origin)
ws_sync_duration_seconds         histogram — догон (p99!)
ws_sync_throttled_total          counter  — отклонённые sync
ws_events_sent_total{type}       counter  — message.new, presence, …
outbox_lag_seconds               gauge    — возраст старейшего pending
http_request_duration_seconds    histogram

Алерты-напоминания: outbox_lag > 5s (доставка встала), ws_sync_duration p99 > 500ms (шторм), ws_connections_active падает до нуля (мессенджер «выключился»).

11.5 Нагрузочный тест

Инструмент — k6: сценарий «подключиться, синхронизироваться, отправить, получить». Целевые числа курса:

  • 2 000 коннектов на ноутбуке — обязательный прогон;
  • 10 000 — на VM с горизонтальной шкалой (если решите остаться на одной ноде — честно зафиксируйте потолок);
  • p99 доставки сообщения (от POST до WS у получателя) < 300 мс;
  • p99 sync < 100 мс при 2 000 коннектов;
  • шторм-тест: kill API при нагрузке → все клиенты догоняют без потерь (< 30 секунд), лаг outbox возвращается к нулю.

11.6 Потолок одной ноды — и что за ним

Одна нода — честный потолок: CPU, дескрипторы, память. Когда 10k–50k соединений пройдены, следующий шаг — горизонтальная шкала реплик ws-gateway: событие диалога публикуется в общий канал (Redis Streams), и каждая реплика доставляет его своим соединениям — неважно, на какой реплике сидит получатель:

Fan-out между репликами (следующий курс)

Рисунок — из будущего курса трека (микросервисная эволюция): обратите внимание, что контракт события и seq не меняются — меняется только транспорт между репликами. Именно поэтому мы с самого начала возили события через outbox: замена брокера — это замена одной внутренности, а не переписывание системы.

💻 Практика

  1. Метрики: подключите prometheus-клиент, закройте набор §11.4 (счётчики соединений — в хуке accept/close, sync-гистограмма — вокруг догона, lag — периодический SELECT min(created_at) FROM outbox_events WHERE status='pending'). GET /metrics — без авторизации, но не наружу: только для внутренней сети (в Модуле 12 прикроем на nginx).

  2. Конфиг и лимиты: ulimit -n, размеры буферов, GOMAXPROCS, пул pgx (max_conns), лимит соединений на пользователя и на IP (у нас есть только per-user — добавьте per-IP на accept).

  3. Реализуйте sync-throttle и глобальный sync-бюджет (§11.2, п.2–3).

  4. k6-сценарий (фрагмент):

js
import ws from 'k6/ws'
export default function () {
  const url = `ws://localhost:8080/api/v1/ws?token=${__ENV.TOKEN}`
  ws.connect(url, {}, (socket) => {
    socket.on('open', () => {
      socket.send(JSON.stringify({ type: 'sync', since: [] }))
    })
    socket.on('message', (msg) => { /* считаем доставку */ })
    socket.setTimeout(() => socket.close(), 60_000)
  })
}
  1. Прогоны и отчёт: базовый (2k), шторм (рестарт под нагрузкой), деградация (остановить Redis — сообщения живы). В отчёт — скриншоты метрик до/во время/после и один абзац «что стало узким местом и что поменяли».

Чек-лист модуля: /metrics отдаёт набор из §11.4; sync — батч + throttle; клиент реконнектится с jitter; шторм-тест с рестартом проходит без потерь; в отчёте есть узкое место и его лечение.

🧩 Quiz

1. Соединение не успевает отправлять события, буфер полон. Что делаем?
2. Шторм-тест: убили API под нагрузкой. Какой результат — «пройден»?
3. Зачем jitter в реконнекте, если есть экспоненциальная пауза?
4. Почему 10 000 WS-коннектов — это не 10 000 подключений к Postgres?
5. outbox_lag_seconds растёт. Что это значит?
6. Что случилось в «шторме реконнектов» и какое лечение — первое?

✅ Итоги модуля

  • Экономика соединений: WS дешёвые, запросы в БД — дорогие; пул отдельно от коннектов.
  • Sync-бюджет: батч + throttle + jitter; рестарт не превращается в само-DDoS.
  • Backpressure: переполнение буфера соединения = дроп, который лечится догоном.
  • Метрики из §11.4 — минимальный язык разговора об инцидентах.
  • Нагрузочный отчёт с узким местом — обязательный артефакт перед деплоем.

🔗 Мостик к Модулю 12

Выдерживает. Пора выкатывать: соберём образы, поднимем VM, настроим release-конвейер с health-гейтом и откатом — по тому же паттерну, по которому живёт рабочий стенд, на который мы равнялись с самого начала.