Модуль 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 (субъективная смерть с точки зрения одного сторожа);
  • Кворум: когда мастер «мертв» для quorum Sentinel'ов (например, 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 определяет слот, слоты разложены по мастер-узлам (реплики — внутри узла).

bash
redis-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. Сводная таблица потерь

Запомните её как рентген (мы вернёмся к ней в инцидентах):

ОтказОдиночный RedisMaster+Replica+SentinelCluster (3 мастера)
Процесс упалПотеря ≤ 1 с (AOF everysec)Потеря хвоста репликации + ≤ 1 сτот же, но только для одного слота
Узел умерПростой, пока не поднимутПростой ~5–10 с (failover)Если был мастер — ~секунды (промоушен реплики); реплики слота — без простоя
Сеть разделилась—Риск split-brain: дубли/потери хвостаКворум: мастер без большинства перестаёт принимать записи — потеря недоступности вместо рассинхрона
Память переполненаeviction/OOM (Модуль 5)τ самоеτ самое, но по узлам

Единица измерения «потеря ≤ 1 с» — это про персистентность. Про репликацию единица другая: «хвост, который мастер не успел отдать репликам» — под нагрузкой это может быть заметно больше секунды.


💻 Практика

Лаборатория «Убить мастера и посчитать потери» — стенд из boilerplate:

bash
make up-sentinel   # master + replica + 3 Sentinel
make topology      # напечатает роли и адрес мастера
  1. Нагрев: продюсер (XADD в цикле, 100 записей/с) и консьюмер (группа billing), оставьте на минуту.
  2. Измерьте репликацию: redis-cli INFO replication на реплике (lag=0?), затем WAIT 1 1000 из продюсера — почувствуйте цену синхронности по задержке.
  3. Убейте мастер: make failover (docker kill мастера). Замерьте: сколько секунд до первого успешного XADD на нового мастера (лог продюсера), что вернул SENTINEL get-master-addr-by-name.
  4. Посчитайте потери честно: продюсер пишет свой счётчик ID; сверьте с тем, что лежит в стриме после фейловера. Потерянные ID — хвост, который мастер не успел скопировать. Повторите дважды — числа разные (в этом и урок).
  5. WAIT-режим: включите в продюсере WAIT 1 200 после каждого XADD и повторите пункты 3–4. Потерь нет, задержка XADD выросла — сколько?

Отчёт лабораторной: таблица «простой / потеря / задержка» в двух режимах (без WAIT / с WAIT) — это та таблица, которую ждут на ревью.


🧩 Quiz "Проверь себя"

1. Что даёт команда WAIT 1 1000 после XADD?
2. Почему в Redis Cluster нельзя «размазать» один горячий стрим по нескольким узлам?
3. Продюсер получил ID от XADD, но через 200 мс мастер упал и Sentinel промоутил реплику. Что стало с записью?
4. min-replicas-to-write 1 при отставших репликах заставляет мастер…

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

ТемаГлавное
РепликацияАсинхронная: ACK мастера ≠ запись на реплике; WAIT N ms — договориться о копиях
SentinelОбнаружение → кворум → промоушен: простой единицы секунд, потеря = хвост репликации
Split-brainИзолированный мастер продолжает писать → рассинхрон; min-replicas-to-write — предохранитель
ClusterСлоты и hash tags; стрим не шардируется — один горячий стрим это один узел
Сводка потерьТаблица «отказ → последствия» — рентген для принятия решения

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

Мы знаем цену отказа. Теперь соберём прод-консьюмер, который эту цену отрабатывает: повторная обработка после фейловера, XAutoClaim зависших, идемпотентность поверх уже написанного обработчика, конкурентные воркеры и graceful shutdown. И проверим его тем же ножом — make failover во время нагрузки.