Deep Engineering
Продвинутый·Опубликовано·60 МИН

Два способа собрать Valkey из нескольких узлов: кластер и Sentinel

Разница между ними не в отказоустойчивости — она есть у обоих. Различают их четыре вещи разом: как разложены данные, кто принимает решение о переключении, кто отвечает клиенту на вопрос «куда слать команду» и что для этого должен уметь сам клиент. Плюс пятое измерение, о котором забывают чаще всего: версия. Правило «в кластере база ровно одна» верно для Redis и Valkey до 9.0 и неверно для Valkey 9 — и здесь это перемерено на двух кластерах, отличающихся одной строкой конфига.

Полное техническое изложение

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

Valkey — Cluster specification

Второй шаг — из слота в узел, и вот он уже не арифметика, а состояние кластера: слоты кому-то принадлежат, и принадлежность меняется. Отсюда важное свойство: переезд данных между узлами не меняет ни одного ключа. Меняется владелец слота, а формула остаётся той же.

Проверяется это одной командой, отправленной по очереди всем шести узлам. Флага -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, чтобы убедиться, что роль экземпляра действительно главная.

Valkey — Sentinel client spec

Третий шаг — проверка 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.
перевод

Если ключ содержит образец вида «{...}», для получения хеш-слота хешируется только подстрока между { и }.

Valkey — Cluster specification

Практическое следствие дороже всего обходится задним числом: в кластере набор ключей, которые понадобится трогать одной командой, решается до первой записи. Под 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 добавляет возможность иметь нумерованные базы в кластере, что полностью меняет прежний совет.

Valkey — Numbered Databases in 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

перевод

вернёт число ключей в текущей базе на том узле, к которому вы подключены

Valkey — Numbered Databases in Valkey 9.0

Оттуда же — про то, за чем нумерованные базы брать не стоит:

numbered databases do not provide any form of resource isolation

перевод

нумерованные базы не дают никакой формы изоляции ресурсов

Там же

Итог: нумерованная база в кластере — это пространство имён, а не отдельное хранилище. Разнести по ним окружения можно; ждать от них изоляции или общего на кластер представления о содержимом — нельзя.

Отсюда правило, которое стоит унести из статьи целиком

Ошибка здесь не в невнимательности. Она в том, что знание про кластер берётся оттуда, откуда его берут все: из общей памяти про Redis Cluster, где база действительно одна. На Valkey 9 это правило перестало быть верным, а выглядит по-прежнему верным — потому что значение по умолчанию его подтверждает.

возможностьValkey ≤ 8.xValkey 9.0Valkey 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.

Valkey — Sentinel client spec

Не «желательно», а каждый раз. Почему — в следующем разделе.

Старый мастер вернулся

Это самое опасное место режима 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.

Почему «никогда», а не «пока не истечёт таймаут», видно в исходнике:

C
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.

Valkey — High availability with Valkey 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.

Valkey — High availability with Valkey Sentinel

Чем это отличается от Redis

Вопрос законный: оба механизма Valkey унаследовал от Redis целиком, и до какого-то момента это буквально один и тот же код. Граница названа в самом Valkey, в одном файле с номером версии:

C
#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.

Valkey — Atomic slot migration

Что здесь важно для читателя: старый способ переноса — тот самый, из-за которого в спецификации живёт перенаправление 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.

перевод

Эта команда позволяет импортировать слоты с других узлов, следить за ходом задач переноса и отменять текущие переносы... Выполняется на узле-получателе. Принимает несколько диапазонов слотов и запускает атомарный перенос для указанных диапазонов.

Redis — CLUSTER MIGRATION

То есть по состоянию на сегодня возможность есть у обоих, а вот команды и протокол разные: у Valkey это CLUSTER MIGRATESLOTS и внутренняя CLUSTER SYNCSLOTS, у Redis — CLUSTER MIGRATION. Для эксплуатации это разница более практическая, чем «есть или нет»: сценарии переноса и оснастка вокруг них не переносятся между проектами, даже когда возможность называется одинаково.

возможностьValkeyRedis
общее прошлое до7.2.47.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 16OK. Номер базы при этом в адресацию не входит: один и тот же ключ в базах 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 — это контракт СЕРВЕРА. Некоторые клиентские библиотеки делают поверх него удобный многоключевой вызов, разбирая его на несколько команд по узлам. Это обёртка, а не та же команда: атомарности у неё нет, модель отказа другая, и задержка складывается из нескольких обращений.

Чем снято

Добавленная оговорка, а не изменённое утверждение: сам отказ измерен и остался прежним, но без этой рамки читатель переносит серверный контракт на поведение своей библиотеки.

Расхожие заблуждения

Утверждение

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, оба или ни одного. Что верно — клиенту кластера хватает ОДНОГО живого адреса, чтобы получить карту; в проде их всё равно указывают несколько, на случай недоступности конкретного узла при старте.

Проверьте себя

Вопрос 1 из 6

Клиент отправил SET foo 1 узлу кластера, который этим слотом не владеет. Что он получит?

Источники и что читать дальше

8 ИСТОЧНИКОВ

  1. 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/
  2. 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/
  3. 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/
  4. 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/
  5. 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/
  6. 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/
  7. 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
  8. 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