Отказоустойчивость и нагрузка: 10k соединений без сюрпризов
Перед началом: модуль 6 (notification-worker на шине); из курса 1 — Модуль 6 (sync-протокол и догон по seq), Модуль 11 (метрики, лимиты, шторм-тест), Модуль 12 (релизный гейт и откат). Инструменты: docker compose, redis-cli, curl, Prometheus/Grafana, k6 (или синтетический генератор на Go). Трудоёмкость: ~6 ч.
🔥 Инцидент: «ночной апгрейд Redis и пропавшие сообщения»
Ночь, плановый апгрейд Redis. Шина — ещё в pub/sub-режиме переходного периода. Redis лежал десять секунд, реплики event-service переподключились сами, все панели позеленели. Утром — тикеты: «отправил сообщение, собеседник его не видел, пока не открыл диалог». Дежурный ищет потерянные данные, готовит объявление об инциденте с потерей.
А терять нечего. Пока Redis был недоступен, pub/sub работал как
at-most-once: события канала ушли в пустоту, и транспорт о них не помнит.
Спасла не шина, а клиент: следующий seq не пришёл — при переподключении
кадр sync дочитал дыру по REST (курс 1, Модуль 6). Инцидент оказался не
«потерей данных», а секундной задержкой доставки, которую никто не измерял:
на лаги не было ни SLO, ни алерта. В этом модуле — расклад всех отказов по
цене и нагрузочный прогон, после которого 10k соединений на реплику
перестают быть сюрпризом.
📖 Теория
7.1 Матрица отказов: радиус поражения и цена
В микросервисной топологии падать можно по очереди — и заранее знать, что увидит пользователь. Полный список «что если»:
| Что падает | Радиус поражения | Что видит клиент | Восстановление |
|---|---|---|---|
| event-реплика | соединения этой реплики | разрыв WS, авто-реконнект | переподключение с jitter + догон по seq (курс 1, Модуль 6); клиенты других реплик ничего не замечают |
| Redis, pub/sub-режим шины | все реплики, окно недоступности | задержка доставки | at-most-once: потерянные события не вернутся — клиенты дочитывают по seq; это норма транспорта, а не потеря данных |
| Redis, штатный Streams | задержка, без потерь | пауза доставки | каждая реплика — своя группа gateway-<replica-id>; pending дочитывается после возвращения Redis |
| user-service | логин, профиль, контакты | переписка работает | JWT валидируются офлайн (RS256 + кэш JWKS); синхронных вызовов на пути кадра нет |
| push-провайдер | только пуши | WS-доставка в норме | notification-worker изолирован (модуль 6): ретраи и чистка подписок не задевают шину и соединения |
Читать таблицу нужно как бюджет риска: у каждого отказа известен радиус,
известен механизм восстановления и известно, что данные при этом не
теряются — потому что хранение (seq, сообщения, outbox) живёт не там,
куда пришёл отказ.
7.2 Деградация Redis: busy-poll fallback
Штатно реплика живёт от шины: XREADGROUP блокируется до событий. Когда
шина недоступна, реплика не умирает, а деградирует: уходит в busy-poll
fallback — периодически пробует вернуться на шину и на время деградации
опирается на прямые проверки состояния (presence TTL, локальные очереди,
health шины) с редким интервалом опроса.
go// цикл потребителя: шина -> пауза -> редкий опрос, пока Redis не вернётся for { msgs, err := bus.Read(ctx, group, block) if err != nil { log.Warn("bus unavailable, degraded", "err", err) time.Sleep(pollInterval) // секунды, не миллисекунды continue } deliver(msgs) }
Компромисс называем честно: страдает latency — кадры приходят с
задержкой до интервала опроса, — но не корректность: клиентская
seq-механика делает потерю транспорта неважной, дыра закроется догоном.
Поэтому fallback обязан быть «медленным и честным», а не быстрым и
придумывающим состояние. После восстановления лаг группы покажет окно
деградации и вернётся к нулю — по этой кривой инцидент и читается.
7.3 SLO на лаги: чем измерять «доставка жива»
Три числа, по которым видно здоровье конвейера:
outbox_lag— возраст старейшего pending в outbox: от транзакции messenger до публикации в шину (метрика курса 1, Модуль 11);- лаг consumer group реплики — это и есть задержка доставки: событие считается доставленным, когда группа реплики дочитала его и разослала своим соединениям; для notification-worker та же метрика — очередь пуше;
- p99 WS-кадра — от публикации в шину до записи в сокет получателя (ориентир курса 1: p99 доставки < 300 мс).
На эти числа вешаются алерты: «лаг группы растёт при стабильном трафике» — реплика не успевает; «лаг дёрнулся и вернулся» — был отказ шины, pending дочитан. Подход к SLO и алертам — тот же, что в практикуме «Архитектурный полигон» (модуль про наблюдаемость и SLO): цель и burn-rate сначала, алерт — когда бюджет ошибки горит.
bashredis-cli XINFO GROUPS messenger.events # lag/pending по группам redis-cli XPENDING messenger.events gateway-replica-2
7.4 Нагрузочный тест: 10k соединений на реплику
Цель — ~10k коннектов на реплику event-service. База уже есть из курса 1,
Модуль 11: 20–40 KB памяти на соединение, ulimit -n 65535, лимит
соединений на пользователя, sync-budget и jitter при рестарте — всё это
переезжает в event-service без изменений.
Чем мерить:
- k6 или vegeta в WS-режиме — если нужен быстрый профиль;
- синтетический генератор на Go (в стиле loadgen полигона): N виртуальных клиентов с профилем «подключиться → sync → тишина → получать», часть клиентов сидит в группах по 30 участников и пишет — это fan-out 30.
bashulimit -n 65535 ./loadgen -url wss://host/api/v1/ws \ -conns 10000 -groups 300 -fanout 30 -dur 10m
Что смотреть:
- Память на соединение —
(RSS_после − RSS_до) / коннекты; сравнить с ориентиром 20–40 KB. Рост в разы — ищем «горутину на событие» и неограниченные буферы (инцидент курса 1, Модуль 11). - Файловые дескрипторы — коннект это сокет; упёрлись в
ulimit— потолок реплики найден раньше, чем в память. - p99 рассылки при fan-out 30 — сколько занимает разослать кадр всем получателям обычной группы.
- Дрейф лага группы — растёт при стабильной нагрузке: реплика не масштабируется, делите соединения на больше реплик; дёргается и возвращается — норма (ресинхронизация, GC).
💻 Практика
Хаос-меню: пять управляемых отказов. Для каждого — зафиксируйте в отчёте,
что увидели клиенты (переподключение, задержка, догон) и что показали
метрики (ws_connections_active, outbox_lag, лаг группы). Стенд — из
предыдущих модулей; база — два устройства и шторм-скрипт из курса 1.
- Уронить реплику event-service (15 мин). При 100+ соединениях:
docker compose kill event-2. Ожидаемо: соединения упали ровно на одну реплику, клиенты переподключились с jitter к живым репликам, догон поseqзакрыл окно; клиенты других реплик не заметили. - Уронить Redis на 10 секунд (20 мин).
docker compose stop redis, таймер,start. Прогнать оба режима шины: pub/sub — записать окно потерянных событий и убедиться, что клиенты дочитали поseq; Streams — убедиться, что после возвращенияpendingдочитан, лаг группы вернулся к нулю. Это и есть цена деградации из §7.2. - Уронить user-service (10 мин).
docker compose stop user. Проверить: логин и профиль недоступны, при этом отправка, получение и история на живых токенах работают — валидация офлайн по кэшу JWKS. - Отключить push-провайдер (10 мин). Заблокировать endpoint пушей (фейк 410/503). Ожидаемо: WS-доставка не задета, у notification-worker растут ошибки отправки и чистятся подписки, соединения и лаг реплик — в норме.
- Рестарт монолита (10 мин).
docker compose restart messengerпри живых event-репликах: соединения не рвутся, но на окно рестарта новые сообщения приходят с задержкой — outbox-воркер стоял; клиенты дочитали поseq. - Нагрузочный прогон (45 мин). Сгенератором из §7.4 доведите реплику до 10k коннектов и снимите профиль: память на соединение, fd, p99 рассылки при fan-out 30, кривая лага группы. В отчёт — число «KB на соединение» и вывод: сколько коннектов держит реплика до потолка.
🧩 Quiz
✅ Итоги модуля
- Назвать для каждого отказа (реплика, Redis в обоих режимах, user-service, push-провайдер) радиус поражения и механизм восстановления.
- Объяснить busy-poll fallback и его компромисс: latency страдает, корректность держит seq-механика клиента.
- Сформулировать SLO на outbox_lag, лаг consumer group и p99 WS-кадра — и повесить на них алерты.
- Прогнать нагрузочный сценарий до 10k коннектов и снять профиль памяти на соединение.
- Провести хаос-меню из пяти отказов с записью наблюдений по клиентам и метрикам.
🔗 Мостик к Модулю 8
Хаос-меню и SLO — это не только эксплуатация: те же проверки («рестарт без потерь», «лаг группы вернулся к нулю») — критерии приёма каждого этапа переезда. Осталось провести сам переезд: четыре сервиса, пять этапов, strangler с откатом на каждом шаге — и финальная защита с ADR.