Два способа собрать Valkey из нескольких узлов: кластер и Sentinel
Разница между ними не в отказоустойчивости — она есть у обоих. Различают их четыре вещи разом: как разложены данные, кто принимает решение о переключении, кто отвечает клиенту на вопрос «куда слать команду» и что для этого должен уметь сам клиент. Плюс пятое измерение, о котором забывают чаще всего: версия. Правило «в кластере база ровно одна» верно для Redis и Valkey до 9.0 и неверно для Valkey 9 — и здесь это перемерено на двух кластерах, отличающихся одной строкой конфига.
Полное техническое изложение
TL;DR
- Это два разных ответа на один вопрос: куда клиенту слать команду. В нативном кластере отвечает сам узел данных, под Sentinel — отдельная служба.
- Кластер шардирует, Sentinel — нет. Ключ → один из 16 384 слотов → узел. У Sentinel данные целиком на одном мастере, реплики только для замены.
- Шардирование меняет набор команд:
MSET foo 1 bar 2даётCROSSSLOT, потому что ключи легли в разные слоты. - А про базы расхожее знание устарело. «В кластере база одна» — это
правило Redis и Valkey до 9.0. В Valkey 9 базы в кластере есть, их число
задаёт
cluster-databases, по умолчанию 1. - При отказе клиент видит разное. В кластере — осмысленный ответ и новый адрес от любого живого узла. Под sentinel — молчание старого адреса.
- Самое опасное место Sentinel: вернувшийся старый мастер десять секунд
отвечал
OKна записи, и потом они исчезли. Ошибки клиент не видел.
Кто знает, куда слать команду
Вся разница выводится отсюда. В кластере карту знает каждый узел данных: придёте не по адресу — он не откажет, а назовёт нужный. Под Sentinel узел данных про топологию не знает вовсе, и спросить надо отдельную службу.
узел кластера: SET foo 1 -> MOVED 12182 127.0.0.1:48711
узел под sentinel: CLUSTER INFO -> ERR This instance has cluster support disabled
sentinel: SENTINEL get-master-addr-by-name cache -> 127.0.0.1:41979
Ключ, слот, узел
Адресация в два шага. Первый — арифметика: HASH_SLOT = CRC16(key) mod 16384.
Второй — состояние кластера: слот кому-то принадлежит. Поэтому переезд данных
не меняет ни одного ключа, он меняет владельца слота.
Фигурные скобки в имени ключа — не украшение: хешируется только то, что внутри
них. {u}:a и {u}:b попадают в один слот и оказываются на одном узле.
Что ломается в кластере
| команда | в кластере | под sentinel |
|---|---|---|
MSET foo 1 bar 2 | CROSSSLOT Keys in request don't hash to the same slot | OK |
MSET {u}:a 1 {u}:b 2 | работает: общая скобка | OK |
SWAPDB 0 1 | ERR SWAPDB is not allowed in cluster mode | OK |
Отказ не про «две записи сразу», а про два разных слота: команда выполняется целиком на одном узле. Набор ключей, которые придётся трогать одной командой, в кластере решается до первой записи.
Нумерованные базы: тут решает версия
Проверить «в кластере база ровно одна» проще всего опытом — и он подтверждает
это ответом SELECT 1 → ERR DB index is out of range. Ответ настоящий,
вывод неверный: это правило Redis и Valkey до 9.0.
кластер А: cluster-databases 1 SELECT 1 -> ERR DB index is out of range
кластер Б: cluster-databases 16 SELECT 1 -> OK
Два кластера, одна строка разницы. Номер базы при этом в адресацию не входит: один и тот же ключ в базах 0 и 1 держит разные значения, но слот у него один и тот же — 12182.
Чего базы в кластере НЕ дают: SWAPDB остаётся запрещён, а DBSIZE, SCAN и
FLUSHDB видят только часть базы — ту, что лежит на узле, куда пришла команда.
Три ключа в одной базе разошлись по трём узлам, и каждый узел насчитал один.
Отказ мастера
Сравнивать длительности бессмысленно — они настраиваются. Сравнивать надо, что получил клиент.
в кластере, опрашивая живой узел:
t= 0.00 MOVED 12182 127.0.0.1:36355 (старый адрес слота)
t= 3.39 CLUSTERDOWN The cluster is down (слот сейчас не покрыт)
t= 3.49 MOVED 12182 127.0.0.1:39129 (новый адрес)
под sentinel, опрашивая старый адрес:
t= 0.00 Could not connect to Valkey at 127.0.0.1:44393
t= 2.30 sentinel называет мастером 127.0.0.1:41979
В кластере клиент всё время получал осмысленные ответы от живого узла. Под sentinel нового адреса ему не сказал никто — он обязан спросить сам.
Самое опасное место
Старый мастер вернулся — и поднялся мастером: он таким себя помнит.
t= 0.06 роль master, запись -> OK
t=10.10 роль slave, запись -> READONLY You can't write against a read only replica.
сразу после перевода: GET who old
после синхронизации: GET who (её нет)
Десять секунд записи принимались, потом исчезли при синхронизации с
настоящим мастером. Ошибки клиент не видел. В кластере такого окна нет:
вернувшийся узел сразу отдаёт MOVED на новый адрес.
Отсюда правило из спецификации клиента Sentinel: адрес надо переспрашивать при каждом переподключении, а не один раз при старте.
Что выбирать
не хватает памяти или пропускной способности одного узла → кластер
данные влезают в узел, нужна отказоустойчивость → Sentinel
код опирается на MSET по произвольным ключам → Sentinel проще
нужны нумерованные базы → Valkey 9+: оба
нужен SWAPDB → только Sentinel
клиент не умеет протокол кластера → кластер не возьмёте
клиент не умеет sentinel → Sentinel не возьмёте
Две последние строки важны: оба режима требуют своей поддержки в клиенте,
и умения эти независимы. Обычный клиент, не знающий про кластер, получив
MOVED, просто отдаст текст ошибки приложению — карту слотов он не ведёт.
Чем измерено
Всё снято на Valkey 9.1.2, собранном из исходников, на одном хосте.
Скрипты и записи прогонов — bench/valkey-cluster/.
Секунды не переносятся: узел считается упавшим через секунду молчания вместо тридцати по умолчанию. А дословные ответы сервера переносятся полностью — это поведение, а не измерение.
И ещё одно, что переносится хуже всего: версия. Нумерованные базы в кластере появились в 9.0, атомарный перенос слотов — в Valkey 9.0 и в Redis 8.4. Правила про кластер, взятые из знаний о Redis, надо помечать версией.
TL;DR
Слов «кластеризация Valkey» два способа, и выбирают между ними обычно по отказоустойчивости — хотя она есть у обоих. Выбирать надо по другому: кто отвечает клиенту на вопрос «куда слать команду».
- Нативный кластер отвечает адресом. Ключ отображается в один из 16 384
слотов, слот принадлежит узлу, и узел, если слот не его, возвращает не
данные, а
MOVED 12182 127.0.0.1:48711. Спросить можно любой узел: каждый знает карту целиком. - Sentinel — это не кластер, а наблюдатель. Данные лежат на обычном
мастере с репликами, шардирования нет вовсе. Узел данных про топологию не
знает ничего и на
CLUSTER INFOотвечаетERR This instance has cluster support disabled. Адрес мастера знает отдельная служба, и клиент обязан спросить её сам. - Шардирование меняет набор доступных команд. В кластере
MSET foo 1 bar 2— этоCROSSSLOT Keys in request don't hash to the same slot: команда выполняется целиком на одном узле, а ключи легли в разные слоты. Под sentinel узел один, и та же команда работает. - А вот про нумерованные базы расхожее знание устарело. «В кластере база
ровно одна» — правило Redis и Valkey до 9.0. В Valkey 9 базы в кластере
есть, их число задаёт
cluster-databases, и по умолчанию оно равно единице — поэтому старое правило и выглядит верным. Перемерено на двух кластерах, отличающихся одной строкой конфига. - И разное поведение при отказе. В кластере клиент получает от живого узла осмысленный ответ и новый адрес слота. Под sentinel старый адрес просто молчит, и нового адреса клиенту не скажет никто.
- Самое опасное место режима Sentinel — возвращение старого мастера.
Замерено: десять секунд он поднимается мастером и отвечает
OKна записи, а после перевода в реплику синхронизация их стирает. Ошибки клиент не видит ни одной. В кластере такого окна нет — проверено тем же прогоном. - Найдено по дороге: «кластер собран» и «кластер переживёт отказ» — разные состояния. Между ними 6,07 секунды, и мастер, упавший внутри этого окна, не заменяется НИКОГДА.
- Кворум и большинство — разные числа. Пять sentinel, кворум 2, живых
двое: кворум выполнен, переключения нет, в журнале
-failover-abort-not-elected. Спросить об этом заранее умеетSENTINEL CKQUORUM. - Версия — пятое измерение сравнения, и его забывают чаще прочих. Нумерованные базы в кластере появились в Valkey 9.0, атомарный перенос слотов — в Valkey 9.0 и в Redis 8.4. Правила, унаследованные из знаний о Redis Cluster, надо помечать версией или проверять.
Всё снято на Valkey 9.1.2, собранном из исходников; прогоны целиком лежат в
bench/valkey-cluster/runs/.
Один вопрос, на который два режима отвечают по-разному
Почти все сравнения кластера и Sentinel начинаются с таблицы «шардирование
есть / шардирования нет». Это правда, но это следствие, а не причина, и из
такой таблицы не выводится ни одно из практических отличий — ни почему
перестаёт работать MSET, ни почему клиенту нужна особая библиотека, ни
откуда берётся окно, в котором записи уходят в никуда.
Различают эти режимы четыре вещи разом, и путать их дорого:
РАСКЛАДКА ДАННЫХ кластер шардирует по 16 384 слотам; Sentinel — нет,
данные целиком на одном мастере
КТО РЕШАЕТ кластер — сами узлы, большинством мастеров;
Sentinel — отдельная служба, большинством наблюдателей
КТО АДРЕСУЕТ кластер — узел данных, обычной командой;
Sentinel — та же служба, отдельной командой
ЧТО УМЕЕТ КЛИЕНТ для каждого режима нужна своя поддержка в библиотеке,
и умения эти независимы
Из первой строки растёт набор доступных команд, из второй — поведение при отказе, из третьей — протокол подключения, из четвёртой — можно ли вообще взять этот режим с вашей библиотекой. Ниже они разобраны по очереди, и начать стоит с третьей: она короче всех объясняет, почему два режима вообще путают.
Итак, у клиента есть вопрос: куда отправить эту команду. Два режима отвечают на него разными сторонами.
| нативный кластер | Sentinel | |
|---|---|---|
| кого спрашивают | любой узел данных | отдельную службу |
| что отвечают | «не мой слот, иди туда: 127.0.0.1:48711» | «мастер сейчас 127.0.0.1:41979» |
| что делает узел данных | сам называет нужный адрес | выполняет команду, про топологию не знает |
| где живёт знание о топологии | в самих данных | рядом с данными, в другой службе |
Дальше — следствия, и каждое проверено прогоном, а не выведено рассуждением. Выводить их рассуждением, кстати, и не получится: раскладка данных и версия добавляют своё, и один только вопрос «кто отвечает адресом» всех отличий не объясняет.
Как ключ превращается в адрес
Адресация в кластере устроена в два шага, и оба стоит держать в голове, потому что путают именно их. Первый шаг — из ключа в слот, и он чистая арифметика:
HASH_SLOT = CRC16(key) mod 16384
HASH_SLOT = CRC16(key) mod 16384
Второй шаг — из слота в узел, и вот он уже не арифметика, а состояние кластера: слоты кому-то принадлежат, и принадлежность меняется. Отсюда важное свойство: переезд данных между узлами не меняет ни одного ключа. Меняется владелец слота, а формула остаётся той же.
Проверяется это одной командой, отправленной по очереди всем шести узлам.
Флага -c у клиента нет — он нарочно «глупый» и никуда не переходит, чтобы
было видно, что именно сказал сервер:
1. КЛАСТЕР ОТВЕЧАЕТ НЕ ДАННЫМИ, А АДРЕСОМ
----------------------------------------------------------------------
CLUSTER KEYSLOT foo 12182
узел 44997 (мастер): SET foo 1 MOVED 12182 127.0.0.1:48711
узел 53953 (мастер): SET foo 1 MOVED 12182 127.0.0.1:48711
узел 48711 (мастер): SET foo 1 OK
узел 39529 (реплика): SET foo 1 MOVED 12182 127.0.0.1:48711
узел 54583 (реплика): SET foo 1 MOVED 12182 127.0.0.1:48711
узел 51665 (реплика): SET foo 1 MOVED 12182 127.0.0.1:48711
MOVED — это ошибка протокола, ошибка перенаправления: команда не
выполнена, и узел вернул не данные, а адрес того, кто её выполнит. Сбоем
приложения она при этом обычно не становится — клиент кластера чинит на ней
свою карту слотов и повторяет команду по названному адресу. Заметьте, что
ответили одинаково и мастера, и реплики: карту знают все.
Разница между «не выполнена и перенаправлена» и «не выполнена и неизвестно
что» здесь принципиальна, и её стоит держать отдельно от повтора после
сетевого сбоя. На MOVED повтор безопасен: узел точно ничего не сделал. А
если оборвалась связь после отправки, клиент не знает, выполнилась команда или
нет, — и повтор там уже вопрос идемпотентности, а не маршрутизации.
Что при этом делает настоящий клиент
Предыдущий блок показывает протокол, но не показывает поведение: настоящая
библиотека MOVED прячет. Что она делает на самом деле, считают сами узлы —
по своему счётчику обработанных команд, с контрольным окном той же длины
вхолостую (узлы кластера всё время говорят друг с другом, и без контроля
клиентское от фонового не отличить).
Клиенту дали один адрес из шести, и не тот, где лежит ключ:
узел вхолостую с клиентом что пришло за второе окно
51665 (адрес клиенту) 2 7 client|setinfo×2 cluster|slots×1 hello×1 set×1
Один поход за картой слотов (cluster|slots) — и дальше запись ушла прямо
нужному узлу. Отсюда то, что обычно имеют в виду под «кластер сам
разбирается»: знать все узлы клиенту не нужно, достаточно одного живого;
посредника между клиентом и данными нет вообще.
Под sentinel порядок обратный, и это видно в том же замере: сначала вопрос службе, потом команда данным. Узел данных в поиске самого себя не участвовал и не мог бы:
2. В РЕЖИМЕ SENTINEL УЗЕЛ ДАННЫХ ПРО ТОПОЛОГИЮ НЕ ЗНАЕТ НИЧЕГО
----------------------------------------------------------------------
узел данных 43471: CLUSTER INFO ERR This instance has cluster support disabled
узел данных 43471: роль master
Здесь стоит сразу сузить формулировку, которая напрашивается и оказывается шире измеренного. «Узел про топологию не знает ничего» — неверно: свою пару репликации он знает вполне.
19. ЧТО УЗЕЛ ПОД SENTINEL ВСЁ-ТАКИ ЗНАЕТ О ТОПОЛОГИИ
----------------------------------------------------------------------
мастер про себя (INFO replication):
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=45047,state=online,offset=154,lag=0,type=replica
slave1:ip=127.0.0.1,port=52369,state=online,offset=154,lag=0,type=replica
реплика про себя:
role:slave
master_host:127.0.0.1
master_port:34351
master_link_status:up
Чего он не знает — так это авторитетного ответа на вопрос «кто мастер этой службы сейчас». Своё мнение у него есть, и в момент переключения оно бывает устаревшим; ровно это и произойдёт в разделе про вернувшийся адрес.
Спецификация клиента Sentinel описывает обязательную последовательность прямым текстом — и в ней три шага, а не один:
it should attempt a connection with the primary, and call the ROLE command in
order to verify the role of the instance is actually a primary.
Клиент должен попытаться подключиться к главному узлу и вызвать команду ROLE, чтобы убедиться, что роль экземпляра действительно главная.
Третий шаг — проверка ROLE — выглядит перестраховкой ровно до раздела про
вернувшийся адрес. Там видно, от чего именно она страхует.
И чего она не даёт: это проверка, а не доказательство. Если устаревшим
окажется и ответ службы, и мнение самого узла — а именно такая пара и
получается в первые секунды после переключения, — ROLE ответит master, и
клиент примет старый адрес за верный. Спецификация этот случай называет редким
и полагается на то, что sentinel позже разорвёт такие соединения; но
согласованности она не обещает.
Что в кластере ломается из привычного
Отсюда и растёт список команд, которые в кластере перестают работать. Список не произвольный: команда выполняется целиком на одном узле, значит команда, которой нужны два ключа из разных слотов, не может быть выполнена нигде.
5. ОДНА КОМАНДА НА ДВА КЛЮЧА: ГДЕ ПРОХОДИТ ГРАНИЦА
----------------------------------------------------------------------
CLUSTER KEYSLOT foo 12182
CLUSTER KEYSLOT bar 5061
CLUSTER KEYSLOT {u}:a 11826
CLUSTER KEYSLOT {u}:b 11826
MSET foo 1 bar 2
в кластере CROSSSLOT Keys in request don't hash to the same slot
под sentinel OK
MSET {u}:a 1 {u}:b 2
в кластере MOVED 11826 127.0.0.1:42549
под sentinel OK
Отказ не про «две записи сразу», а про два разных слота: те же две записи с общей фигурной скобкой проходят. Скобки — не украшение имени:
If the key contains a "{...}" pattern only the substring between `{` and `}` is hashed in order to obtain the hash slot.Если ключ содержит образец вида «{...}», для получения хеш-слота хешируется только подстрока между { и }.
Практическое следствие дороже всего обходится задним числом: в кластере набор ключей, которые понадобится трогать одной командой, решается до первой записи. Под sentinel такого решения принимать не надо вовсе.
Второй пункт списка — нумерованные базы, и вот тут расхожее знание даёт неверный ответ. Заблуждение поучительное, поэтому разобрано оно не строчкой, а отдельным разделом ниже.
Нумерованные базы: здесь ответ зависит от версии Valkey
Проверить «в кластере база одна» проще всего одним опытом:
6. НУМЕРОВАННЫЕ БАЗЫ: В ОДНОМ РЕЖИМЕ ЕСТЬ, В ДРУГОМ НЕТ
----------------------------------------------------------------------
SELECT 1
в кластере ERR DB index is out of range
под sentinel OK
SWAPDB 0 1
в кластере ERR SWAPDB is not allowed in cluster mode
под sentinel OK
— и делала из него вывод «в кластере база ровно одна». Ответ сервера настоящий, вывод неверный. Правильный вывод из одного этого опыта только такой: в ЭТОМ кластере база настроена одна. Отличить одно от другого можно было лишь вторым опытом, и его не было.
Потому что в Valkey 9.0 нумерованные базы в кластере появились:
Valkey 9.0 adds the ability to have numbered databases on a cluster, changing
everything about that advice.
Valkey 9.0 добавляет возможность иметь нумерованные базы в кластере, что полностью меняет прежний совет.
Число баз задаёт отдельная настройка, cluster-databases, и по умолчанию она
равна единице — поэтому кластер, поднятый обычным способом, ведёт себя
по-старому. Два кластера, отличающиеся ровно этой строкой:
14. СКОЛЬКО БАЗ В КЛАСТЕРЕ, РЕШАЕТ НАСТРОЙКА, А НЕ РЕЖИМ
----------------------------------------------------------------------
кластер А: cluster-databases 1
кластер Б: cluster-databases 16
кластер А (по умолчанию): SELECT 0 OK
кластер А (по умолчанию): SELECT 1 ERR DB index is out of range
кластер А (по умолчанию): SELECT 15 ERR DB index is out of range
кластер А (по умолчанию): SELECT 16 ERR DB index is out of range
кластер Б (16 баз): SELECT 0 OK
кластер Б (16 баз): SELECT 1 OK
кластер Б (16 баз): SELECT 15 OK
кластер Б (16 баз): SELECT 16 ERR DB index is out of range
Одна строка конфига — и SELECT 1 из отказа превращается в OK. Настройка
неизменяемая (IMMUTABLE_CONFIG в src/config.c), то есть выбирается при
запуске узла и на лету не меняется.
Номер базы не участвует в адресации
Это второй вопрос, и он интереснее первого: не ломает ли новая возможность двухшаговую адресацию, на которой держится весь кластер. Не ломает.
15. НОМЕР БАЗЫ НЕ УЧАСТВУЕТ В ВЫЧИСЛЕНИИ СЛОТА
----------------------------------------------------------------------
узел-владелец слота ключа foo 127.0.0.1:51799
в базе 0: SET foo db0 OK
в базе 1: SET foo db1 OK
в базе 0: GET foo db0
в базе 1: GET foo db1
в базе 0: CLUSTER KEYSLOT foo 12182
в базе 1: CLUSTER KEYSLOT foo 12182
Один ключ, два значения, один слот. База добавляется третьим измерением уже внутри узла, а раскладка данных по узлам от неё не зависит ни на ключ.
И чего это НЕ даёт
Из «базы появились» так же легко сделать обратный миф — будто в кластере они теперь как в одиночном узле. Нет:
16. ЧТО НЕ ИЗМЕНИЛОСЬ: ЗАПРЕТЫ И ГРАНИЦА ВИДИМОСТИ
----------------------------------------------------------------------
SWAPDB 0 1 ERR SWAPDB is not allowed in cluster mode
в базе 1 записаны три ключа, разошедшиеся по трём узлам:
CLUSTER KEYSLOT foo 12182
CLUSTER KEYSLOT bar 5061
CLUSTER KEYSLOT user:1 10778
что отвечает DBSIZE в базе 1 на каждом узле:
узел 37191 1
узел 50991 1
узел 51799 1
SCAN в базе 1 на узле-владельце 0 foo
SCAN в базе 1 на узле 37191 0 bar
SWAPDB как был запрещён, так и остался. А DBSIZE и SCAN считают не базу
целиком, а её часть на том узле, куда пришла команда: три ключа записаны в
одну базу, разошлись по трём узлам, и ни один узел не видит всех трёх. То же
касается FLUSHDB. Документация называет это прямо:
will return the number of keys in the current database on the connected node
вернёт число ключей в текущей базе на том узле, к которому вы подключены
Оттуда же — про то, за чем нумерованные базы брать не стоит:
numbered databases do not provide any form of resource isolation
нумерованные базы не дают никакой формы изоляции ресурсов
Итог: нумерованная база в кластере — это пространство имён, а не отдельное хранилище. Разнести по ним окружения можно; ждать от них изоляции или общего на кластер представления о содержимом — нельзя.
Отсюда правило, которое стоит унести из статьи целиком
Ошибка здесь не в невнимательности. Она в том, что знание про кластер берётся оттуда, откуда его берут все: из общей памяти про Redis Cluster, где база действительно одна. На Valkey 9 это правило перестало быть верным, а выглядит по-прежнему верным — потому что значение по умолчанию его подтверждает.
| возможность | Valkey ≤ 8.x | Valkey 9.0 | Valkey 9.1 |
|---|---|---|---|
| база 0 в кластере | да | да | да |
| несколько баз в кластере | нет | да | да |
cluster-databases | нет | да, по умолчанию 1 | да, по умолчанию 1 |
SELECT n в кластере | только 0 | до cluster-databases − 1 | до cluster-databases − 1 |
SWAPDB в кластере | нет | нет | нет |
Правило простое: любое утверждение про кластер, унаследованное из знаний о Redis, надо помечать версией — или проверять. Проверка здесь стоила одной строки конфига и двух минут.
Отказ мастера: одно событие, два разных клиентских опыта
Здесь и лежит главное практическое отличие, и оно не в секундах. Сравнивать длительности бессмысленно: обе настраиваются, и в замере обе намеренно укорочены, чтобы прогон укладывался в минуты. Сравнивать надо текст, который получает клиент.
В кластере опрашивался живой узел — не тот, что упал:
8. ОТКАЗ МАСТЕРА В КЛАСТЕРЕ ГЛАЗАМИ КЛИЕНТА
----------------------------------------------------------------------
слот ключа принадлежит 127.0.0.1:36355
опрашивается живой узел 127.0.0.1:47039
cluster-node-timeout 2000 мс
t=0.00 узел 36355 убит
t= 0.00 MOVED 12182 127.0.0.1:36355
t= 3.39 CLUSTERDOWN The cluster is down
t= 3.49 MOVED 12182 127.0.0.1:39129
слот перешёл к 127.0.0.1:39129
вернувшийся узел: его роль slave
вернувшийся узел: SET foo MOVED 12182 127.0.0.1:39129
Три разных ответа, и все три осмысленные: старый адрес слота, честное «слот сейчас не покрыт никем», новый адрес. Клиенту не пришлось гадать ни секунды — узел, до которого он дозвонился, знает состояние всего кластера, потому что узлы непрерывно обмениваются им по шине. И никто не спрашивал внешнюю службу: реплику повысили сами узлы, большинством голосов мастеров.
Под sentinel то же событие выглядит иначе:
9. ОТКАЗ МАСТЕРА ПОД SENTINEL ГЛАЗАМИ КЛИЕНТА
----------------------------------------------------------------------
мастер по мнению sentinel 127.0.0.1:44393
down-after-milliseconds 1000 мс
t=0.00 узел 44393 убит
t= 0.00 старый адрес: Could not connect to Valkey at 127.0.0.1:44393
t= 2.30 sentinel называет мастером 127.0.0.1:41979
новый мастер отвечает на запись OK
вторая реплика 49651 READONLY You can't write against a read only replica.
Старый адрес не ответил ничего — там никого нет. Ни один узел данных не назвал клиенту нового адреса и не мог: узлы про свою пару не знают, а служба, которая знает, клиенту сама не звонит. Отсюда и правило из спецификации клиента:
every time a reconnection is needed, the client should resolve again the
address using Sentinels restarting from Step 1.
Каждый раз, когда требуется переподключение, клиент должен заново определить адрес через Sentinel, начиная с шага 1.
Не «желательно», а каждый раз. Почему — в следующем разделе.
Старый мастер вернулся
Это самое опасное место режима Sentinel, и оно не про отказ, а про восстановление. Убитый мастер поднимается обратно — и поднимается мастером: он таким себя помнит, и ничего другого ему пока никто не сказал.
10. СТАРЫЙ МАСТЕР ВЕРНУЛСЯ: САМОЕ ОПАСНОЕ МЕСТО РЕЖИМА SENTINEL
----------------------------------------------------------------------
мастер сейчас 127.0.0.1:41979
t=0.00 узел 44393 поднят обратно
t= 0.06 роль master, запись -> OK
t=10.10 роль slave, запись -> READONLY You can't write against a read only replica.
переведён в реплику через 10.10 с
сразу после перевода: GET who old
после синхронизации: GET who (её нет)
Десять секунд узел принимал записи и отвечал OK. Две последние строки
читаются вместе и именно в таком порядке: сразу после перевода запись ещё на
месте, а после синхронизации ключа нет — данные заменены копией настоящего
мастера. Клиент получил OK, и запись исчезла, не увидев ни одной ошибки.
В этом опыте в кластере такого окна не возникло, и это проверено тем же
прогоном — две последние строки блока 8. Вернувшийся узел узнал из шины, что
его слоты уже у другого, ещё до первой команды: роль slave, ответ MOVED на
новый адрес.
Слово «в этом опыте» здесь не оговорка вежливости. Проверен был один сценарий — убили и подняли обратно; узел при старте прочитал шину и получил свежую конфигурацию раньше, чем первую команду клиента. Сценарий разделения сети спецификация кластера разбирает отдельно, и там ограниченная потеря записей допускается: клиент с устаревшей картой пишет на бывшего мастера, который ещё не узнал о своей отставке. Мы этого не мерили, и утверждать, что такого окна в кластере не бывает, статья не будет. Измерено другое: в кластере оно закрывается само, а под sentinel — только действиями клиента.
Вот от чего страхует проверка ROLE из спецификации клиента. Клиент, который
держит адрес данных напрямую и переспрашивать sentinel не умеет, в это окно
попадает целиком.
Найдено по дороге: готов к работе ≠ готов к отказу
Этот раздел появился из ошибки в замере. Скрипт поднимал кластер, дожидался
cluster_state:ok у всех узлов и ронял мастера. Реплика не повышалась — ни
через двадцать секунд, ни через минуту, никогда.
Оказалось, что это не поломка замера:
11. СРАЗУ ПОСЛЕ СБОРКИ КЛАСТЕР РАБОТАЕТ, НО ОТКАЗ НЕ ПЕРЕЖИВЁТ
----------------------------------------------------------------------
cluster-node-timeout 2000 мс
cluster-replica-validity-factor 10
repl-ping-replica-period 10
как расходятся два признака готовности:
t= 0.01 cluster_state:ok у всех узлов (слоты покрыты, запросы обслуживаются)
t= 6.08 master_link_status:up у всех реплик (есть кому повышаться)
окно между ними 6.07 с
Внутри этих четырёх секунд кластер выглядит полностью рабочим: слоты покрыты, ключи пишутся и читаются. Но у реплик ещё нет ни одной завершённой синхронизации — и мастер, упавший здесь, не заменяется никогда:
реплика повысилась НЕТ, за 40 с
что реплика записала в журнал:
Currently unable to failover: Disconnected from primary for
longer than allowed. Please check the
'cluster-replica-validity-factor' configuration option.
Почему «никогда», а не «пока не истечёт таймаут», видно в исходнике:
if (server.repl_state == REPL_STATE_CONNECTED) {
data_age = (mstime_t)(server.unixtime - server.primary->last_interaction) * 1000;
} else {
data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000;
}У реплики, ни разу не подключавшейся к мастеру, repl_down_since равен нулю.
Возраст её данных получается отсчитанным от начала эпохи Unix, и никакой
допуск такого возраста не покрывает.
Слово «никогда» здесь надо сузить, и сужается оно замером, а не оговоркой.
Проверка свежести данных реплики управляется настройкой
cluster-replica-validity-factor; в опыте выше у неё значение по умолчанию.
Тот же опыт с нулём:
17. ТО ЖЕ ОКНО, НО С ОТКЛЮЧЁННОЙ ПРОВЕРКОЙ СВЕЖЕСТИ
----------------------------------------------------------------------
cluster-replica-validity-factor 0
cluster-node-timeout 2000 мс
t=0.00 мастер 34455 убит, ждём переключения 40 с
t= 0.00 состояние кластера ok, роль реплики slave
t= 3.53 состояние кластера fail, роль реплики slave
t= 4.15 состояние кластера fail, роль реплики master
реплика повысилась да
Одна строка конфига — и «никогда» превращается в четыре секунды до повышения. Значит, правильная формулировка такая: при значении по умолчанию реплика, не завершившая ни одной синхронизации, не повышается никогда; настройкой это отключается. Совет так делать отсюда не следует: проверка стоит там не зря, и реплика, ни разу не синхронизировавшаяся, повышается ПУСТОЙ — то есть такое переключение лечит доступность ценой данных.
Вывод же не про Valkey, а про эксплуатацию: «кластер поднялся» и «кластер
держит отказ» — разные проверки. Первая отвечает, обслуживаются ли запросы;
вторая требует смотреть на master_link_status каждой реплики. Автоматика,
которая раскатывает кластер и сразу гасит старый, первую пройдёт, а вторую —
нет.
Кворум и большинство — разные числа
В настройке sentinel monitor <имя> <адрес> <порт> <кворум> последнее число
читают как «сколько sentinel достаточно, чтобы переключить». Это неверно, и
документация говорит прямо:
The quorum is only used to detect the failure. In order to actually perform a
failover, one of the Sentinels need to be elected leader... and be authorized to
proceed. This only happens with the vote of the majority of the Sentinel
processes.
Кворум используется только для обнаружения отказа. Чтобы переключение действительно выполнить, один из Sentinel должен быть избран ведущим и получить право действовать. Это происходит только при голосовании большинства процессов Sentinel.
Числа подобраны так, чтобы одно условие выполнялось, а второе нет: пять sentinel, кворум 2, погашены три. Кворум выполнен — и переключения нет:
13. КВОРУМ ОТКРЫВАЕТ ПЕРЕКЛЮЧЕНИЕ, А ВЫПОЛНЯЕТ ЕГО БОЛЬШИНСТВО
----------------------------------------------------------------------
sentinel в наборе 5
кворум в настройке 2
большинство от пяти 3 (нигде не настраивается)
мастер 127.0.0.1:47213
осталось живых sentinel 2 из 5
что оставшийся считает мастером 127.0.0.1:47213
t=0.00 мастер 47213 убит
за 30 с переключения НЕ произошло
кого оставшийся всё ещё зовёт мастером 127.0.0.1:47213
журнал оставшегося sentinel:
+sdown master cache 127.0.0.1 47213
+new-epoch 1
+vote-for-leader b272810fff90a5f0c6e6fea1106eda051b041c3b 1
+odown master cache 127.0.0.1 47213 #quorum 2/2
+new-epoch 2
+try-failover master cache 127.0.0.1 47213
+vote-for-leader d7d122dbed4aac4d80d17107ef91b4ae88351035 2
#quorum 2/2 — условие из настройки выполнено, мастер объявлен упавшим. И
следом -failover-abort-not-elected: голосов двух из пяти не хватило, чтобы
стать ведущим. Стоило поднять одного из погашенных — большинство появилось, и
переключение состоялось за две секунды, при том же самом кворуме.
Спрашивать об этом по факту аварии необязательно — есть команда, которая отвечает заранее, и она проверяет оба условия:
18. КАК ПРОВЕРИТЬ КВОРУМ И БОЛЬШИНСТВО ЗАРАНЕЕ, А НЕ ПО ФАКТУ
----------------------------------------------------------------------
sentinel в наборе / кворум 5 / 2
живых sentinel 5
SENTINEL CKQUORUM cache OK 5 usable Sentinels. Quorum and failover authorization can be reached
живых sentinel 2
SENTINEL CKQUORUM cache NOQUORUM 2 usable Sentinels. Not enough available Sentinels to reach the majority and authorize a failover
Отказ назван по второму условию прямым текстом: наблюдателей не хватает, чтобы достичь большинства и авторизовать переключение, — при том что кворума из настройки хватает. Это готовый признак для наблюдения: он отвечает на вопрос «переключимся ли мы, если мастер упадёт сейчас», а не «всё ли хорошо прямо сейчас».
Практическое правило, которое из одной настройки не выводится: потерю большинства sentinel система не переживёт, каким бы низким ни был кворум. Отсюда и минимум в документации:
You need at least three Sentinel instances for a robust deployment.
Для надёжной установки нужно как минимум три экземпляра Sentinel.
Чем это отличается от Redis
Вопрос законный: оба механизма Valkey унаследовал от Redis целиком, и до какого-то момента это буквально один и тот же код. Граница названа в самом Valkey, в одном файле с номером версии:
#define VALKEY_VERSION "9.1.2"
/* Redis OSS compatibility version, should never
* exceed 7.2.x. */
#define REDIS_VERSION "7.2.4"То есть совместимость Valkey объявляет по 7.2.4 — версии, на которой проекты разошлись.
Пользоваться этой строкой как доказательством, однако, нельзя, и это стоит сказать прямо. Она означает ровно одно: до 7.2.4 общее прошлое. Из неё не следует ни что всё до этой точки совпадает сегодня, ни что всё после неё разошлось. Обе стороны с тех пор меняли и то, что было общим. Единственный честный способ сравнивать — брать возможность и смотреть текущую документацию обоих проектов; ниже так и сделано, и результат оказался не тем, к которому подводит рассуждение «появилось в Valkey — значит, в Redis нет».
Первый пример — атомарный перенос слотов, появившийся в Valkey 9.0.
Valkey 9.0 introduced a option for migrating hash slots known as atomic slot
migration, which is faster, more reliable, and has less impact on client
applications than the legacy CLUSTER SETSLOT-based migration.
В Valkey 9.0 появился способ переноса хеш-слотов, называемый атомарной миграцией слотов; он быстрее, надёжнее и меньше задевает клиентские приложения, чем прежняя миграция на основе CLUSTER SETSLOT.
Что здесь важно для читателя: старый способ переноса — тот самый, из-за
которого в спецификации живёт перенаправление ASK и предупреждение, что
многоключевые операции на время переезда могут отвечать TRYAGAIN. Атомарный
перенос эту фазу сокращает.
А вот напрашивающийся вывод «и в Redis такого нет» неверен. В Redis Open Source 8.4 атомарный перенос тоже появился, своей командой:
This command allows you to import slots from other nodes, monitor the progress
of migration tasks, and cancel ongoing migrations... Executes on the
destination master. Accepts multiple slot ranges and triggers atomic migration
for the specified ranges.
Эта команда позволяет импортировать слоты с других узлов, следить за ходом задач переноса и отменять текущие переносы... Выполняется на узле-получателе. Принимает несколько диапазонов слотов и запускает атомарный перенос для указанных диапазонов.
То есть по состоянию на сегодня возможность есть у обоих, а вот команды и
протокол разные: у Valkey это CLUSTER MIGRATESLOTS и внутренняя
CLUSTER SYNCSLOTS, у Redis — CLUSTER MIGRATION. Для эксплуатации это
разница более практическая, чем «есть или нет»: сценарии переноса и оснастка
вокруг них не переносятся между проектами, даже когда возможность называется
одинаково.
| возможность | Valkey | Redis |
|---|---|---|
| общее прошлое до | 7.2.4 | 7.2.4 |
| несколько баз в кластере | с 9.0, cluster-databases | нет |
| атомарный перенос слотов | с 9.0, CLUSTER MIGRATESLOTS | с 8.4, CLUSTER MIGRATION |
Всё остальное в этой статье — слоты, MOVED, CROSSSLOT, устройство
Sentinel, кворум против большинства — досталось обоим проектам из общего
прошлого. На Redis здесь не мерилось ничего, и утверждать «в Redis точно
так же» статья не будет: не мерили — не говорим. Ровно это и подвело первую
версию раздела.
Что из этого выбирать
Разбор короткий, и он не про производительность.
нужно больше памяти или пропускной способности, чем даёт один узел
→ нативный кластер: это единственный из двух, который шардирует
данные помещаются в один узел, нужна только отказоустойчивость
→ Sentinel остаётся кандидатом
код опирается на многоключевые команды по произвольным ключам
→ Sentinel проще; в кластере придётся сводить ключи в один слот скобками
нужны нумерованные базы
→ Valkey до 9: только Sentinel
→ Valkey 9+: и кластер тоже, через cluster-databases
нужен SWAPDB
→ только Sentinel: в кластере он запрещён и в 9.x
клиент умеет протокол кластера?
→ нет: кластер недоступен без замены клиента или прокси
клиент умеет sentinel?
→ нет: Sentinel недоступен без замены клиента или прокси
Две последние строки стоит прочитать внимательно: ходовое «клиент не умеет
sentinel → берите кластер» подводит к мысли, будто кластер прощает клиенту
наивность. Не прощает. Обычный клиент,
не знающий про кластер, получив MOVED, просто отдаст текст ошибки
приложению: карту слотов он не ведёт, повторять по адресу не умеет, ASK и
ASKING не знает, TRYAGAIN не обрабатывает. Оба режима требуют своей
поддержки в клиенте, и умения эти независимы: библиотека может уметь один,
другой, оба или ни одного. Если ни одного — не заработает ни то ни другое, и
вопрос переносится с выбора режима на выбор библиотеки или прокси.
Что верно и осталось верным: клиенту кластера достаточно одного живого адреса, чтобы получить карту (блок 3). В проде их всё равно указывают несколько — на случай, если конкретно этот узел недоступен в момент старта.
И ещё одна оговорка про многоключевые команды. Отказ CROSSSLOT — это
контракт СЕРВЕРА. Некоторые клиентские
библиотеки поверх него делают удобный многоключевой вызов, разбирая его на
несколько команд по узлам. Работает это как обёртка, а не как та же команда:
атомарности у неё нет, модель отказа другая (часть подзапросов может пройти,
часть нет), и задержка складывается из нескольких обращений.
Что должно повториться, а что нет
Числа сняты на Valkey 9.1.2 (собран из исходников — как именно, написано в README
каталога bench/valkey-cluster/),
на одном хосте, процессы на случайных портах. Скрипты и записи прогонов
открываются отсюда: bench/valkey-cluster/.
Секунды не переносятся никуда. Узел считается упавшим через секунду
молчания вместо тридцати по умолчанию, cluster-node-timeout — 2000 мс вместо
15 000. Это сделано затем, чтобы набор укладывался в минуты. Десять секунд
из блока 10 и 6,07 секунды из блока 11 на ваших настройках будут другими — а
вот то, что оба окна существуют, от настроек не зависит.
Дословные ответы сервера переносятся полностью. MOVED, CROSSSLOT,
ERR DB index is out of range, READONLY, -failover-abort-not-elected — это
поведение, а не измерение, и на той же версии оно будет тем же.
Между версиями — не обязательно. Всё снято на 9.1.2; ни на 8.x, ни на Redis эти прогоны не повторялись. Место, где расхождение заведомо есть, названо выше отдельно.
Исправления
12 сентября 2026Здесь собрано то, что в этом материале было сказано неверно, и то, чем это снято. Каждая запись держится на прогоне или на документе, а не на том, что мы передумали.
«В кластере база ровно одна» — и отсюда вывод, что нумерованных баз в кластере нет вовсе.
В Valkey 9 базы в кластере есть. Их число задаёт cluster-databases, и по умолчанию оно равно единице — поэтому старое правило и выглядит верным. Правилом Redis и Valkey до 9.0 оно быть не перестало; всеобщим законом оно не было никогда.
Два свежих кластера, отличающихся одной строкой конфига: при cluster-databases 1 команда SELECT 1 отвечает ERR DB index is out of range, при cluster-databases 16 — OK. Номер базы при этом в адресацию не входит: один и тот же ключ в базах 0 и 1 держит разные значения, а слот у него один — 12182 (bench/valkey-cluster/databases.py).
«Атомарный перенос слотов появился в Valkey 9.0 — значит, в Redis такого нет».
Возможность есть у обоих, протоколы разные. В Redis Open Source 8.4 атомарный перенос появился своей командой CLUSTER MIGRATION вместо CLUSTER MIGRATESLOTS.
Текущая документация обоих проектов, процитированная в тексте. Это исправление МЕТОДА сравнения: из того, что возможность объявлена в одном проекте, не следует ничего о другом — сравнивать надо по возможностям и текущей документации, а не по датам появления. На Redis здесь ничего не перемерялось, и статья этого не утверждает.
«Узел под sentinel про топологию не знает ничего».
Свою пару репликации узел знает вполне: мастер видит своих реплик, реплика — своего мастера. Чего он не знает — это состава всей группы и того, кто принимает решение о переключении.
Прогон INFO replication на обеих сторонах пары (bench/valkey-cluster/boundaries.py). Формулировка сужена до измеренного.
«Клиент не умеет sentinel → берите кластер» — и отсюда, что кластер прощает клиенту наивность.
Не прощает. Обычный клиент, не знающий про кластер, получив MOVED, отдаст текст ошибки приложению: карту слотов он не ведёт, повторять по адресу не умеет, ASK и ASKING не знает, TRYAGAIN не обрабатывает. Оба режима требуют своей поддержки в клиенте, и умения эти независимы.
Это разбор контракта, а не замер: поведение MOVED, ASK и TRYAGAIN описано в спецификации кластера, процитированной в тексте.
Отказ CROSSSLOT подавался без оговорки о клиентских библиотеках.
Отказ CROSSSLOT — это контракт СЕРВЕРА. Некоторые клиентские библиотеки делают поверх него удобный многоключевой вызов, разбирая его на несколько команд по узлам. Это обёртка, а не та же команда: атомарности у неё нет, модель отказа другая, и задержка складывается из нескольких обращений.
Добавленная оговорка, а не изменённое утверждение: сам отказ измерен и остался прежним, но без этой рамки читатель переносит серверный контракт на поведение своей библиотеки.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Слов «кластеризация Valkey» два способа, и выбирают между ними обычно по отказоустойчивости — хотя она есть у обоих. Выбирать надо по другому: кто отвечает клиенту на вопрос «куда слать команду».
- Нативный кластер отвечает адресом. Ключ отображается в один из 16 384 слотов, слот принадлежит узлу, и узел, если слот не его, возвращает не данные, а
MOVED 12182 127.0.0.1:48711. Спросить можно любой узел: каждый знает карту целиком. - Sentinel — это не кластер, а наблюдатель. Данные лежат на обычном мастере с репликами, шардирования нет вовсе. Узел данных про топологию не знает ничего и на
CLUSTER INFOотвечаетERR This instance has cluster support disabled. Адрес мастера знает отдельная служба, и клиент обязан спросить её сам. - Шардирование меняет набор доступных команд. В кластере
MSET foo 1 bar 2— этоCROSSSLOT Keys in request don't hash to the same slot: команда выполняется целиком на одном узле, а ключи легли в разные слоты. Под sentinel узел один, и та же команда работает. - А вот про нумерованные базы расхожее знание устарело. «В кластере база ровно одна» — правило Redis и Valkey до 9.0. В Valkey 9 базы в кластере есть, их число задаёт
cluster-databases, и по умолчанию оно равно единице — поэтому старое правило и выглядит верным. Перемерено на двух кластерах, отличающихся одной строкой конфига. - И разное поведение при отказе. В кластере клиент получает от живого узла осмысленный ответ и новый адрес слота. Под sentinel старый адрес просто молчит, и нового адреса клиенту не скажет никто.
- Самое опасное место режима Sentinel — возвращение старого мастера. Замерено: десять секунд он поднимается мастером и отвечает
OKна записи, а после перевода в реплику синхронизация их стирает. Ошибки клиент не видит ни одной. В кластере такого окна нет — проверено тем же прогоном. - Найдено по дороге: «кластер собран» и «кластер переживёт отказ» — разные состояния. Между ними 6,07 секунды, и мастер, упавший внутри этого окна, не заменяется НИКОГДА.
- Кворум и большинство — разные числа. Пять sentinel, кворум 2, живых двое: кворум выполнен, переключения нет, в журнале
-failover-abort-not-elected. Спросить об этом заранее умеетSENTINEL CKQUORUM. - Версия — пятое измерение сравнения, и его забывают чаще прочих. Нумерованные базы в кластере появились в Valkey 9.0, атомарный перенос слотов — в Valkey 9.0 и в Redis 8.4. Правила, унаследованные из знаний о Redis Cluster, надо помечать версией или проверять.
- Всё снято на Valkey 9.1.2, собранном из исходников; прогоны целиком лежат в
bench/valkey-cluster/runs/.
На самом деле
- Sentinel вообще не кластер: он ничего не шардирует. Данные целиком лежат на одном мастере, реплики нужны только чтобы было кем его заменить. Проверяется одной командой: узел под sentinel на
CLUSTER INFOотвечаетERR This instance has cluster support disabled. Отсюда и то, ради чего эти два режима обычно путают: если данные в один узел не помещаются, Sentinel не поможет никак — он решает другую задачу, отказоустойчивость, и решает её без шардирования. - Это правило Redis и Valkey до 9.0, и в Valkey 9 оно неверно. Нумерованные базы в кластере появились в 9.0, а их число задаёт отдельная настройка
cluster-databases; по умолчанию она равна единице — поэтому кластер, поднятый обычным способом, отвечает наSELECT 1отказом, и старое правило выглядит верным. Перемерено на двух кластерах, отличающихся одной этой строкой: при 16 работают SELECT с 0 по 15. Номер базы при этом в адресацию не входит — один и тот же ключ в базах 0 и 1 держит разные значения при одном и том же слоте 12182. Обратный миф тоже неверен:SWAPDBостаётся запрещён, аDBSIZE,SCANиFLUSHDBвидят только ту часть базы, что лежит на узле, куда пришла команда. - Это ошибка перенаправления: команда НЕ выполнена, но вместо данных узел вернул адрес того, кто её выполнит. Узел не отказал: он назвал слот и того, кому слот принадлежит, —
MOVED 12182 127.0.0.1:48711. Клиент кластера на таком ответе чинит свою карту слотов и повторяет команду по названному адресу, и повтор здесь безопасен: узел точно ничего не сделал. Отличать это надо от повтора после сетевого обрыва — там клиент не знает, выполнилась команда или нет, и вопрос уже про идемпотентность, а не про маршрутизацию. Замерено, что отвечают так и мастера, и реплики: карту знают все шесть узлов. - Работают, если ключи в одном слоте. Граница проходит не по числу ключей, а по слотам:
MSET foo 1 bar 2даётCROSSSLOT Keys in request don't hash to the same slot, потому что foo в слоте 12182, а bar в 5061. Те же две записи какMSET {u}:a 1 {u}:b 2проходят: фигурные скобки заставляют хешировать только то, что внутри, и оба ключа попадают в слот 11826. Цена этого — не строчка кода, а решение о схеме ключей, которое принимается до первой записи. - Оно задаёт только, сколько нужно, чтобы ОБЪЯВИТЬ мастер упавшим. Выполнить переключение может лишь тот sentinel, которого избрало большинство всех известных, а это число нигде не настраивается. Замерено: пять sentinel, кворум 2, живых двое. Кворум выполнен — в журнале
+odown ... #quorum 2/2, — и тут же-failover-abort-not-elected: голосов двух из пяти не хватило. Стоило поднять одного из погашенных, и переключение прошло за две секунды при том же кворуме. - К работе — да, к отказу — нет, и между этими состояниями измеренные 6,07 секунды. Слоты покрываются почти сразу, а репликационный канал между мастером и его репликой поднимается ещё несколько секунд, и всё это время
master_link_statusу репликdown. Мастер, упавший внутри окна, не заменяется НИКОГДА: у реплики, ни разу не подключавшейся, отметка «когда упал канал» осталась нулевой, возраст её данных считается от начала эпохи Unix, и она навсегда застревает на Currently unable to failover: Disconnected from primary for longer than allowed. Слово «навсегда» тут про значение по умолчанию: тот же опыт сcluster-replica-validity-factor 0даёт повышение за четыре секунды — правда, реплика при этом повышается пустой. - Поднять можно, но он вернётся МАСТЕРОМ — он таким себя помнит, — и будет принимать записи, пока sentinel его не обнаружит и не переведёт в реплику. Замерено: десять секунд
SETотвечалOK. После перевода полная синхронизация с настоящим мастером эти записи стёрла, и клиент не увидел ни одной ошибки. Опасность не в самом окне, а в том, кто в него попадает: клиент, держащий адрес данных напрямую. В кластере такого окна нет — вернувшийся узел сразу отдаётMOVEDна нового владельца слота. - Общее прошлое у них до 7.2.4 — это Valkey объявляет сам, строкой
REDIS_VERSION "7.2.4"рядом сVALKEY_VERSION "9.1.2". Но брать эту строку за доказательство нельзя: из неё не следует ни что всё до этой точки совпадает сегодня, ни что всё после неё разошлось. Сравнивать надо по возможностям и текущей документации обоих. Пример, на котором рассуждение «появилось в Valkey — значит, в Redis нет» и спотыкается: атомарный перенос слотов появился в Valkey 9.0 — и в Redis Open Source 8.4 тоже, только своей командойCLUSTER MIGRATIONвместоCLUSTER MIGRATESLOTS. Возможность есть у обоих, протоколы разные. Здесь на Redis ничего не перемерялось. - Скажет — но воспользоваться этим умеет только клиент, знающий протокол кластера. Обычный, получив
MOVED, просто отдаст текст ошибки приложению: карту слотов он не ведёт, повторять по адресу не умеет, проASK,ASKINGиTRYAGAINне знает. Оба режима требуют своей поддержки в библиотеке, и умения эти независимы: она может уметь кластер, sentinel, оба или ни одного. Что верно — клиенту кластера хватает ОДНОГО живого адреса, чтобы получить карту; в проде их всё равно указывают несколько, на случай недоступности конкретного узла при старте.
Что разобрано
- Один вопрос, на который два режима отвечают по-разному
- Как ключ превращается в адрес
- Что при этом делает настоящий клиент
- Что в кластере ломается из привычного
- Нумерованные базы: здесь ответ зависит от версии Valkey
- Отказ мастера: одно событие, два разных клиентских опыта
- Старый мастер вернулся
- Найдено по дороге: готов к работе ≠ готов к отказу
- Кворум и большинство — разные числа
- Чем это отличается от Redis
- Что из этого выбирать
- Что должно повториться, а что нет
Расхожие заблуждения
Sentinel — это «лёгкий кластер» Valkey
Sentinel вообще не кластер: он ничего не шардирует. Данные целиком лежат на одном мастере, реплики нужны только чтобы было кем его заменить. Проверяется одной командой: узел под sentinel на CLUSTER INFO отвечает ERR This instance has cluster support disabled. Отсюда и то, ради чего эти два режима обычно путают: если данные в один узел не помещаются, Sentinel не поможет никак — он решает другую задачу, отказоустойчивость, и решает её без шардирования.
В кластере Valkey доступна только база 0
Это правило Redis и Valkey до 9.0, и в Valkey 9 оно неверно. Нумерованные базы в кластере появились в 9.0, а их число задаёт отдельная настройка cluster-databases; по умолчанию она равна единице — поэтому кластер, поднятый обычным способом, отвечает на SELECT 1 отказом, и старое правило выглядит верным. Перемерено на двух кластерах, отличающихся одной этой строкой: при 16 работают SELECT с 0 по 15. Номер базы при этом в адресацию не входит — один и тот же ключ в базах 0 и 1 держит разные значения при одном и том же слоте 12182. Обратный миф тоже неверен: SWAPDB остаётся запрещён, а DBSIZE, SCAN и FLUSHDB видят только ту часть базы, что лежит на узле, куда пришла команда.
MOVED — это ошибка, и клиенту надо её обрабатывать как сбой
Это ошибка перенаправления: команда НЕ выполнена, но вместо данных узел вернул адрес того, кто её выполнит. Узел не отказал: он назвал слот и того, кому слот принадлежит, — MOVED 12182 127.0.0.1:48711. Клиент кластера на таком ответе чинит свою карту слотов и повторяет команду по названному адресу, и повтор здесь безопасен: узел точно ничего не сделал. Отличать это надо от повтора после сетевого обрыва — там клиент не знает, выполнилась команда или нет, и вопрос уже про идемпотентность, а не про маршрутизацию. Замерено, что отвечают так и мастера, и реплики: карту знают все шесть узлов.
В кластере не работают многоключевые команды
Работают, если ключи в одном слоте. Граница проходит не по числу ключей, а по слотам: MSET foo 1 bar 2 даёт CROSSSLOT Keys in request don't hash to the same slot, потому что foo в слоте 12182, а bar в 5061. Те же две записи как MSET {u}:a 1 {u}:b 2 проходят: фигурные скобки заставляют хешировать только то, что внутри, и оба ключа попадают в слот 11826. Цена этого — не строчка кода, а решение о схеме ключей, которое принимается до первой записи.
Число в sentinel monitor ... <кворум> задаёт, сколько наблюдателей достаточно для переключения
Оно задаёт только, сколько нужно, чтобы ОБЪЯВИТЬ мастер упавшим. Выполнить переключение может лишь тот sentinel, которого избрало большинство всех известных, а это число нигде не настраивается. Замерено: пять sentinel, кворум 2, живых двое. Кворум выполнен — в журнале +odown ... #quorum 2/2, — и тут же -failover-abort-not-elected: голосов двух из пяти не хватило. Стоило поднять одного из погашенных, и переключение прошло за две секунды при том же кворуме.
Если cluster_state:ok, кластер готов к работе
К работе — да, к отказу — нет, и между этими состояниями измеренные 6,07 секунды. Слоты покрываются почти сразу, а репликационный канал между мастером и его репликой поднимается ещё несколько секунд, и всё это время master_link_status у реплик down. Мастер, упавший внутри окна, не заменяется НИКОГДА: у реплики, ни разу не подключавшейся, отметка «когда упал канал» осталась нулевой, возраст её данных считается от начала эпохи Unix, и она навсегда застревает на Currently unable to failover: Disconnected from primary for longer than allowed
. Слово «навсегда» тут про значение по умолчанию: тот же опыт с cluster-replica-validity-factor 0 даёт повышение за четыре секунды — правда, реплика при этом повышается пустой.
После переключения старый мастер можно просто поднять обратно
Поднять можно, но он вернётся МАСТЕРОМ — он таким себя помнит, — и будет принимать записи, пока sentinel его не обнаружит и не переведёт в реплику. Замерено: десять секунд SET отвечал OK. После перевода полная синхронизация с настоящим мастером эти записи стёрла, и клиент не увидел ни одной ошибки. Опасность не в самом окне, а в том, кто в него попадает: клиент, держащий адрес данных напрямую. В кластере такого окна нет — вернувшийся узел сразу отдаёт MOVED на нового владельца слота.
Кластеризация Valkey и кластеризация Redis — одно и то же
Общее прошлое у них до 7.2.4 — это Valkey объявляет сам, строкой REDIS_VERSION "7.2.4" рядом с VALKEY_VERSION "9.1.2". Но брать эту строку за доказательство нельзя: из неё не следует ни что всё до этой точки совпадает сегодня, ни что всё после неё разошлось. Сравнивать надо по возможностям и текущей документации обоих. Пример, на котором рассуждение «появилось в Valkey — значит, в Redis нет» и спотыкается: атомарный перенос слотов появился в Valkey 9.0 — и в Redis Open Source 8.4 тоже, только своей командой CLUSTER MIGRATION вместо CLUSTER MIGRATESLOTS. Возможность есть у обоих, протоколы разные. Здесь на Redis ничего не перемерялось.
Кластер прощает наивному клиенту: любой узел сам скажет, куда идти
Скажет — но воспользоваться этим умеет только клиент, знающий протокол кластера. Обычный, получив MOVED, просто отдаст текст ошибки приложению: карту слотов он не ведёт, повторять по адресу не умеет, про ASK, ASKING и TRYAGAIN не знает. Оба режима требуют своей поддержки в библиотеке, и умения эти независимы: она может уметь кластер, sentinel, оба или ни одного. Что верно — клиенту кластера хватает ОДНОГО живого адреса, чтобы получить карту; в проде их всё равно указывают несколько, на случай недоступности конкретного узла при старте.
Проверьте себя
Клиент отправил SET foo 1 узлу кластера, который этим слотом не владеет. Что он получит?
Источники и что читать дальше
8 ИСТОЧНИКОВ
- Valkey — Cluster specificationОфициальная документация. Правило адресации, из которого следует всё остальное: «HASH_SLOT = CRC16(key) mod 16384» (ХЕШ_СЛОТ = CRC16(ключ) по модулю 16384). И то, чем это правило обходят: «If the key contains a "{...}" pattern only the substring between `{` and `}` is hashed in order to obtain the hash slot» (Если ключ содержит образец вида "{...}", для получения хеш-слота хешируется только подстрока между `{` и `}`). Оттуда же — честная оговорка про многоключевые операции: «Multi-key operations may become unavailable when a resharding of the hash slot the keys belong to is in progress» (Многоключевые операции могут стать недоступны, пока идёт перераспределение хеш-слота, которому принадлежат ключи).https://valkey.io/topics/cluster-spec/
- Valkey — High availability with Valkey SentinelОфициальная документация. Четыре обязанности службы, названные одним списком: «Monitoring, Notification, Automatic failover, Configuration provider» (Наблюдение, оповещение, автоматическое переключение, поставщик конфигурации). Последняя и есть ответ на вопрос, зачем клиенту вообще sentinel: клиенты обращаются к нему «for the address of the current Valkey primary responsible for a given service» (за адресом текущего главного узла Valkey, отвечающего за данную службу). И различение, которое статья проверяет замером: «The quorum is only used to detect the failure. In order to actually perform a failover, one of the Sentinels need to be elected leader... and be authorized to proceed. This only happens with the vote of the majority of the Sentinel processes» (Кворум используется только для обнаружения отказа. Чтобы переключение действительно выполнить, один из Sentinel должен быть избран ведущим... и получить право действовать. Это происходит только при голосовании большинства процессов Sentinel). Там же — минимум: «You need at least three Sentinel instances for a robust deployment» (Для надёжной установки нужно как минимум три экземпляра Sentinel).https://valkey.io/topics/sentinel/
- Valkey — Sentinel client specОфициальная документация. Что обязан делать клиент, и почему «просто подключиться к адресу» в этом режиме неверно. Шаг за шагом: «The client should iterate the list of Sentinel addresses» (Клиент должен перебрать список адресов Sentinel), затем `SENTINEL get-master-addr-by-name`, затем «it should attempt a connection with the primary, and call the `ROLE` command in order to verify the role of the instance is actually a primary» (клиент должен попытаться подключиться к главному узлу и вызвать команду ROLE, чтобы убедиться, что роль экземпляра действительно главная). И правило, объясняющее блок про вернувшийся адрес: «every time a reconnection is needed, the client should resolve again the address using Sentinels restarting from Step 1» (каждый раз, когда требуется переподключение, клиент должен заново определить адрес через Sentinel, начиная с шага 1).https://valkey.io/topics/sentinel-clients/
- Valkey — Numbered Databases in Valkey 9.0Официальная документация. Возможность, из-за которой пришлось переписывать целый раздел: «Valkey 9.0 adds the ability to have numbered databases on a cluster, changing everything about that advice» (Valkey 9.0 добавляет возможность иметь нумерованные базы в кластере, что полностью меняет прежний совет). Оттуда же — границы, без которых получается обратный миф: «will return the number of keys in the current database *on the connected node*» (вернёт число ключей в текущей базе на том узле, к которому вы подключены) про DBSIZE, то же про SCAN и FLUSHDB, и прямо: «numbered databases do not provide any form of resource isolation» (нумерованные базы не дают никакой формы изоляции ресурсов).https://valkey.io/blog/numbered-databases/
- Redis — CLUSTER MIGRATIONОфициальная документация. Источник, из-за которого исправлен раздел про отличия. Атомарный перенос слотов есть и в Redis: «This command allows you to import slots from other nodes, monitor the progress of migration tasks, and cancel ongoing migrations» (Эта команда позволяет импортировать слоты с других узлов, следить за ходом задач переноса и отменять текущие переносы), и там же — «Executes on the destination master. Accepts multiple slot ranges and triggers atomic migration for the specified ranges» (Выполняется на узле-получателе. Принимает несколько диапазонов слотов и запускает атомарный перенос для указанных диапазонов). Появилась в Redis Open Source 8.4.0, то есть возможность есть у обоих проектов, а команды разные.https://redis.io/docs/latest/commands/cluster-migration/
- Valkey — Atomic slot migrationОфициальная документация. Что появилось в Valkey 9.0: «Valkey 9.0 introduced a option for migrating hash slots known as atomic slot migration, which is faster, more reliable, and has less impact on client applications than the legacy `CLUSTER SETSLOT`-based migration» (В Valkey 9.0 появился способ переноса хеш-слотов, называемый атомарной миграцией слотов; он быстрее, надёжнее и меньше задевает клиентские приложения, чем прежняя миграция на основе CLUSTER SETSLOT). Отсюда напрашивается вывод «и в Redis такого нет», и он неверен: в Redis Open Source 8.4 атомарный перенос тоже появился, своей командой CLUSTER MIGRATION.https://valkey.io/topics/atomic-slot-migration/
- valkey/src/cluster_legacy.c — почему реплика отказывается повышатьсяИсходный код. Условие, которое объясняет находку блоков 11-12. Возраст данных реплики считается так: «if (server.repl_state == REPL_STATE_CONNECTED) { data_age = (mstime_t)(server.unixtime - server.primary->last_interaction) * 1000; } else { data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000; }» (если состояние репликации есть REPL_STATE_CONNECTED, возраст данных считается от последнего обмена с мастером; иначе — от момента, когда канал упал). У реплики, ни разу не подключавшейся к мастеру, `repl_down_since` равен нулю — возраст получается отсчитанным от начала эпохи Unix, и никакой допуск его не покрывает. Отсюда навсегда застревающее «Currently unable to failover: Disconnected from primary for longer than allowed» (Сейчас переключение невозможно: отключён от мастера дольше допустимого).https://github.com/valkey-io/valkey/blob/9.1.2/src/cluster_legacy.c
- valkey/src/version.h — что Valkey сообщает о совместимостиИсходный код. Две строки рядом, которые стоит знать до разговора о различиях: «#define VALKEY_VERSION "9.1.2"» (версия Valkey — 9.1.2) и «/* Redis OSS compatibility version, should never exceed 7.2.x. */ #define REDIS_VERSION "7.2.4"» (версия совместимости с Redis OSS, никогда не должна превышать 7.2.x; версия Redis — 7.2.4). То есть Valkey сам себя объявляет совместимым по 7.2.4 — версии, на которой проекты разошлись, — а всё, что появилось в обоих после неё, совпадением не обеспечено ничем.https://github.com/valkey-io/valkey/blob/9.1.2/src/version.h