Модуль 2. Отказоустойчивость: репликация, Sentinel, Cluster — и что теряется при отказе
Цель модуля: понять, что именно происходит с вашей очередью, когда умирает узел Redis: какая часть данных теряется, сколько длится простой, что видит клиент. Собрать мастер+реплику+Sentinel локально, сломать кластер руками и прочитать потери по журналу приложения.
Перед началом: Модуль 1 (Streams, группы, персистентность), Docker. Инструменты:
redis:7,go-redis/v9(Sentinel-режим клиента), стендboilerplate(make up-sentinel,make failover). Трудоёмкость: ~10–12 ч (теория 5 ч · практика 5 ч · квиз 1 ч).
📖 2.1. Репликация: асинхронная, и это правда о ней
Репликация в Redis — ведущая снизу вверх: реплика подключается к мастеру, получает первичный снапшот и дальше — поток команд. Ключевое слово — асинхронная: мастер отвечает клиенту «OK» до того, как команду увидела хоть одна реплика.
bash# На реплике: redis-cli REPLICAOF 127.0.0.1 6379 redis-cli INFO replication # role:slave, master_link_status:up, lag=0
Что это значит для очереди:
- Гарантия ACK ≠ гарантия записи.
XADDвернул ID — запись лежит в памяти мастера и, возможно, уже в буфере репликации, но не обязательно на диске реплики и не обязательно в её памяти; - Отставание реплики — нормальная метрика, а не аномалия: под нагрузкой или сетевой икотой реплика живёт на десятки-сотни миллисекунд позже. Пока она «жива» — failover передаст ей роль мастера вместе с её отставанием;
WAITпревращает асинхронность в договариваемую:
bash# Запись + дождаться подтверждения от 1 реплики, таймаут 1 с redis-cli SET order:42 paid redis-cli WAIT 1 1000 # вернёт число реплик, успевших подтвердить
WAIT даёт окно «записал на N реплик» ценой задержки; для критичных
событий (деньги) он обязателен, для продуктовых метрик — избыточен.
Правило для очереди на реплике: читать из реплики можно (replica-read),
но читать и подтверждать из неё нельзя — данные отстают, а XACK-и на
реплике — потерянная работа.
📖 2.2. Sentinel: автоматический failover из трёх сторожей
Sentinel — отдельный процесс (обычно 3+ инстанса), который следит за мастером и репликами и умеет выбирать нового мастера. Его арифметика:
- Мониторинг: каждый Sentinel шлёт
PINGвсем участникам; отсутствие ответа дольшеdown-after-milliseconds— этоs_down(субъективная смерть с точки зрения одного сторожа); - Кворум: когда мастер «мертв» для
quorumSentinel'ов (например, 2 из 3), наступаетo_down— объективная смерть, начинается выборы лидера Sentinel и выбор новой реплики; - Промоушен: лучшая реплика (
replica-priority, свежесть оффсета) становится мастером; остальные перенаправляются на неё; - Раздача: клиенты спрашивают у Sentinela адрес мастера
(
SENTINEL get-master-addr-by-name mymaster) и подписываются на события+switch-master. Клиентgo-redisв Sentinel-режиме делает это сам.
Честная бухгалтерия фейловера:
| Что | Число |
|---|---|
Обнаружение (down-after-ms) | ~1–5 с |
| Выборы + промоушен | ~1–3 с |
| Клиент узнал нового мастера | ~секунда |
| Простой на запись | единицы секунд |
| Потерянные записи | всё, что мастер записал и не успел скопировать на реплики |
Последняя строка — главное. Персистентность (AOF) не спасает, если она
не успела сделаться: мастер ответил OK, упал, промоутили реплику без
этих записей. Единственные противоядия: WAIT для критичных записей и
идемпотентность потребителя, чтобы повторная обработка («запись воскресла
из WAL приложения») не ломала бизнес.
Split-brain: если мастер потерял связь со всеми Sentinel'ами, но жив
и продолжает принимать записи, а кворум поднял нового мастера — записи
старого будут потеряны при возврате. Именно поэтому min-replicas-to-write
полезен: min-replicas-to-write 1 + min-replicas-max-lag 10 заставляют
мастер отказывать в записи, когда реплики отстали — недоступность вместо
рассинхрона (перечитайте это правило дважды, оно неочевидно).
📖 2.3. Cluster: слоты, MOVED и что не так с мультиключами
Redis Cluster шардирует данные по 16384 слотам: CRC16(key) % 16384
определяет слот, слоты разложены по мастер-узлам (реплики — внутри узла).
bashredis-cli -c CLUSTER KEYSLOT orders # слот ключа orders redis-cli -c CLUSTER INFO # cluster_state:ok, slots_assigned
Что важно знать про кластер и очереди:
- Hash tags задают сожительство ключей: если все ключи стрима и его
группы в одном слоте —
{orders}:streamи{orders}:lock— операции над ними работают вместе (CRC16считается только по содержимому{}). Без хеш-теговMGET/Lua/PIPELINEчерез разные слоты падают сCROSSSLOT; - Стрим целиком живёт в одном слоте. Consumer group работает
(
XREADGROUP— одна команда, один ключ), но «поперегор» между слотами нет: один горячий стрим — это один узел и его память. В отличие от Kafka-партиций, размазать нагрузку стрима по узлам нельзя; - Клиент должен уметь
-cрежим:MOVED(слот теперь на другом узле) иASK(миграция слота в процессе) обрабатывает клиентская библиотека; - Кластер и Sentinel — несовместимые миры: в Cluster отказ узла лечится кластерными кворумами, Sentinel не используют.
Зачем Queue-инженеру кластер вообще? Ровно для двух сценариев: память в десятки ГБ (не влезает в один узел) и геораспределение. Для очереди на десятки-сотни МБ кластер — это сложность без выигрыша.
📖 2.4. Сводная таблица потерь
Запомните её как рентген (мы вернёмся к ней в инцидентах):
| Отказ | Одиночный Redis | Master+Replica+Sentinel | Cluster (3 мастера) |
|---|---|---|---|
| Процесс упал | Потеря ≤ 1 с (AOF everysec) | Потеря хвоста репликации + ≤ 1 с | τот же, но только для одного слота |
| Узел умер | Простой, пока не поднимут | Простой ~5–10 с (failover) | Если был мастер — ~секунды (промоушен реплики); реплики слота — без простоя |
| Сеть разделилась | — | Риск split-brain: дубли/потери хвоста | Кворум: мастер без большинства перестаёт принимать записи — потеря недоступности вместо рассинхрона |
| Память переполнена | eviction/OOM (Модуль 5) | τ самое | τ самое, но по узлам |
Единица измерения «потеря ≤ 1 с» — это про персистентность. Про репликацию единица другая: «хвост, который мастер не успел отдать репликам» — под нагрузкой это может быть заметно больше секунды.
💻 Практика
Лаборатория «Убить мастера и посчитать потери» — стенд из boilerplate:
bashmake up-sentinel # master + replica + 3 Sentinel make topology # напечатает роли и адрес мастера
- Нагрев: продюсер (
XADDв цикле, 100 записей/с) и консьюмер (группаbilling), оставьте на минуту. - Измерьте репликацию:
redis-cli INFO replicationна реплике (lag=0?), затемWAIT 1 1000из продюсера — почувствуйте цену синхронности по задержке. - Убейте мастер:
make failover(docker kill мастера). Замерьте: сколько секунд до первого успешногоXADDна нового мастера (лог продюсера), что вернулSENTINEL get-master-addr-by-name. - Посчитайте потери честно: продюсер пишет свой счётчик ID; сверьте с тем, что лежит в стриме после фейловера. Потерянные ID — хвост, который мастер не успел скопировать. Повторите дважды — числа разные (в этом и урок).
- WAIT-режим: включите в продюсере
WAIT 1 200после каждогоXADDи повторите пункты 3–4. Потерь нет, задержкаXADDвыросла — сколько?
Отчёт лабораторной: таблица «простой / потеря / задержка» в двух режимах (без WAIT / с WAIT) — это та таблица, которую ждут на ревью.
🧩 Quiz "Проверь себя"
✅ Итоги модуля
| Тема | Главное |
|---|---|
| Репликация | Асинхронная: ACK мастера ≠ запись на реплике; WAIT N ms — договориться о копиях |
| Sentinel | Обнаружение → кворум → промоушен: простой единицы секунд, потеря = хвост репликации |
| Split-brain | Изолированный мастер продолжает писать → рассинхрон; min-replicas-to-write — предохранитель |
| Cluster | Слоты и hash tags; стрим не шардируется — один горячий стрим это один узел |
| Сводка потерь | Таблица «отказ → последствия» — рентген для принятия решения |
🔗 Мостик к Модулю 3
Мы знаем цену отказа. Теперь соберём прод-консьюмер, который эту цену
отрабатывает: повторная обработка после фейловера, XAutoClaim зависших,
идемпотентность поверх уже написанного обработчика, конкурентные воркеры
и graceful shutdown. И проверим его тем же ножом — make failover во время
нагрузки.