Нагрузка и надёжность
Перед началом: весь функционал (Модули 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 на ноду |
| Goroutines | read-pump + write-loop на коннект — это ок; «горутина на событие» — нет |
Правило: соединение — дешёвое, запрос в БД на соединение — дорогое. Всё, что можно, считается в памяти: подписки, буферы, агрегация.
11.2 Sync-бюджет: почему шторм убивает базу
Из инцидента: N коннектов × M диалогов отдельных запросов — квадратичный взрыв. Лечим тремя механизмами:
- Батч — догон одним запросом по всем диалогам (сделали в Модуле 6:
unnest-пары). 200k запросов превращаются в 10k. - Throttle — на соединение разрешён один sync не чаще, чем раз в
~5 секунд; повторные
syncв окне — дропаем (клиент получит live). - Jitter — клиентский реконнект с экспоненциальной паузой и случайным разбросом (иначе 10k клиентов синхронны как метроном):
tsconst 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 коннектов;
- шторм-тест:
killAPI при нагрузке → все клиенты догоняют без потерь (< 30 секунд), лаг outbox возвращается к нулю.
11.6 Потолок одной ноды — и что за ним
Одна нода — честный потолок: CPU, дескрипторы, память. Когда 10k–50k соединений пройдены, следующий шаг — горизонтальная шкала реплик ws-gateway: событие диалога публикуется в общий канал (Redis Streams), и каждая реплика доставляет его своим соединениям — неважно, на какой реплике сидит получатель:
Рисунок — из будущего курса трека (микросервисная эволюция): обратите
внимание, что контракт события и seq не меняются — меняется только
транспорт между репликами. Именно поэтому мы с самого начала возили
события через outbox: замена брокера — это замена одной внутренности, а
не переписывание системы.
💻 Практика
-
Метрики: подключите prometheus-клиент, закройте набор §11.4 (счётчики соединений — в хуке accept/close, sync-гистограмма — вокруг догона, lag — периодический
SELECT min(created_at) FROM outbox_events WHERE status='pending').GET /metrics— без авторизации, но не наружу: только для внутренней сети (в Модуле 12 прикроем на nginx). -
Конфиг и лимиты:
ulimit -n, размеры буферов,GOMAXPROCS, пул pgx (max_conns), лимит соединений на пользователя и на IP (у нас есть только per-user — добавьте per-IP на accept). -
Реализуйте sync-throttle и глобальный sync-бюджет (§11.2, п.2–3).
-
k6-сценарий (фрагмент):
jsimport 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) }) }
- Прогоны и отчёт: базовый (2k), шторм (рестарт под нагрузкой), деградация (остановить Redis — сообщения живы). В отчёт — скриншоты метрик до/во время/после и один абзац «что стало узким местом и что поменяли».
Чек-лист модуля: /metrics отдаёт набор из §11.4; sync — батч +
throttle; клиент реконнектится с jitter; шторм-тест с рестартом проходит
без потерь; в отчёте есть узкое место и его лечение.
🧩 Quiz
outbox_lag_seconds растёт. Что это значит?✅ Итоги модуля
- Экономика соединений: WS дешёвые, запросы в БД — дорогие; пул отдельно от коннектов.
- Sync-бюджет: батч + throttle + jitter; рестарт не превращается в само-DDoS.
- Backpressure: переполнение буфера соединения = дроп, который лечится догоном.
- Метрики из §11.4 — минимальный язык разговора об инцидентах.
- Нагрузочный отчёт с узким местом — обязательный артефакт перед деплоем.
🔗 Мостик к Модулю 12
Выдерживает. Пора выкатывать: соберём образы, поднимем VM, настроим release-конвейер с health-гейтом и откатом — по тому же паттерну, по которому живёт рабочий стенд, на который мы равнялись с самого начала.