Пока Redis живёт на одном узле, у него ровно один потолок производительности и ровно одна точка отказа. Чтобы снять оба ограничения, Redis умеет реплицироваться, автоматически переключать мастера при отказе (Sentinel) и шардировать данные по узлам (Cluster). Но каждый из этих механизмов делает Redis распределённой системой — со всеми вытекающими компромиссами модели CAP, о которых легко забыть, пока всё работает. Асинхронная репликация быстра, но не гарантирует, что подтверждённая клиенту запись переживёт падение мастера.
Это четвёртая статья серии «Redis: глубокое погружение», и именно здесь сквозная нить «Redis как кэш vs Redis как источник истины» становится острой. Мы честно разберём, что репликация и Cluster дают, а чего не дают: где возможна потеря записей, что такое split-brain в Redis, зачем нужна команда WAIT и почему Sentinel не стоит путать с настоящим консенсусом.
В статье
- Как читать числа этой статьи
- Асинхронная репликация и
WAIT - Cluster: hash slots и resharding под нагрузкой
- Cluster failover: сервер переключился, а клиент — нет
- Sentinel: failover при
down-after-milliseconds=5000— и TILT, который его срывает - Split-brain: 75 подтверждённых записей, которых клиенты не видят
- Где здесь граница durability
- Что дальше
- Источники
Как читать числа этой статьи
Этот раздел придётся прочитать до таблиц, иначе из них можно унести неправду.
Все тайминги failover — следствие конфигурации, а не свойство Redis/Valkey. Что стояло на стенде:
| Параметр | Значение | Что определяет |
|---|---|---|
cluster-node-timeout |
15000 (дефолт) |
когда Cluster признаёт мастер упавшим |
Sentinel down-after-milliseconds |
5000 (снижено с дефолтных 30000) |
когда Sentinel признаёт мастер s_down |
Sentinel failover-timeout |
10000 (снижено с дефолтных 180000) |
ограничения на процесс failover |
Sentinel quorum |
2 из 3 |
сколько Sentinel должны согласиться |
| Клиент go-redis | redis.ClusterOptions{Addrs: addrs} — всё по умолчанию, DialTimeout/ReadTimeout/MaxRetries не настраивались |
как долго клиент висит на мёртвом узле |
| Режим сетевого отказа | docker bridge: контейнер удалён из сети → connect: no route to host |
как быстро сокет узнаёт о смерти узла |
С дефолтными 30000/180000 числа Sentinel были бы принципиально другими. Не цитировать «Sentinel переключается за 7 секунд» без указания down-after-milliseconds=5000.
Две последние строки — не формальность, а половина заголовочной цифры статьи. Клиентское окно в 42–49 секунд, о котором пойдёт речь ниже, — свойство ненастроенного клиента на виртуализованном bridge, а не свойство ClusterClient как библиотеки. Не цитировать «go-redis восстанавливается 45 секунд» как характеристику go-redis.
Оговорка про среду здесь сильнее, чем в предыдущих статьях серии. Стенд поднят на Windows-хосте через Docker Desktop: сеть и таймеры виртуализованы. Для этой статьи это не фон, а действующее лицо. Партиция делается через docker network disconnect — интерфейс выдёргивается целиком, что ближе к «выдернули кабель», чем к типичной сетевой аварии с потерей пакетов или односторонней связностью. А Sentinel на таком хосте регулярно уходит в TILT — на bare-metal Linux этого, скорее всего, не случилось бы. Всё, что ниже, — правда про эту среду.
Асинхронная репликация и WAIT
Репликация в Redis асинхронна по умолчанию, и это главный факт раздела. Мастер отвечает клиенту OK не дожидаясь подтверждения от реплик; поток изменений уходит к ним следом. Отсюда прямое следствие: между моментом, когда клиент получил OK, и моментом, когда запись доехала до реплики, существует окно. Упадёт мастер внутри этого окна — подтверждённая запись исчезнет из системы, хотя клиенту про неё сказали «принято».
Команда WAIT смещает эту границу, но не отменяет её. WAIT <числоРеплик> <таймаут> блокирует клиента, пока указанное число реплик не подтвердит получение. Это не превращает репликацию в синхронную и не даёт консенсуса: WAIT возвращает фактическое число подтвердивших реплик, а не гарантию, и по истечении таймаута вернёт меньше запрошенного, не откатив запись. То есть WAIT — инструмент сужения окна, а не его ликвидации.
Именно поэтому Sentinel не стоит путать с консенсус-протоколом. Sentinel — это механизм обнаружения отказа и координации переключения, а не Raft: он не реплицирует журнал решений и не даёт линеаризуемости записей. Кворум в Sentinel определяет, сколько наблюдателей должны согласиться, что мастер мёртв, — но не защищает от того, что мастер жив и продолжает принимать записи, пока его считают мёртвым. Обзор семейства алгоритмов согласования — в «Ландшафте консенсуса»; здесь важно их отсутствие и его последствия, которые ниже измерены.
Cluster: hash slots и resharding под нагрузкой
Redis Cluster шардирует ключи по 16384 hash slots, распределённым между мастерами. Клиент, обратившийся не к тому узлу, получает MOVED с адресом правильного — и переподключается. Во время миграции слота возможен ASK: ключ уже частично переехал, и запрос надо адресовать точечно.
Одно ограничение стоит знать до переезда, а не после: в Cluster логических баз нет — поддерживается только DB 0, а команда SELECT в кластерном режиме запрещена. Приложение должно исходить из DB 0 и разделять данные префиксами ключей; то, что разложило сервисы по номерам баз на standalone-инстансе, в Cluster не поедет без переделки схемы ключей. Почему номера баз вообще не стоит считать изоляцией и что использовать вместо них — в финальной статье серии.
Проверка на живом кластере: redis-cli --cluster reshard переносит 2000 слотов, одновременно клиент пишет 100000 ключей через ClusterClient.
| Метрика | Redis 8.8 | Valkey 8.1 | Сравнимо между образами? |
|---|---|---|---|
Время --cluster reshard (2000 слотов) |
27274 мс | 4973 мс | ❌ нет |
| Запись 100000 ключей | 40.811 с (2450.3 ops/s) | 22.109 с (4523.1 ops/s) | ❌ нет |
MOVED-редиректов |
46 | 14 | ❌ нет |
ASK-редиректов |
0 | 0 | — |
| Ошибок записи, дошедших до клиента | 0 | 0 | ✅ да |
Верификация GET каждого из 100000 |
missing=0 wrong=0 |
missing=0 wrong=0 |
✅ да |
Три верхние строки нельзя цитировать как сравнение образов — ни «Valkey решардит в 5 раз быстрее», ни «Valkey пишет в 1.8 раза быстрее». Причина у всех трёх одна: reshard и запись идут одновременно и мешают друг другу. Чем дольше длится reshard, тем больше данных успевает налиться в переезжающие слоты и тем дольше клиент платит за миграцию своими записями. Это одна связка, а не три независимых измерения: у Redis reshard (27.3 с) перекрыл около двух третей окна записи (40.8 с), у Valkey (5.0 с) — примерно четверть (22.1 с). Отсюда и разница в MOVED. Что здесь причина, а что следствие, этот стенд развязать не может — для честного сравнения нужен другой стенд, где reshard и запись разведены.
Что цитировать можно: resharding под нагрузкой не теряет записи на обоих образах. Все 100000 ключей прочитаны обратно с верным значением, ошибок до клиента не дошло. Клиент видит реальные MOVED (46 и 14 за прогон) и переподключается сам — приложение ничего не замечает.
Кстати про ASK: поймать его почти не удалось. За все прогоны он встретился однажды в отладочном запуске, а в финальных прогонах ASK=0 на обоих образах. Он живёт только в узком окне, пока конкретный ключ мигрирует, и попасть в него случайной записью маловероятно.
Открытая точка. В одном прогоне redis-cli --cluster reshard на Valkey свалился на ~196-м слоте из 2000 с ошибкой NOREPLICAS Not enough good replicas to write — при том, что min-replicas-to-write=0, то есть классический триггер этой ошибки выключен. Почему она возникла, установить не удалось: воспроизвести по требованию не получилось, повторные resharding и полный повторный прогон матрицы прошли успешно. Фиксирую как наблюдавшийся, но не воспроизводимый сбой с неизвестным механизмом. В единственном redis-прогоне такого не было — но это n=1, то есть отсутствие свидетельства, а не свидетельство отсутствия.
Cluster failover: сервер переключился, а клиент — нет
docker kill -s SIGKILL мастера во время плотной записи, три повтора на образ. Кил подтверждён во всех шести прогонах (ExitCode=137), промоушен — на другом живом мастере через cluster nodes, где строка реплики меняет роль со slave на master.
Серверный промоушен — от кила до появления master в топологии:
| Прогон | Redis 8.8 | Valkey 8.1 |
|---|---|---|
| #1 | 18496 мс | 20880 мс |
| #2 | 22933 мс | 18855 мс |
| #3 | 22962 мс | 20710 мс |
| спред | 18.5–23.0 с | 18.9–20.9 с |
Клиентское окно недоступности — от первой деградации до восстановления записи:
| Прогон | Redis 8.8 | Valkey 8.1 |
|---|---|---|
| #1 | 41.656 с | 45.597 с |
| #2 | 46.612 с | 44.760 с |
| #3 | 48.582 с | 46.677 с |
| спред | 41.7–48.6 с | 44.8–46.7 с |
Главный результат стенда — и он не про Redis, а про клиента. Клиентское окно вдвое с лишним длиннее серверного промоушена. Кластер уже выбрал нового мастера и раздаёт корректную топологию, а приложение всё ещё не может писать. Причина видна в логах клиента: пул соединений ненастроенного клиента продолжает долбиться в IP мёртвого узла, и один вызов Set() висит 41.5–48.4 секунды, прежде чем вернуть ошибку и заставить клиент перечитать топологию.
Но самое ценное — вот эта строка из лога прогона:
cluster-failover-writes: итог — attempts=279597 success=279596 failed=1 stalled=0 maxLatency=41.503994733sОбратите внимание на failed=1. Одна-единственная ошибка за весь прогон — и она же 41.5 секунды простоя. Отсюда практический вывод: мониторинг, считающий долю ошибочных запросов, такой инцидент не покажет. Одна ошибка из 279597 — это 0.0004%, любой разумный порог по error rate останется зелёным, пока приложение сорок с лишним секунд не может писать. Смотреть надо на латентность хвоста.
Чего здесь не происходило — важно, потому что легко приписать лишнее. В Cluster-сценарии длинный вызов всё-таки возвращал ошибку: failed=1 stalled=0 во всех шести прогонах. То есть детектор «по факту ошибки» этот инцидент бы заметил; не заметил бы его мониторинг по доле ошибок. Это разные вещи. Случай, когда клиент вообще не отдаёт ошибку и возвращает OK после многосекундной блокировки, на этом стенде поймался в Sentinel-сценарии, а не в Cluster: failed=0 stalled=1 maxLatency=10.641790839s. Утверждение «go-redis возвращает OK после 45-секундного Cluster-failover» логи этого стенда опровергают.
Redis и Valkey здесь неразличимы: спреды перекрываются, разница внутри разброса между повторами одного образа.
Sentinel: failover при down-after-milliseconds=5000 — и TILT, который его срывает
docker kill -s SIGKILL мастера, три повтора на образ. Промоушен опрашивается с хоста через SENTINEL get-master-addr-by-name — независимо от Go-клиента.
| Прогон | Redis 8.8: промоушен | Redis 8.8: окно клиента | Valkey 8.1 |
|---|---|---|---|
| #1 | 6983 мс | 10.642 с | ❌ TILT, промоушена нет за 80 с |
| #2 | ❌ TILT, промоушена нет за 80 с | — | ❌ TILT, промоушена нет за 80 с |
| #3 | 7294 мс | 10.644 с | ❌ TILT, промоушена нет за 80 с |
Где промоушен состоялся, картина устойчивая: Sentinel промоутит за ~7 секунд (при down-after-milliseconds=5000 это ~5 с на признание мастера мёртвым плюс ~2 с на выборы), клиент восстанавливается за ~10.6 с. Разрыв между «Sentinel уже знает нового мастера» и «клиент снова пишет» — 4.3–4.9 секунды.
Контраст с Cluster заметный: 10.6 секунды против 42–49. Но это не «Sentinel быстрее Cluster вообще»: у Sentinel down-after-milliseconds снижен до 5000, у Cluster cluster-node-timeout оставлен дефолтным 15000, плюс FailoverClient узнаёт новый адрес от Sentinel явно, а ClusterClient вынужден сначала упереться в таймаут пула.
TILT — самая важная находка стенда. В части прогонов Sentinel вообще не делал failover. Причина зафиксирована инструментально:
диагностика T+11602ms: sentinel_tilt:1 master0:name=mymaster,status=ok,address=172.28.0.10:6379,slaves=2,sentinels=3
флаги мастера по sentinel-1: master
диагностика T+45159ms: sentinel_tilt:1 master0:name=mymaster,status=odown,address=172.28.0.10:6379,slaves=2,sentinels=3Мастер физически мёртв, а Sentinel держит status=ok, флаг master и не помечает его упавшим. Это задокументированное поведение: TILT — защитный режим, в который Sentinel входит, если задержка итерации его событийного цикла превысила две секунды (или часы прыгнули назад). В TILT Sentinel продолжает собирать информацию, но перестаёт действовать, потому что не доверяет собственным измерениям времени. На перегруженном виртуализованном хосте событие «цикл задержался больше чем на 2 с» происходит регулярно.
Про 0/3 у Valkey против 2/3 у Redis: это не сравнение движков. TILT — свойство среды, а матрицы Redis и Valkey гонялись в разное время при разной загрузке хоста (valkey-матрица шла позже, когда хост был нагружен сильнее). Утверждать «у Valkey Sentinel хуже» на этих данных нельзя. Честная формулировка: на этом хосте TILT сорвал 1 из 3 прогонов Redis и 3 из 3 прогонов Valkey, механизм в обоих случаях один и тот же.
Практический вывод, который здесь действительно есть: TILT — не экзотика. Если Sentinel живёт на шумном или переподписанном по CPU хосте, в виртуалке со скачущими часами, он может молча не выполнить failover — при живом кворуме, корректном конфиге и полностью мёртвом мастере. Проверяется одной командой:
redis-cli -p 26379 info sentinel | grep tiltОтдельный нюанс для мониторинга: у Valkey в INFO sentinel есть sentinel_tilt:, но нет sentinel_total_tilt: (у Redis 8.8 есть оба). Если ваш мониторинг считает накопленное число входов в TILT — на Valkey этой метрики не будет.
Мониторить по IP или по DNS-имени
Первая версия конфигурации мониторила мастер по имени (sentinel monitor mymaster redis-master 6379 + resolve-hostnames yes), и failover не случился ни разу. После перехода на статические IP заработал. Соблазн склеить это в красивую причинно-следственную цепочку большой, поэтому разложим, что факт, а что домысел.
Измерено: DNS Docker после убийства контейнера отдаёт по его имени ошибки и таймауты (видно в логе Go-клиента); конфигурация с DNS дала 0 промоушенов из 2 попыток, конфигурация с IP — 2 из 3.
Не измерено: что резолв имени задерживал событийный цикл Sentinel. Таймауты DNS видны со стороны клиента, а не Sentinel; инструмента, который показал бы «Sentinel потратил N мс на resolve внутри итерации», нет, и сам Sentinel такой метрики не отдаёт. Механизм «имя → синхронный резолв → задержка итерации >2 с → TILT» правдоподобен, но прямых доказательств нет.
Что мешает считать DNS единственной причиной: TILT возникает и на статических IP — 1/3 прогонов Redis и 3/3 Valkey, где DNS в пути мониторинга нет вообще. Значит DNS в лучшем случае усугубляющий фактор, спутанный с загрузкой хоста, а не корень. Выборки крошечные, обе конфигурации гонялись в разное время.
Честный вывод: мониторить мастер по IP, а не по DNS-имени — дешёвая и разумная предосторожность, за которой стоит наблюдаемая корреляция и правдоподобный механизм. Но это не доказанная причинность, и «DNS ломает Sentinel» из этих данных не следует.
Split-brain: 75 подтверждённых записей, которых клиенты не видят
Механика партиции: мастер подключён к двум сетям — одна с Sentinel и репликами, другая только с изолированным писателем. Отключение первой сети оставляет писателя наедине с мастером: Sentinel и реплики мастер теряют, а писатель продолжает его видеть и писать в него.
На Redis 8.8 split-brain воспроизвести не удалось — и причина измерена, а не додумана. Sentinel всё время партиции сидел в TILT и не промоутил реплику. Второго мастера не возникло, расходиться было нечему: изолированный писатель сделал 80 из 80 записей успешно, промоушен не произошёл за 60 секунд партиции, после воссоединения старый мастер остался мастером со всеми 80 ключами. Это честный отрицательный результат: при заблокированном TILT-ом Sentinel split-brain не возникает — цена в другом, failover не происходит вообще.
Обе половины этой картины — один и тот же TILT, а не свойство движков. Split-brain не возник на Redis не потому, что Redis к нему устойчив, а потому, что failover сорвался первым. Приписывать split-brain движку Valkey эти данные не позволяют: там просто не помешал TILT, и сценарий отработал так, как задуман. Это два симптома одной причины — перегруженного виртуализованного хоста, — а не две независимые находки про два движка.
На Valkey 8.1 split-brain воспроизвёлся полностью. Здесь TILT не помешал, и получилось ровно то, ради чего писался сценарий: два мастера одновременно.
| Событие | Значение |
|---|---|
| Партиция | T+0 |
| Sentinel промоутит новую реплику | T+8663 мс |
| Старый мастер (изолированный) | продолжает считать себя master и принимать записи |
| Изолированный писатель | 80 из 80 SET подтверждены |
| После воссоединения: ключей на старом мастере | 80 |
| После воссоединения: ключей на новом мастере | 5 |
Вот цена split-brain, измеренная. Из 80 записей, которые изолированный мастер подтвердил клиенту ответом OK, на новом мастере — том, за которым идут все клиенты, спрашивающие адрес у Sentinel, — оказалось 5. Остальные 75 подтверждённых записей этим клиентам не видны. Уцелевшие 5 — те, что писатель успел сделать до того, как партиция вступила в силу, и они успели дореплицироваться.
Формулировка «75 записей потеряны» была бы неточной, и это важно. На момент конца прогона эти 75 записей физически существуют: они лежат на старом мастере, который всё ещё отвечает и всё ещё сообщает role:master. За 60 секунд после воссоединения он не стал репликой. То есть наблюдалось расхождение двух живых узлов, которое не сошлось за окно наблюдения, а не уничтожение данных: полного цикла «демоушен → ресинк → затирание расходящейся истории» этот прогон не показал. Точная формулировка — 75 записей не видны клиентам, идущим за Sentinel. Что стало бы с ними после демоушена, стенд не измерял: ожидаемо их бы затёрло, но это ожидание, а не результат. Почему демоушен не случился за 60 секунд — не установлено; вероятный кандидат тот же TILT, но инструментально это не подтверждено.
Одну ложную находку отсюда пришлось выбросить, и об этом стоит сказать. В первом прогоне воссоединение делалось без фиксации адреса — docker выдавал вернувшемуся контейнеру новый IP, а Sentinel мониторит старый. Старый мастер возвращался «не туда», Sentinel его не находил и в принципе не мог демоутить. Результат выглядел как содержательный вывод про поведение Redis («старый мастер не демоутится, данные остаются»), но был дефектом стенда: настоящая сетевая партиция адрес узла не меняет. Числа выше — из прогона с фиксированным адресом.
И про механизм партиции: docker network disconnect выдёргивает интерфейс целиком. Реальная партиция может выглядеть иначе — TCP-соединения не рвутся мгновенно, а висят до таймаута; бывает односторонняя связность. Не экстраполировать эти числа на «любую сетевую аварию».
Где здесь граница durability
Предыдущая статья закончилась тем, что локальная персистентность защищает только от рестарта процесса, но не от отказа узла, — и обещала, что реплики эту границу сдвигают. Сдвигают, но вот куда именно.
Репликация асинхронна: подтверждённая запись может не доехать до реплики. WAIT сужает окно, но не закрывает его и не даёт консенсуса. Sentinel обнаруживает отказ и переключает мастера — но может уйти в TILT и не сделать этого молча. При разделении сети возможны два мастера одновременно, и цена этому измерена: 75 подтверждённых записей, не видных клиентам, идущим за Sentinel.
То есть добавление реплик не отменяет границу durability, а перемещает её: вместо «переживёт ли запись рестарт процесса» вопрос становится «переживёт ли запись потерю мастера и переключение». Ответ по-прежнему «не гарантированно».
Для Redis-как-кэша это ничего не меняет: потеря приемлема, а Cluster и Sentinel решают задачу доступности и масштаба, с которой справляются хорошо. Для Redis-как-источника-истины это означает, что репликация — не заменитель настоящей durability, а ещё одна вероятностная надстройка над ней. Если каждая подтверждённая запись обязана пережить любой отказ — это требование к системе с консенсусом в фундаменте, а не к Redis с Sentinel сверху.
Что дальше
Следующая граница — память: что происходит на maxmemory, когда данные некуда класть, и как Redis выбирает, кем пожертвовать. Дальше — streams и Lua, где однопоточность из второй статьи превращается в практическую атомарность. Завершает серию эксплуатация и карта выбора, где эти находки собираются в набор типовых инцидентов — и где, в частности, показано, как мониторить то, что доля ошибок в 1 из 279597 не покажет.
Поведение клиентских библиотек при недоступности сервера — паттерны переподключения, повторы, работа с failover — тема отдельной статьи «Redis: клиенты на Go, Java, Rust». Обзор алгоритмов согласования, которых у Sentinel нет, — в «Ландшафте консенсуса».
Про Redis и Valkey в этом разделе честно сказать нечего: единственные различия, которые видны в числах, объясняются загрузкой хоста и разным временем прогона, а не движками. Контекст форка — в отдельной статье про ValkeyСкоро.
Источники
- Официальная документация: redis.io, раздел redis.io/docs.
- Репликация: redis.io/docs — Replication.
- Cluster: redis.io/docs — масштабирование, спецификация Cluster.
- Sentinel (включая режим TILT): redis.io/docs — Sentinel.
- Смежное на сайте: «Ландшафт консенсуса» (алгоритмы согласования — то, чем Sentinel не является), «KV и документные: транзакций почти нет», персистентность Redis (предыдущая граница durability).
- Стенд: digital-cookbook/databases/redis/deep-dive (
redis:8.8,valkey/valkey:8.1, модульtopology; оркестрацияops/topology-demo.sh).
Комментарии