Растянутый кэш на два датацентра: почему аналогия с базой ложная
«База же у нас растянута репликами, и ничего» — про базу это правда, но «растянуть» у базы и у кэша означает разное. Кэш держится сравнительным свойством: поход в него должен обходиться дешевле операции источника, которую он заменяет. На дешёвом чтении по ключу за каналом между ДЦ этого не остаётся — 41,09 мс против 0,09 мс до базы под рукой. А кластер, разложенный по датацентрам поровну, при разрыве отказал в обеих половинах сразу, включая ту, у которой большинство голосов. Заодно здесь проверен сам измерительный стенд — и оказалось, что одно из опубликованных чисел мерило не расстояние, а дефект прибора.
Полное техническое изложение
TL;DR
- Мысль «растянем кэш на два датацентра» опирается на аналогию с базой. База растянута и работает — значит, и кэш можно. Аналогия неверна.
- Кэш нужен затем, чтобы повторное получение данных обходилось дешевле обращения к источнику. «Дешевле» — это и задержка, и снятая нагрузка, и несделанный пересчёт, и лимит внешнего вызова. Замерена одна сторона — задержка — на самом невыгодном для кэша случае: 41,09 мс в чужом ДЦ против 0,09 мс до базы под рукой.
- Мастер не ждёт реплику. При разрыве сразу после залпа записей потерялось
93 записи из 200. Клиенту на все двести пришёл
OK. - Окно потерь задаёт отставание, а не круг. На одном и том же канале разный темп записи даёт 104, 3 и 0 потерянных записей.
- Кластер поровну на два ДЦ при разрыве отказал в обеих половинах — включая ту, где было большинство мастеров. Каждая половина проверена на своём свежем кластере.
- Проверен и сам измерительный стенд. У ретранслятора, изображавшего канал
между ДЦ, не был выставлен
TCP_NODELAY, и прибор добавлял к каждой записи сWAITсорок миллисекунд собственного залипания. Ровно они и публиковались здесь как «слагаемое неизвестной природы». - Те же утверждения прогнаны на Redis 8.10.1 и Valkey 9.1.2 — ни одно не разошлось.
- Как надо: свой кэш в каждом ДЦ. Расхождение между ними допустимо и складывается из отставания источника, задержки инвалидации, политики свежести и последствий отказов; TTL — одно слагаемое из четырёх.
Три способа растянуть кэш
Растягивают кэш одним из трёх способов:
- один кластер на два ДЦ, узлы поровну;
- мастер в одном ДЦ, реплика в другом;
- приложение в обоих ДЦ ходит в один кэш.
Довод под всеми тремя один: база у нас растянута репликами и работает, значит, и кэш так можно.
Довод не глупый. Он просто неверен, и неверен в трёх разных местах.
Кэш и база решают разные задачи
База — источник истины. Её нельзя потерять, и ради этого не жалко платить время на дорогу между датацентрами.
Кэш нужен затем, чтобы повторное получение данных обходилось дешевле обращения к источнику. «Дешевле» — не обязательно «быстрее»: это и снятая нагрузка, и несделанный дорогой пересчёт, и не потраченный лимит внешнего вызова, и защита горячего ключа. Свойство в любом случае сравнительное: отодвиньте кэш дальше источника — и его не станет.
Дальше измерена одна из этих сторон — задержка, — и измерена на самом невыгодном для кэша случае.
7. КЭШ В ЧУЖОМ ДЦ МЕДЛЕННЕЕ БАЗЫ В СВОЁМ
------------------------------------------------------------------
медиана, мс p95, мс
кэш в своём ДЦ 0.10 0.14
база в своём ДЦ 0.09 0.13
кэш в чужом ДЦ 41.09 41.36
кэш в чужом ДЦ медленнее базы в своём примерно в 450 раз
Третий способ умирает здесь. Половина запросов пойдёт в чужой ДЦ, заплатит дорогу туда и обратно и вернётся позже, чем вернулся бы поход в свою базу.
Из первых двух строк не следует, что кэш не нужен: там измерено чтение одной строки по ключу из маленькой таблицы в памяти — самое дешёвое, что база умеет. Следует другое, и это правило, а не число: поход в кэш обязан быть дешевле той операции источника, которую он заменяет, на этом расстоянии. Для дешёвого чтения по ключу ответ на сорока миллисекундах отрицательный. Для агрегата, который считается сто миллисекунд, — уже нет, и такой случай здесь не измерялся.
Терять записи кэша — норма, терять строки базы — авария
Второй способ: мастер здесь, реплика там. Мастер отвечает клиенту
OK, не дожидаясь реплики. Это не настройка, а объявленное поведение:
Redis uses by default asynchronous replication
(Redis по умолчанию использует асинхронную репликацию).
Теперь поднимем реплику на место мастера — то есть сделаем то, ради чего её и держат.
2. ПЕРЕКЛЮЧЕНИЕ НА РЕПЛИКУ ТЕРЯЕТ ПОДТВЕРЖДЁННЫЕ ЗАПИСИ
------------------------------------------------------------------
записей подтверждено клиенту 200
разрыв сразу после записи:
оказалось на реплике 107
потеряно 93 (46.5%)
разрыв через секунду после записи:
оказалось на реплике 200
потеряно 0 (0.0%)
Обе строки надо читать вместе. Теряется не «почти всё» и не «ничего», а то,
что не успело уехать к реплике. Ни одна из потерянных записей об ошибке не
сообщила: на все клиент получил OK.
Окно потерь задаёт отставание, а не круг
Напрашивается правило «под угрозой всё, что записано за последний круг». Правило ходит по статьям в таком виде, и оно неверно: круг здесь совпал с потерей случайно — двести быстрых записей как раз в него и уложились. Проверяется это изменением одного лишь темпа записи, при том же канале.
9. ОКНО ПОТЕРЬ ЗАДАЁТ ОТСТАВАНИЕ, А НЕ КРУГ
------------------------------------------------------------------
200 записей залпом, обрыв сразу:
подтверждено клиенту 200
оказалось на реплике 96
потеряно 104
master_repl_offset 6403
slave_repl_offset 2855
отставание, байт 3548
Два других темпа на том же канале дают другое: с паузой в 5 мс между записями теряется 3 записи при отставании 99 байт, а через секунду тишины — ноль при нулевом отставании. Канал один и тот же, круг тот же; меняется отставание, и вместе с ним потеря.
Правило в верном виде: под угрозой то, что в момент разрыва лежит между
master_repl_offset мастера и смещением реплики. Оба числа Redis отдаёт в
INFO replication, и по их разнице видно, сколько вы потеряете, если
переключение случится прямо сейчас.
И вот здесь аналогия ломается. Для базы потерянный хвост — авария: строку никто не пересчитает. Для кэша потеря записи — обычное дело: промах, поход в источник, запись обратно.
Значит, вся машинерия, ради которой терпят растянутую базу, кэшу по умолчанию
не нужна. А платить за неё придётся: команда WAIT, которой обычно предлагают
«сделать репликацию синхронной», делает запись дороже в 171,6 раза —
41,73 мс вместо 0,24 мс на каждую запись.
Из чего складывается эта цена, по одной точке сказать нельзя — и догадка «это два круга» напрашивается сама собой. Тот же опыт на трёх расстояниях отвечает точнее, и отвечает не то.
10. ЦЕНА WAIT РАСТЁТ ВМЕСТЕ С РАССТОЯНИЕМ, И ЭТО ПРОВЕРЯЕТСЯ
------------------------------------------------------------------
в одну сторону круг обычная с WAIT 1 WAIT / круг
5 мс 10 мс 0.15 мс 10.90 мс 1.09
20 мс 40 мс 0.14 мс 41.52 мс 1.04
40 мс 80 мс 0.20 мс 81.40 мс 1.02
Отношение к кругу — единица на всех трёх расстояниях. То есть WAIT стоит
ровно один круг до реплики, и с расстоянием растёт один к одному.
Этот блок публикуется во второй редакции, и первая была неверна — причём
неверна не выводом, а числами. В ней отношение к кругу было 5,20, 2,10 и 1,55,
а сверх круга оставалось «слагаемое в 42–44 мс неизвестной природы». Природа
выяснилась, и она оказалась не про Redis: у ретранслятора, изображавшего канал
между ДЦ, не был выставлен TCP_NODELAY, и пара «алгоритм Нейгла — отложенное
подтверждение» добавляла сорок миллисекунд к каждой операции, где по каналу
шло несколько мелких сообщений подряд. Прибор мерил себя. Полный разбор — в
подробной версии статьи, блоки 11–14.
Покупает же WAIT не гарантию: он сообщает, сколько реплик подтвердило, но
отменить запись не может — на мастере она уже применена и уже видна читателям.
Для дёшево восстанавливаемого кэша платить круг на каждую запись трудно
оправдать. Но если прогрев стоит часов или источник не переживёт лавины
холодного старта, надёжность репликации приобретает цену, и сравнивать её надо
со стоимостью повторного наполнения, а не с нулём.
Слово «растянуть» у базы и у кэша означает разное
Асинхронная горячая реплика Postgres в другом ДЦ — отдельная система: читающая, отстающая, и приложение об этом знает. Она ничего не решает за мастера.
Оговорка обязательна: «база растянута» — не описание архитектуры. У Postgres есть и синхронная репликация, при которой коммит ждёт удалённый standby, бывают и базы с кворумом. Сравнивать надо конкретную модель репликации с конкретной и оба конца называть — ниже сравнивается асинхронная реплика Postgres с кластером Redis.
Кластер Redis на два ДЦ — одна система, которая принимает решения голосованием. Условие выживания записано в спецификации, и в нём два требования:
Redis Cluster is able to survive partitions where the majority of the master
nodes are reachable and there is at least one reachable replica for every master
node that is no longer reachable.
Кластер Redis способен пережить разделения сети, при которых доступно большинство мастер-узлов и для каждого недоступного мастер-узла доступна хотя бы одна реплика.
Первое помнят все: нужно большинство. Второе читают по диагонали. Из-за него всё и происходит.
Главное: отказали обе половины
Шесть узлов, по три в каждом ДЦ, мастера поделены два и один. Схема симметричная, и этим она убеждает.
5. РАСКЛАД B: УЗЛЫ ПОРОВНУ, МАСТЕРА ПОДЕЛЕНЫ 2 И 1
------------------------------------------------------------------
── опыт на свежем кластере, наблюдается ДЦ-1 (2 мастера)
ДЦ-1: [52467, 45191, 54701]
52467: мастер
45191: мастер
54701: реплика 52467
ДЦ-2: [35445, 32777, 38349]
35445: мастер
32777: реплика 45191
38349: реплика 35445
право реплик наблюдаемой половины повыситься, до разрыва:
узел 54701: канал up, смещение 201, validity-factor 10
ДЦ-1 (2 мастера): 2 мастеров из 3 узлов
узел 52467: состояние fail GET -> CLUSTERDOWN
узел 45191: состояние fail GET -> CLUSTERDOWN
узел 54701: состояние fail GET -> CLUSTERDOWN
роли на наблюдаемой половине не изменились
Кэша не стало нигде. Ни в одном из двух датацентров.
С ДЦ-2 всё понятно: один мастер из трёх, большинства нет.
А ДЦ-1 большинство имел — два мастера из трёх. Голосов хватило. Но повышать
оказалось некого: реплика того мастера, что остался в ДЦ-2, тоже осталась в
ДЦ-2 (посмотрите карту вверху блока). Слоты этого мастера не покрыл никто, а
кластер с неполным покрытием (cluster-require-full-coverage yes по умолчанию)
перестаёт принимать записи.
Почему отказали и чтения
За чтения отвечает другая настройка — cluster-allow-reads-when-down, по
умолчанию no. Напрашивается вывод, что наблюдаемое поведение «шире
написанного»; вывод неверен — поведение описано. Цепочка такая:
неполное покрытие слотов → кластер помечен отказавшим → узел отказавшего
кластера не обслуживает и чтения. Проверяется тем же раскладом, прогнанным
дважды.
8. ЧТЕНИЯ В ОТКАЗАВШЕМ КЛАСТЕРЕ РЕШАЕТ ОТДЕЛЬНАЯ НАСТРОЙКА
------------------------------------------------------------------
cluster-allow-reads-when-down no (по умолчанию):
узел 43151: master состояние fail 20 ключей -> 20 CLUSTERDOWN
узел 47729: master состояние fail 20 ключей -> 20 CLUSTERDOWN
узел 35021: slave состояние fail 20 ключей -> 20 CLUSTERDOWN
SET на том же узле -> CLUSTERDOWN
cluster-allow-reads-when-down yes:
узел 54835: master состояние fail 20 ключей -> 5 ответил, 15 перенаправил
узел 46259: master состояние fail 20 ключей -> 9 ответил, 11 перенаправил
узел 48093: slave состояние fail 20 ключей -> 20 перенаправил
SET на том же узле -> CLUSTERDOWN
Со значением yes узел отвечает только по тем ключам, чьи слоты остались за
ним: из двадцати выжившая половина отдала четырнадцать, остальные перенаправила
на мастер за оборванным каналом. Записи отказаны в обоих случаях. То есть
настройка не возвращает кэш — она превращает «не работает ничего» в «работает
часть чтений на части пространства ключей».
Отсюда правило: большинство голосов необходимо, но недостаточно. Полное
условие состоит из трёх частей: большинство мастеров на своей стороне, плюс
доступная реплика каждого потерянного мастера, плюс достаточная свежесть этой
реплики — отставшая сверх cluster-replica-validity-factor выборы не начнёт. В
этих опытах свежесть не мешала, и это видно в строке «право реплик
повыситься».
Проверить это глазами по схеме нельзя: на схеме видно число узлов, а не то, чья реплика где стоит. Расставляет реплики сам кластер — при создании и после каждого переключения.
Переложить реплику руками можно, и тогда одна половина выживет. Но только одна: так вы выбираете, какой датацентр переживёт разрыв.
Границу этого вывода надо очертить точно. Утверждение не «оба датацентра не могут работать», а вот такое: в одном обычном кластере Redis нельзя получить симметричный разрыв, при котором обе половины независимо продолжают принимать записи, а потом автоматически сливаются обратно. Другие исходы возможны — два независимых кэша, работа только на чтение в урезанном виде, продукт с active-active и явной моделью разрешения конфликтов, — просто это уже не «один растянутый кластер», и здесь они не измерялись.
Как надо
- Кэш в каждом ДЦ свой, наполняется своим приложением из своего источника. Разрыв канала для него — не событие.
- Цена в идеальном установившемся режиме — отдельное наполнение каждого горячего ключа в каждом регионе. Это нижняя граница, а не оценка: «два промаха — и всё» было бы обманом, реальную нагрузку задают вытеснение, перезапуски, переключения, выкладки, инвалидация, холодный старт и переброс трафика. Отсюда и то, чего у одного общего кэша не было: два независимых домена лавины.
- Расхождение между ДЦ допустимо, но им управляет не один только TTL. Наблюдаемое расхождение складывается из четырёх слагаемых: отставание самого источника, задержка инвалидации, политика свежести (собственно TTL и его родня — обновление заранее, скользящий срок, отдача устаревшего значения на время пересчёта) и последствия отказов и восстановления. Бюджетом надо называть сумму.
- Синхронная инвалидация между ДЦ — дорогая сделка: ожидание подтверждения возвращает круг, от которого мы ушли, и возвращает его в горячий путь записи. Если ждать всё-таки приходится, дело не в TTL: требование «подтверждаем запись только после того, как её увидят в другом регионе» — это требование к модели согласованности кэш-слоя, и решается оно версионированием, маршрутизацией читателя в тот же регион или выносом кэша с пути корректности.
- Одноразовый кэш должен быть восстановим из авторитетного источника или из повторяемого вычисления. Источником бывает база, объектное хранилище, журнал событий, внешний API или канонический расчёт — важно не что это, а что путь восстановления известен.
- Свой кэш в каждом ДЦ убирает канал между ДЦ из пути к кэшу, но не делает приложение независимым от него. Если запись всё равно идёт в мастер базы в другом ДЦ, разрыв остаётся событием для приложения.
- Общий кэш остаётся кэшем. Ходовое правило звучит так: «если кэш обязан быть общим, значит, в нём лежит не кэш». Это неверно. Общий внешний кэш на несколько приложений — нормальная конструкция: он лучше снимает нагрузку с источника и переживает выкладку приложения. Красный флаг не «общий», а невосстановимость. Проверка простая: удалите кэш целиком. Стало медленнее, данные восстановились — это кэш. Потерялись сессии, блокировки, счётчики — это хранилище состояния, и разговор о нём другой.
Когда растянутый кэш всё-таки допустим
- Если разрыв должен пережить один назначенный ДЦ, а не оба. Это работает — но вы выбираете победителя заранее, во втором ДЦ кэша не будет, а расклад реплик придётся проверять после каждого переключения.
- Если реплика в чужом ДЦ нужна не для работы, а чтобы не наполнять кэш с нуля. Условия: реплики сами не повышаются, переключать придётся руками, и потеряется то, что в момент разрыва лежало между смещениями мастера и реплики. Для кэша последнее терпимо: записи пересчитаются при промахе.
- Если поток записи редкий. Вторая строка блока 2 и весь блок 9: опасен не объём данных, а отставание, которое темп записи успевает создать. Проверяется по разнице смещений, а не по расстоянию между ДЦ.
- Если операция источника дороже круга. Блок 7 сравнил кэш в чужом ДЦ с самым дешёвым, что умеет база. Для агрегата, который считается сто миллисекунд, сравнение другое, и кэш за сорок миллисекунд его выигрывает. Числа для дорогих операций здесь не снимались.
Общее у всех четырёх — то, чего в них нет: ни в одном не обещано, что обе половины одного кластера независимо продолжат принимать записи. Как только в требовании появляется слово «оба», из перечисленного не годится ничего.
TL;DR
Довод звучит разумно: «база у нас растянута на два датацентра — растянем и кэш». Аналогия ложная, и ломается она в трёх разных местах сразу.
- Кэш держится сравнительным свойством: поход в него должен обходиться дешевле той операции источника, которую он заменяет. На дешёвом чтении по ключу за каналом между ДЦ этого не остаётся: попадание в кэш в чужом ДЦ стоит 41,09 мс, чтение той же строки из базы под рукой — 0,09 мс.
- Мастер не ждёт реплику. Разрыв сразу после залпа записей потерял
93 записи из 200; клиенту на все двести пришёл
OK. Это не сбой, а объявленный договор асинхронной репликации. WAITпокупает не то, за что его принимают: запись дорожает в 171,6 раза, а отменить её нечем — к моменту ответа она уже применена. Цена — ровно круг до реплики, и это проверено на трёх расстояниях.- Окно потерь задаёт отставание, а не круг. На одном и том же канале разный
темп записи даёт 104, 3 и 0 потерянных записей; смотреть надо на разницу
смещений в
INFO replication. - Главное. Кластер из шести узлов, разложенный по датацентрам поровну, при разрыве отказал в обеих половинах — включая ту, у которой было большинство мастеров. Каждая половина проверена на своём свежем кластере. Большинство голосов необходимо, но недостаточно.
- Проверен и сам измерительный стенд, и это стоило одного числа. В прежней
версии у
WAITнаходилось «слагаемое в 42–44 мс неизвестной природы». Природа выяснилась: у ретранслятора, изображавшего канал, не был выставленTCP_NODELAY. Прибор показывал сорок миллисекунд собственного залипания. - Те же утверждения проверены на Redis 8.10.1 и Valkey 9.1.2. Ни одно не разошлось.
- Что делать. Умолчание — свой кэш в каждом ДЦ, наполняется своим приложением. Расхождение между ДЦ складывается из отставания источника, задержки инвалидации, политики свежести и последствий отказов; TTL — одно слагаемое из четырёх.
Три способа растянуть кэш
Растягивают кэш одним из трёх способов, и они настолько разные, что общего у них только слово «растянуть».
- Один кластер на два ДЦ, узлы поровну. Самая убедительная на схеме: три коробочки слева, три справа, разрыв посередине — и кажется, что каждая половина продолжит работать.
- Мастер в одном ДЦ, реплика в другом. Прямая калька с базы: там реплика в соседнем ДЦ, и здесь пусть будет.
- Приложение в обоих ДЦ ходит в один кэш. Формально это даже не растягивание кластера — просто один адрес на две площадки.
Разбирать их приходится по отдельности, потому что ломаются они в разных местах. Но растут все три из одной мысли, и с неё стоит начать.
Аналогия, из которой всё растёт
Довод в пользу растягивания обычно не проговаривают целиком: он кажется самоочевидным. Полностью он звучит так: база у нас растянута на два датацентра репликами, работает годами, никто не жалуется — значит, и с кэшем так можно.
Это не глупый довод. Он опирается на настоящий и удачный опыт: растянутая база действительно работает, и инженер, который её построил, имеет право на обобщение. Обобщение просто оказывается неверным — и не в мелочи, а во всех трёх местах, где оно опирается на слово «так же».
У базы и кэша разные задачи. У них разная цена потери одной записи. И слово «растянуть» означает для них разные вещи. Дальше — по одному различию на раздел, и на каждое есть замер.
Оговорка: на чём снято и что из этого переносится
Все числа этой статьи сняты на Redis 7.0.15, а сказанное дальше относится и к Valkey, и к текущим версиям самого Redis. Что при этом переносится, а что нет.
Valkey — форк Redis, и он сам об этом говорит:
Valkey is a fork of the open-source Redis (REmote DIctionary Server) database created in 2009 by the Italian hacker Salvatore "antirez" Sanfilippo
(Valkey — форк открытой базы данных Redis (REmote DIctionary Server), созданной в 2009 году итальянским хакером Сальваторе „antirez“ Санфилиппо).
Это разрешает перенести не числа, а договор: там, где документация Valkey
описывает то же поведение теми же словами, утверждение статьи держится на
цитате из неё, а не на аналогии.
Ссылаться на общее прошлое, однако, мало: с версией поведение меняется, и проверять это надо, а не предполагать. Поэтому все содержательные утверждения статьи прогнаны заново на Redis 8.10.1 и Valkey 9.1.2 — таблица в отдельном разделе ниже. Ни одно не разошлось.
А числа не переносятся никуда. Здесь они и не нужны как абсолютные значения: содержательна кратность, знак и то, какая половина кластера ответила, а какая нет.
Различие первое: у кэша и базы разные задачи
База — источник истины. Её нельзя потерять, и ради этого не жалко платить кругом между датацентрами: строка, доехавшая до второго ДЦ за сорок миллисекунд, всё равно дешевле строки, которой нет нигде.
Кэш нужен затем, чтобы повторное получение данных обходилось дешевле обращения к источнику. «Дешевле» — не обязательно «быстрее»: это может быть и снятая нагрузка с источника, и несделанный дорогой пересчёт, и не потраченный лимит внешнего вызова, и сглаженный всплеск, и защита горячего ключа. Свойство в любом случае сравнительное, а не абсолютное, и потому его можно потерять, не сломав ни одной настройки, — просто передвинув кэш дальше источника.
Дальше измерена одна из этих сторон — задержка, — и измерена на самом невыгодном для кэша случае.
7. КЭШ В ЧУЖОМ ДЦ МЕДЛЕННЕЕ БАЗЫ В СВОЁМ
------------------------------------------------------------------
медиана, мс p95, мс
кэш в своём ДЦ 0.10 0.14
база в своём ДЦ 0.09 0.13
кэш в чужом ДЦ 41.09 41.36
кэш в чужом ДЦ медленнее базы в своём примерно в 450 раз
Здесь ломается третий способ — «приложение в обоих ДЦ ходит в один кэш», — но ломается он не вообще, а для этого класса запросов. Половина обращений пойдёт в чужой датацентр, заплатит круг и вернётся позже, чем вернулся бы поход в локальную базу. Если кэш стоял ради задержки на дешёвых чтениях, он её не уменьшает, а увеличивает — и вдобавок ставит между приложением и данными ещё одну систему, которая может отказать.
Чего этот замер не говорит, и это важнее самих чисел. Первые две строки почти совпали, и из этого не следует, что кэш не нужен. Здесь измерена самая дешёвая операция базы: пять тысяч строк, всё в памяти, чтение по первичному ключу, никакой конкуренции за соединения. Кэш ставят не против такого запроса, а против соединений, агрегатов по миллионам строк и тех же запросов, повторённых тысячу раз в секунду.
Из замера следует ровно одно, и это правило, а не число: выигрыш у кэша берётся из сравнения с источником, поэтому проверять надо не «быстрый ли Redis», а «дешевле ли поход в кэш той операции источника, которую он заменяет, на этом расстоянии». Для дешёвого чтения по ключу из прогретой таблицы ответ на сорока миллисекундах отрицательный. Для агрегата на сто миллисекунд — уже нет, и такой случай в этой статье не измерялся.
Различие второе: потеря строки и потеря записи стоят разного
Второй способ — мастер в одном ДЦ, реплика в другом — калькирует базу буквально. И первое, что замер показывает: расстояние видно.
1. ЗАПИСЬ ДОХОДИТ ДО РЕПЛИКИ НЕ СРАЗУ, И РАССТОЯНИЕ ЭТО ВИДНО
------------------------------------------------------------------
реплика в том же ДЦ: запись видна через, мс 1.1
реплика в другом ДЦ: запись видна через, мс 20.8
разница, мс 19.7
Само по себе отставание бедой не является. Клиент к этому моменту давно получил
OK: мастер отвечает, не дожидаясь реплики, и это не особенность настройки, а
объявленный договор.
Redis uses by default asynchronous replication, which being low latency and high
performance, is the natural replication mode for the vast majority of Redis use
cases.
Redis по умолчанию использует асинхронную репликацию, которая, будучи низколатентной и высокопроизводительной, является естественным режимом репликации для подавляющего большинства сценариев использования Redis.
So the master does not wait every time for a command to be processed by the
replicas, however it knows, if needed, what replica already processed what
command.
Итак, мастер не ждёт каждый раз, пока команда будет обработана репликами, однако он знает, если это потребуется, какая реплика какую команду уже обработала.
У Valkey та же страница говорит то же самое:
Valkey uses by default asynchronous replication
(Valkey по умолчанию использует асинхронную репликацию) — дальше слово в слово.
Заодно стоит убрать одно недоразумение, на котором держится половина споров: асинхронность — не отличие кэша от базы. У Postgres потоковая репликация по умолчанию тоже асинхронная, и последствия документация называет прямо:
If the primary server crashes then some transactions that were committed may not
have been replicated to the standby server, causing data loss.
Если основной сервер аварийно завершится, некоторые зафиксированные транзакции могут оказаться не реплицированными на резервный сервер, что приведёт к потере данных.
То есть окно потерь есть у обоих. Различаются они не наличием окна, а ценой попадания в него, и вот к ней мы и идём.
Бедой отставание становится в тот момент, когда реплику поднимают на место мастера, — то есть в том самом сценарии, ради которого её и держат в другом ДЦ.
2. ПЕРЕКЛЮЧЕНИЕ НА РЕПЛИКУ ТЕРЯЕТ ПОДТВЕРЖДЁННЫЕ ЗАПИСИ
------------------------------------------------------------------
записей подтверждено клиенту 200
разрыв сразу после записи:
оказалось на реплике 107
потеряно 93 (46.5%)
разрыв через секунду после записи:
оказалось на реплике 200
потеряно 0 (0.0%)
Читать надо обе строки сразу, иначе из первой уносится «теряется почти всё», а
из второй — «не теряется ничего». Не верно ни то ни другое: теряется то, что не
успело уехать к реплике. Ни одна из потерянных записей об ошибке не сообщила:
на все клиент получил OK.
Чем измеряется окно потерь, и почему не кругом
Из этого опыта напрашивается правило «под угрозой всё, что записано за последний круг до разрыва». Правило это ходит по статьям в таком виде, и оно неверно: круг здесь совпал с потерей случайно. Проверяется это изменением одного лишь ТЕМПА записи, при том же самом канале.
9. ОКНО ПОТЕРЬ ЗАДАЁТ ОТСТАВАНИЕ, А НЕ КРУГ
------------------------------------------------------------------
200 записей залпом, обрыв сразу:
подтверждено клиенту 200
оказалось на реплике 96
потеряно 104
master_repl_offset 6403
slave_repl_offset 2855
отставание, байт 3548
Два других темпа на том же канале дают другое: с паузой в 5 мс между записями теряется 3 записи при отставании 99 байт, а через секунду тишины — ноль при нулевом отставании. Канал один и тот же, круг тот же; меняется отставание, и вместе с ним потеря.
Отсюда правило в верном виде: под угрозой то, что в момент разрыва лежит
между master_repl_offset мастера и смещением реплики. Redis отдаёт оба
числа в INFO replication, и по их разнице видно, сколько вы потеряете, если
переключение случится прямо сейчас. Круг задаёт лишь нижнюю границу того, за
какое время отставание вообще может сойти к нулю; само отставание зависит ещё
от темпа записи, размера команд и от того, успевает ли реплика их применять.
Вот здесь аналогия с базой ломается второй раз. Окно потерь, как мы только что видели, есть и у базы. Разная у них не механика, а цена: для базы потерянный хвост — авария, строку никто не пересчитает, её нет нигде. Для кэша потеря записи — норма, ради которой кэш и устроен так, как устроен: промах, поход в источник, запись обратно. То есть машинерия, которая оправдывает растянутую базу, кэшу по умолчанию не нужна, — а заплатить за неё придётся.
Счёт можно увидеть прямо. WAIT — тот самый инструмент, которым обычно
предлагают «сделать репликацию синхронной».
3. WAIT ДЕЛАЕТ ЗАПИСЬ ДОРОЖЕ, НО НЕ ДЕЛАЕТ ЕЁ ТРАНЗАКЦИЕЙ
------------------------------------------------------------------
обычная запись, мс 0.24
запись с WAIT 1, мс 41.73
во сколько раз дороже 171.6
реплик подтвердило (из 20 записей по 1) 20
Сорок один и семь десятых миллисекунды вместо ноля целых двадцати четырёх сотых — это цена ожидания, добавленная к каждой записи. Из чего она складывается, по одной точке сказать нельзя — и догадка «это два круга» напрашивается сама собой. Тот же опыт на трёх расстояниях отвечает точнее, и отвечает не то.
10. ЦЕНА WAIT РАСТЁТ ВМЕСТЕ С РАССТОЯНИЕМ, И ЭТО ПРОВЕРЯЕТСЯ
------------------------------------------------------------------
в одну сторону круг обычная с WAIT 1 WAIT / круг
5 мс 10 мс 0.15 мс 10.90 мс 1.09
20 мс 40 мс 0.14 мс 41.52 мс 1.04
40 мс 80 мс 0.20 мс 81.40 мс 1.02
Последний столбец — единица на всех трёх расстояниях. То есть WAIT стоит
ровно один круг до реплики, и с расстоянием растёт один к одному: круг вырос
на 30 мс — цена выросла на 30,62; круг вырос на 40 — цена на 39,87. Сверх круга
остаётся меньше полутора миллисекунд, и это уже работа самого Redis, а не
расстояние.
Этот блок публикуется во второй редакции, и первая была неверна — причём неверна не выводом, а числами. В ней стояло: отношение к кругу 5,20 / 2,10 / 1,55, значит «фиксированное число кругов» не подтверждается, а сверх круга остаётся слагаемое в 42–44 мс, чем вызвано — замер не устанавливает. Вывод «с расстоянием цена растёт на один круг» был верен и тогда, потому что держался на разностях. А вот слагаемое оказалось не свойством Redis, а дефектом измерительного прибора — и это отдельный раздел, следующий.
Купленное же свойство слабее ожидаемого: WAIT сообщает, сколько реплик
подтвердило, но не отменяет запись, если их меньше. К моменту ответа запись на
мастере уже применена и уже видна читателям — откатывать нечем. Документация
говорит это прямо:
However WAIT is only able to ensure there are the specified number of
acknowledged copies in the other Redis instances, it does not turn a set of
Redis instances into a CP system with strong consistency: acknowledged writes
can still be lost during a failover, depending on the exact configuration of the
Redis persistence.
Однако WAIT способен лишь обеспечить наличие указанного числа подтверждённых копий на других экземплярах Redis; он не превращает набор экземпляров Redis в CP-систему со строгой согласованностью: подтверждённые записи всё ещё могут быть потеряны при переключении, в зависимости от конкретной настройки постоянного хранения Redis.
То есть за эти 171,6 раза покупается не гарантия, а отчёт о числе копий. Стоит ли она того — вопрос не про Redis, а про то, во что обходится повторное наполнение кэша; к нему статья возвращается в разделе о том, что делать.
Проверка прибора: чем на самом деле был канал между ДЦ
Этот раздел появился из внешнего разбора, и вопрос в нём стоял уровнем ниже всех прежних. Прежние спрашивали «что мы заключили из замера». Этот спросил что замер на самом деле моделировал.
Канал между датацентрами во всех замерах изображает не tc netem, а
ретранслятор в пользовательском коде: процесс слушает порт, читает порцию,
выжидает и передаёт дальше. Претензия была конкретная: задержка ставится на
каждую порцию, которую вернул recv(), а TCP границ сообщений не хранит.
Значит, измерялось не «Redis плюс фиксированная задержка распространения», а
«Redis плюс задержка, умноженная на то, на сколько порций разбился поток».
Сверить с netem, как просил разбор, в этой среде нельзя: ядро собрано без
CONFIG_NET_SCH_NETEM, и tc qdisc add ... netem отвечает
Error: Specified qdisc kind is unknown. Права тут ни при чём — дисциплины
очереди в ядре просто нет. Поэтому сверяться пришлось с тем, что доступно:
измерить сам прибор.
Первое, что он показал, — что подозревать надо было не то.
11. ЧТО СТОИТ САМ СТЕНД, ЕСЛИ ЗАДЕРЖКУ ВЫКРУТИТЬ В НОЛЬ
------------------------------------------------------------------
без канала вовсе: обычная запись, мс 0.20
без канала вовсе: запись с WAIT 1, мс 0.42
канал 0 мс, БЕЗ TCP_NODELAY (стенд, на котором снят блок 10):
обычная запись, мс 0.24
запись с WAIT 1, мс 44.00
канал 0 мс, С TCP_NODELAY (исправленный стенд):
обычная запись, мс 0.18
запись с WAIT 1, мс 0.70
цена прежнего стенда на одной записи, мс +43.30
Вот и происхождение «слагаемого в 42–44 мс», которое блок 10 нашёл и не смог
объяснить. Оно целиком принадлежит измерительному прибору. Redis ставит
TCP_NODELAY на своих соединениях сам; ретранслятор создаёт два новых
соединения, и на них флага не было. Дальше работает известная пара: алгоритм
Нейгла придерживает мелкую порцию до подтверждения предыдущей, а отложенное
подтверждение придерживается до сорока миллисекунд. Отсюда и ровно сорок с
небольшим, и их независимость от расстояния, и то, что они появлялись только
там, где по каналу шло несколько мелких сообщений подряд.
Теперь — исходная претензия разбора, тоже в числах. Сколько раз задержка применяется к одной операции:
12. СКОЛЬКО РАЗ ЗАДЕРЖКА ПРИМЕНЯЕТСЯ К ОДНОЙ ОПЕРАЦИИ
------------------------------------------------------------------
канал сериализующий, 20 мс в одну сторону, 20 раз SET+WAIT:
порций мастер → реплика вхолостую 0
порций реплика → мастер вхолостую 1
порций мастер → реплика с нагрузкой 40
порций реплика → мастер с нагрузкой 21
порций сверх фона, всего 60
то же на одну операцию 3.00
задержка применена, мс на операцию 60.0
измерено на операцию, мс 61.81
Три порции на операцию: сама запись, запрос подтверждения и подтверждение. Но на критическом пути их две, а не три — первые две идут в одну сторону и могли бы уйти вместе. У сериализующего ретранслятора они не уходят вместе: пока поток спит, он не читает следующую порцию. Цена этого измеряется прямо:
13. СЕРИАЛИЗУЮЩИЙ КАНАЛ ПРОТИВ КОНВЕЙЕРНОГО НА ТРЁХ РАССТОЯНИЯХ
------------------------------------------------------------------
в одну сторону круг сериализующий конвейерный разница
5 мс 10 мс 16.06 мс 11.06 мс +5.00
20 мс 40 мс 61.64 мс 41.48 мс +20.16
40 мс 80 мс 121.86 мс 81.36 мс +40.51
Разбор был прав: сериализующий канал стоит ровно на один перелёт в одну сторону дороже на каждом расстоянии. Не на случайную величину — на ту самую задержку, которую он применяет к порции.
И вот что тут поучительнее самой ошибки. Пока залипание в сорок миллисекунд сидело в обоих каналах, этой разницы не было видно вовсе: она тонула в артефакте, который был вчетверо больше её самой. Два дефекта стенда маскировали друг друга. Найти каждый по отдельности, глядя на итоговые миллисекунды, было нельзя — только разложив прибор на части.
Последнее — калибровка, и она же граница применимости:
14. КАЛИБРОВКА: КАКУЮ ЗАДЕРЖКУ КАНАЛ ДАЁТ НА «ЗАПРОС — ОТВЕТ»
------------------------------------------------------------------
GET напрямую, мс 0.10
через канал, GET мимо ретранслятора к тому же мастеру:
в одну сторону круг сериализующий конвейерный
5 мс 10 мс 10.81 мс 10.79 мс
20 мс 40 мс 40.98 мс 41.00 мс
40 мс 80 мс 81.10 мс 81.11 мс
Как читать после этого всю статью. Там, где за операцию по каналу проходит
одна порция в каждую сторону — блоки 7 и 1, — ретранслятор даёт ровно круг, и
числа можно брать как есть. Там, где по каналу идёт несколько сообщений подряд
— репликация под нагрузкой, конвейер команд, WAIT, — прибор добавлял своё, и
оба слагаемых теперь измерены и убраны. Погрешность третьего рода остаётся и
снять её нечем: у ретранслятора нет ни очередей, ни потерь, ни джиттера, ни
ограничения полосы. Это приближение задержки, а не эмулятор сети на уровне
пакетов, и абсолютные миллисекунды на потоковых операциях надо читать именно
так.
Качественные выводы — знак, кратность, порядок событий, какая половина кластера ответила — от этого не зависят вовсе.
Различие третье: «растянуть» означает разное
Осталось самое главное — первый способ, «узлы поровну». И здесь аналогия ломается не в оценке цены, а в понимании самого слова.
Возьмём ту конфигурацию, которую обычно и имеют в виду, когда говорят «база растянута»: асинхронная горячая реплика Postgres в другом датацентре. Это отдельная система: читающая, отстающая, и приложение об этом знает. Она не голосует и не решает судьбу первой; при разрыве канала мастер продолжает работать, а реплика продолжает отставать, пока канал не вернётся.
Оговорка здесь обязательна, иначе получится ровно та ошибка, против которой
статья и написана. «База растянута» — не описание архитектуры: у Postgres есть
и синхронная репликация, при которой коммит ждёт удалённый standby
(if the standby is the last one in a synchronous group, setting this to on will result in commits waiting for the standby to confirm receipt
(если резервный сервер последний в синхронной группе, значение on приведёт к тому, что фиксации будут ждать подтверждения приёма от резервного)),
а бывают и базы с кворумом. Сравнивать надо не «базу» с «кэшем», а конкретную
модель репликации с конкретной моделью репликации — с ответами про
синхронность, про то, кто принимает решение о переключении, и про поведение при
разрыве. Ниже сравнивается асинхронная реплика Postgres с кластером Redis, и
оба конца названы поимённо не для формальности.
Кластер Redis в двух датацентрах — это одна система, принимающая решения большинством голосов. Условие, при котором она переживает разрыв, записано в спецификации, и требований в нём два, а не одно:
Redis Cluster is able to survive partitions where the majority of the master
nodes are reachable and there is at least one reachable replica for every master
node that is no longer reachable.
Кластер Redis способен пережить разделения сети, при которых доступно большинство мастер-узлов и для каждого недоступного мастер-узла доступна хотя бы одна реплика.
Первое требование помнят все — «нужно большинство». Второе читают по диагонали,
и вся статья, по существу, про него. У Valkey формулировка та же:
Valkey Cluster is able to survive partitions where the majority of the primary nodes are reachable and there is at least one reachable replica for every primary node that is no longer reachable
(Кластер Valkey способен пережить разделения сети, при которых доступно большинство первичных узлов и для каждого недоступного первичного узла доступна хотя бы одна реплика).
Прежде чем дойти до расклада «поровну», стоит посмотреть на расклад, который кажется самым осторожным: все мастера в одном ДЦ, все реплики во втором.
4. РАСКЛАД A: ВСЕ МАСТЕРА В ОДНОМ ДЦ, РЕПЛИКИ ВО ВТОРОМ
------------------------------------------------------------------
── опыт на свежем кластере, наблюдается ДЦ-1 (мастера)
ДЦ-1: [46073, 38365, 48713]
46073: мастер
38365: мастер
48713: мастер
ДЦ-2: [54531, 48957, 34965]
54531: реплика 46073
48957: реплика 38365
34965: реплика 48713
ДЦ-1 (мастера): 3 мастеров из 3 узлов
узел 46073: состояние ok GET -> ответил
узел 38365: состояние ok GET -> отдал слот другому узлу
узел 48713: состояние ok GET -> отдал слот другому узлу
роли на наблюдаемой половине не изменились
── опыт на свежем кластере, наблюдается ДЦ-2 (реплики)
ДЦ-1: [42971, 44779, 43549]
42971: мастер
44779: мастер
43549: мастер
ДЦ-2: [47655, 34721, 52771]
47655: реплика 42971
34721: реплика 44779
52771: реплика 43549
право реплик наблюдаемой половины повыситься, до разрыва:
узел 47655: канал up, смещение 201, validity-factor 10
узел 34721: канал up, смещение 163, validity-factor 10
узел 52771: канал up, смещение 443, validity-factor 10
ДЦ-2 (реплики): 0 мастеров из 3 узлов
узел 47655: состояние fail GET -> CLUSTERDOWN
узел 34721: состояние fail GET -> CLUSTERDOWN
узел 52771: состояние fail GET -> CLUSTERDOWN
роли на наблюдаемой половине не изменились
ДЦ-1 работает, ДЦ-2 не отвечает ничем. Реплики не стали мастерами: для
повышения нужен голос большинства мастеров — Once the replica receives ACKs from the majority of masters, it wins the election
(Как только реплика получает подтверждения от большинства мастеров, она выигрывает выборы), —
а все мастера остались по ту сторону обрыва.
Это первый результат, который стоит забрать с собой: вторая половина не запасная. При потере первой она первую не заменит — во всяком случае, не сама.
Кульминация: отказали обе половины
Теперь расклад, ради которого всё и затевается. Шесть узлов, по три в каждом датацентре, мастера поделены два и один. На схеме это выглядит симметрично, и именно симметрия убеждает.
5. РАСКЛАД B: УЗЛЫ ПОРОВНУ, МАСТЕРА ПОДЕЛЕНЫ 2 И 1
------------------------------------------------------------------
── опыт на свежем кластере, наблюдается ДЦ-1 (2 мастера)
ДЦ-1: [52467, 45191, 54701]
52467: мастер
45191: мастер
54701: реплика 52467
ДЦ-2: [35445, 32777, 38349]
35445: мастер
32777: реплика 45191
38349: реплика 35445
право реплик наблюдаемой половины повыситься, до разрыва:
узел 54701: канал up, смещение 201, validity-factor 10
ДЦ-1 (2 мастера): 2 мастеров из 3 узлов
узел 52467: состояние fail GET -> CLUSTERDOWN
узел 45191: состояние fail GET -> CLUSTERDOWN
узел 54701: состояние fail GET -> CLUSTERDOWN
роли на наблюдаемой половине не изменились
── опыт на свежем кластере, наблюдается ДЦ-2 (1 мастер)
ДЦ-1: [35315, 47793, 43215]
35315: мастер
47793: мастер
43215: реплика 35315
ДЦ-2: [49019, 40491, 48663]
49019: мастер
40491: реплика 47793
48663: реплика 49019
право реплик наблюдаемой половины повыситься, до разрыва:
узел 40491: канал up, смещение 163, validity-factor 10
узел 48663: канал up, смещение 443, validity-factor 10
ДЦ-2 (1 мастер): 1 мастеров из 3 узлов
узел 49019: состояние fail GET -> CLUSTERDOWN
узел 40491: состояние fail GET -> CLUSTERDOWN
узел 48663: состояние fail GET -> CLUSTERDOWN
роли на наблюдаемой половине не изменились
Кэша не стало нигде. Ни в одном из двух датацентров, ни по одному ключу.
Прежде чем разбирать почему, стоит сказать, как этот результат получен, потому
что метод здесь был исправлен после разбора. Раньше обе половины проверялись
подряд на одном кластере: остановили дальнюю, спросили ближнюю, отпустили,
остановили ближнюю, спросили дальнюю. Так делать нельзя. После первой проверки
топология не обязана вернуться к исходной: реплика могла повыситься,
configEpoch — смениться, слоты — переехать, и вторая половина мерилась бы уже
на другом кластере. Две последовательные проверки — не две стороны одного
разрыва. Теперь каждая сторона наблюдается на своём свежем кластере с той же,
заданной руками топологией.
Разберём, почему проиграл ДЦ-2, — это как раз ожидаемо. У него один мастер из
трёх, большинства нет, и спецификация не оставляет вариантов:
Redis Cluster is not available in the minority side of the partition
(Кластер Redis недоступен на той стороне разделения, где меньшинство).
Так и должно быть: если бы меньшинство продолжало обслуживать записи, кластер
разъехался бы на два расходящихся набора данных.
Интересен ДЦ-1. У него большинство мастеров — два из трёх. Голосов на
повышение реплики ему хватает. И всё равно CLUSTERDOWN.
Причина видна на карте выше, и её стоит проследить пальцем. Мастер, оставшийся в ДЦ-2, — узел 35445. Его реплика — узел 38349 — тоже осталась в ДЦ-2. Значит, в ДЦ-1 некого повышать: слоты этого мастера не покрыты ни одним доступным узлом. Это ровно то второе требование из спецификации, которое читают по диагонали.
Дальше срабатывает настройка, из-за которой отказ становится тотальным, а не частичным:
If this is set to yes, as it is by default, the cluster stops accepting writes
if some percentage of the key space is not covered by any node. If the option is
set to no, the cluster will still serve queries even if only requests about a
subset of keys can be processed.
Если установлено yes, как по умолчанию, кластер перестаёт принимать записи, если некоторая часть пространства ключей не покрыта ни одним узлом. Если параметр установлен в no, кластер продолжит обслуживать запросы, даже если может обработать запросы лишь о части ключей.
Отсюда напрашивается вывод, что наблюдаемое поведение «шире написанного»:
документация говорит про записи, а GET тоже получил CLUSTERDOWN. Вывод
неверный. Про чтения отвечает другая настройка, и она тоже описана —
cluster-allow-reads-when-down со значением no по умолчанию. Цепочка такая: неполное покрытие слотов →
кластер помечен отказавшим → узел отказавшего кластера не обслуживает и
чтения.
Проверяется это тем же раскладом, прогнанным дважды.
8. ЧТЕНИЯ В ОТКАЗАВШЕМ КЛАСТЕРЕ РЕШАЕТ ОТДЕЛЬНАЯ НАСТРОЙКА
------------------------------------------------------------------
cluster-allow-reads-when-down no (по умолчанию):
узел 43151: master состояние fail 20 ключей -> 20 CLUSTERDOWN
узел 47729: master состояние fail 20 ключей -> 20 CLUSTERDOWN
узел 35021: slave состояние fail 20 ключей -> 20 CLUSTERDOWN
SET на том же узле -> CLUSTERDOWN
cluster-allow-reads-when-down yes:
узел 54835: master состояние fail 20 ключей -> 5 ответил, 15 перенаправил
узел 46259: master состояние fail 20 ключей -> 9 ответил, 11 перенаправил
узел 48093: slave состояние fail 20 ключей -> 20 перенаправил
SET на том же узле -> CLUSTERDOWN
И вот что из этого следует на самом деле — оно интереснее исправленной ошибки.
Со значением yes узел отвечает только по тем ключам, чьи слоты остались за
ним: из двадцати выжившая половина отдала четырнадцать, а остальные
перенаправила на мастер, который сейчас за оборванным каналом. Записи отказаны
в обоих случаях.
То есть настройка не возвращает кэш. Она превращает «не работает ничего» в «работает часть чтений на части пространства ключей», и треть ключей — слоты мастера за обрывом — недоступна при любом её значении.
Правило, ради которого замер и поставлен. Большинство голосов необходимо, но недостаточно. Полное условие состоит из трёх частей, и третью обычно не называют вовсе:
большинство мастеров на своей стороне
+ доступная реплика каждого потерянного мастера
+ эта реплика достаточно свежа, чтобы ей разрешили повышаться
Третья часть — не придирка: свежесть реплики проверяется настройкой
cluster-replica-validity-factor, и реплика, отставшая сверх допустимого,
выборы не начнёт. В опытах выше она не мешала, и это видно в строках «право
реплик повыситься»: у всех реплик канал up ещё до разрыва. Но условие из
спецификации без третьей части неполно, и полагаться на две из трёх — значит
проверять не то.
При раскладке «поровну» условие выполняется не всегда — и, что важнее, проверить его глазами по схеме из трёх коробочек нельзя: на схеме видно число узлов, а не то, чья реплика где стоит. Кластер назначает реплики сам, при создании и при каждом последующем переключении.
Вот отсюда и берётся главный вывод. Растягивают ради симметрии — «чтобы пережили оба». Симметрия и есть то, что убивает: у ровно поделённого кластера условие выживания не выполняется ни у кого, и в самый нужный момент кэша не оказывается в обоих датацентрах сразу.
Расклад, который выживает, и что он на самом деле выбирает
Условие можно выполнить руками: оставить в ДЦ-1 не свою реплику, а реплику того мастера, который уезжает в ДЦ-2.
6. РАСКЛАД C: ТО ЖЕ ДЕЛЕНИЕ, НО РЕПЛИКА ЧУЖОГО МАСТЕРА ДОМА
------------------------------------------------------------------
── опыт на свежем кластере, наблюдается ДЦ-1 (2 мастера + реплика третьего)
ДЦ-1: [46819, 40665, 36267]
46819: мастер
40665: мастер
36267: реплика 53509
ДЦ-2: [53509, 39129, 47199]
53509: мастер
39129: реплика 46819
47199: реплика 40665
право реплик наблюдаемой половины повыситься, до разрыва:
узел 36267: канал up, смещение 443, validity-factor 10
ДЦ-1 (2 мастера + реплика третьего): 3 мастеров из 3 узлов
узел 46819: состояние ok GET -> ответил
узел 40665: состояние ok GET -> отдал слот другому узлу
узел 36267: состояние ok GET -> отдал слот другому узлу
что изменилось в ролях за время разрыва:
узел 36267: реплика 53509 -> мастер
Теперь ДЦ-1 выживает: и голосов хватило, и было кого повысить. Последняя строка
показывает это прямо — реплика, привезённая домой, стала мастером и приняла
слоты потерянного. Все три узла перешли в состояние ok.
Но симметрии не появилось. В ДЦ-2 кэша по-прежнему нет, и это не дефект расклада, а его смысл. Такой раскладкой вы выбираете, какой датацентр переживёт разрыв.
Границу этого вывода стоит очертить точно, иначе он превратится в лозунг. Утверждение не «оба датацентра не могут работать», а вот такое: в одном обычном кластере Redis нельзя получить симметричный разрыв, при котором обе половины независимо продолжают принимать записи, а потом автоматически сливаются обратно в один кластер. Причина не в раскладке, а в том, что кластер один и решение он принимает одно.
Другие исходы возможны — просто это уже не «один растянутый кластер»: два
независимых кэша, работа только на чтение в урезанном виде, продукт с
active-active и явной моделью разрешения конфликтов
(Conflict resolution is handled by conflict-free replicated data types (CRDTs)
(Разрешение конфликтов выполняется бесконфликтными реплицируемыми типами данных (CRDT))),
разрешение конфликтов на стороне приложения. У каждого своя цена, и ни один из
них эта статья не измеряла.
Заодно видно, насколько хрупка эта конструкция. Между блоком 5 и блоком 6 одна перестановка реплики. Она нигде не записана как требование, не проверяется при старте и не переживает автоматическое переключение: после первого же отказа мастера роли перераспределятся, и «правильный» расклад станет тем, что в блоке 5, — молча.
То же самое на текущих сборках
Все числа выше сняты на Redis 7.0.15. Ветка старая: к сентябрю 2026 актуальная линия Redis Open Source — 8.10.x, а Valkey ушёл в 9.1. Кластерная часть между Redis 7 и 8 менялась заметно, и «совпало однажды» не делает свойство неизменным. Поэтому те же утверждения прогнаны на всех трёх сборках — не производительность, а именно утверждения.
15. ТЕ ЖЕ УТВЕРЖДЕНИЯ НА ТЕКУЩИХ СБОРКАХ
------------------------------------------------------------------
Redis 7.0.15 Redis 8.10.1 Valkey 9.1.2
версия 7.0.15 8.10.1 9.1.2
обычная запись, мс 0.17 0.18 0.18
мастер ждёт реплику нет нет нет
видна на реплике через, мс 21.4 21.9 21.1
WAIT 1, мс 41.46 41.48 41.29
WAIT / круг 1.04 1.04 1.03
потеряно из 200 при разрыве 72 106 116
расклад B: состояние половины с большинством fail fail fail
расклад B: ответов на GET нет нет нет
reads-when-down no: отдано ключей из 60 0 0 0
reads-when-down yes: отдано ключей из 60 15 15 15
Ни одно утверждение статьи не разошлось: мастер не ждёт реплику нигде, WAIT
везде стоит круг, половина с большинством мастеров везде отказывает целиком, и
судьбу чтений везде решает cluster-allow-reads-when-down. Заодно закрыт
вопрос про Valkey: раньше статья опиралась на его документацию и честно
говорила, что на форке не мерила. Теперь мерила.
Чего эта таблица не делает. Она не переносит числа блоков 1–10 на новые версии и не заменяет их. Опубликованная основа остаётся на 7.0.15; таблица отвечает на другой вопрос — не разъехалось ли поведение.
Что делать вместо этого
Без этого раздела весь разбор бесполезен, поэтому по пунктам. Первый ответ — хороший умолчательный, а не единственный: в конце раздела перечислены случаи, где выбирают иначе.
Кэш в каждом ДЦ свой, независимый. Не реплика, не половина кластера — отдельная установка, которую наполняет своё приложение из своего источника. Разрыв канала тогда не событие для кэша: обе площадки продолжают работать, каждая со своими данными.
Оговорка к этому важна, и без неё получается лозунг. Независимые региональные кэши убирают WAN-зависимость из пути к кэшу, но не делают приложение целиком независимым от канала между ДЦ. Если запись всё равно идёт в мастер базы в другом датацентре, разрыв остаётся событием для приложения — просто перестаёт быть событием для кэша. Разносить эти две доступности надо явно, иначе «независимый кэш» превращается в обещание, которого он не давал.
Цена этого — лишние промахи. Первый запрос на ключ промахнётся в ДЦ-1 и сходит в источник, первый такой же запрос в ДЦ-2 промахнётся ещё раз. Но формулировать это надо аккуратно: в идеальном установившемся режиме, без вытеснения и перезапусков, минимальная дополнительная цена — отдельное наполнение каждого горячего ключа в каждом регионе. Это нижняя граница, а не оценка.
Реальную нагрузку на источник задаёт не арифметика ключей, а вытеснение, перезапуски, переключения, перешардирование, выкладки, смена версии ключа, инвалидация, холодный старт и переброс трафика между ДЦ. Каждое из этих событий наполняет кэш заново, и теперь — в каждом датацентре отдельно. Отсюда и то, чего у одного общего кэша не было: два независимых домена лавины, две разные доли попаданий и два набора графиков, за которыми надо следить.
Расхождение между ДЦ допустимо, и им управляют — но не одним только TTL. Два независимых кэша неизбежно разойдутся, и вопрос не «как этого избежать», а «на сколько секунд можно разойтись». TTL — самая прямая ручка в этом вопросе, и поэтому её легко принять за весь ответ — «TTL и есть бюджет рассогласования, названный в секундах». Это неверно, и вот из чего складывается наблюдаемое расхождение на самом деле:
- отставание самого источника. Если в каждом ДЦ своя читающая реплика базы, её отставание ложится под расхождение кэшей, а не вместо него;
- задержка инвалидации — сколько идёт до второго ДЦ событие «ключ изменился», если оно вообще идёт;
- политика свежести — собственно TTL, а с ним и его родня: обновление заранее, скользящий срок, отдача устаревшего значения на время пересчёта;
- последствия отказов и восстановления — после переключения или переброса трафика кэш в одном ДЦ холодный, а в другом прогретый.
TTL — одно слагаемое из четырёх. Называть бюджетом надо сумму, и считать её надо по своей схеме, а не по этой статье.
Синхронная инвалидация между датацентрами — дорогая сделка, и решать её надо не TTL. Соблазн есть: удалили ключ здесь — удалите и там, дождитесь подтверждения. Это возвращает круг, от которого мы только что ушли, и возвращает его в горячий путь записи. Для одноразового кэша сделка почти всегда плохая: локальность и была тем, ради чего кэш разносили по регионам.
Но если ждать всё-таки приходится, вывод «значит, TTL выбран неправильно» неверен, и неверен он не по мелочи, а по существу. Требование «подтверждаем запись только после того, как её увидят в другом регионе» — это требование к модели согласованности кэш-слоя, а не к сроку жизни ключа. Одним TTL оно не решается: нужна либо другая модель (версионирование, маршрутизация читателя к тому же региону, ограниченная устарелость с явной границей), либо кэш надо убрать с пути корректности. TTL управляет тем, на сколько можно разойтись, а не тем, можно ли разойтись вообще.
И WAIT не бессмыслен для кэша — он просто редко окупается. Категоричное
«для кэша WAIT бессмыслен» — ошибка того же рода, что и предыдущие две. Для
дёшево восстанавливаемого кэша платить круг на каждую запись действительно трудно
оправдать: потеря записи предусмотрена конструкцией. Но кэш кэшу рознь. Если
прогрев стоит часов, если источник не переживёт лавины холодного старта, если
за ним внешний API с лимитом или тяжёлый пересчёт, — надёжность репликации
приобретает цену, и сравнивать её надо не с нулём, а со стоимостью повторного
наполнения и с допустимым окном потерь. Промышленный разбор той же ловушки
говорит о ней без обиняков:
a cache can be a big and highly damaging blast radius
(кэш может быть большим и крайне разрушительным радиусом поражения).
Заметьте, что и сама эта статья ниже допускает удалённую реплику ради тёплого
кэша: это тот же аргумент, просто про другой инструмент.
Источник истины — не обязательно база, но он обязан быть. Ходовая формулировка — «всё, что нельзя пересчитать из базы, в кэше лежать не должно» — для системного разбора слишком узка. Правило шире: одноразовый кэш должен быть восстановим из авторитетного источника или из повторяемого вычисления. Авторитетным источником бывает база, объектное хранилище, журнал событий, внешний API, другая служба или канонический результат расчёта. Что именно — не важно; важно, что он есть и что путь восстановления известен.
Общий кэш остаётся кэшем, и «общий» — не улика. Ходовое правило звучит так: «если кэш обязан быть общим, значит, в нём лежит не кэш». Это не так. Общий внешний кэш, в который ходят несколько приложений, — нормальная и распространённая конструкция: он снимает нагрузку с источника лучше, чем набор процессных кэшей, избавляет от расхождения между узлами приложения и переживает выкладку приложения, не начиная с холодного.
Красный флаг не в слове «общий», а в невосстановимости. Кэш перестаёт быть кэшем, когда потеря записи превращается не в промах, а в потерю состояния: единственная копия сессии, блокировка, счётчик, состояние процесса, данные, которых больше нигде нет. Проверка на это простая и не требует спора о терминах: удалите кэш целиком и посмотрите, что произошло. Стало медленнее, данные восстановились — это кэш, и растягивать его через ДЦ незачем. Потерялись деньги, сессии, блокировки — это не кэш, а хранилище состояния, и разговор о нём другой; ему нужна отдельная статья, и она будет.
Шесть раскладов рядом, и чего они стоят
Всё, что выше, разбирает три способа растянуть кэш и один способ не растягивать. Способов больше, и держать их в голове списком неудобно — поэтому вот они рядом. Таблица не замер: в ней нет ни одного числа, и быть не может, потому что три из шести строк здесь никто не мерил. Это карта, по которой выбирают, что мерить дальше.
| Расклад | Задержка у себя | При разрыве между ДЦ | RPO содержимого кэша | RTO: когда снова тепло |
|---|---|---|---|---|
| Один кэш на два ДЦ, ходят оба | у дальней стороны плохая (замерено) | дальняя сторона теряет кэш | неприменимо: копия одна | сразу, если ближняя жива |
| Кластер Redis на два ДЦ | смешанная | зависит от кворума и покрытия слотов (замерено) | отставание реплики шарда | после выборов, если было кого повышать |
| Мастер здесь, реплика в чужом ДЦ | хорошая на стороне мастера | переключение вручную плюс окно потерь (замерено) | отставание репликации в момент разрыва | время ручного переключения |
| Свой кэш в каждом ДЦ | хорошая | кэш переживает разрыв | потеря одноразовая по устройству | прогрев своего региона |
| Кэш в процессе плюс региональный | лучшая | изоляция по регионам | два слоя, оба одноразовые | прогрев двух слоёв |
| Active-active с разрешением конфликтов | хорошая | на это и рассчитан | зависит от продукта и модели конфликтов | локальная доступность не теряется |
Читать эту таблицу надо не как рейтинг, а как список вопросов к своей схеме. Порядок вопросов такой: что именно кэшируется и можно ли это потерять; во сколько обходится та операция источника, которую кэш заменяет; какое расстояние до источника; какое расхождение допустимо; что должно пережить разрыв; сколько времени кэш греется заново. Ответы на них и выбирают строку — а не то, как конструкция выглядит на схеме.
Когда растянутый кэш всё-таки допустим
Статья написана против растянутого кэша, но не против всех его случаев. Есть четыре, где растянутая конфигурация защитима, — и все четыре опираются на те же замеры.
Когда разрыв должен пережить один назначенный датацентр, а не оба. Расклад C работает: ДЦ-1 ответил всеми тремя узлами. Если у вас есть основной ДЦ и резервный, а не два равноправных, то «кластер выживает в основном» — законная цель. Но три условия: во-первых, вы должны понимать, что выбираете победителя заранее; во-вторых, что в резервном ДЦ кэша не будет; в-третьих, что расклад реплик надо проверять после каждого переключения, потому что кластер расставляет их сам.
Когда реплика в чужом ДЦ нужна не для работы, а чтобы не наполнять кэш с
нуля. Большой прогретый кэш — это часы работы и заметная нагрузка на источник
при холодном старте; именно про это предупреждение о том, что служба
is likely to be unable to handle the resulting load on its dependencies
(вероятно, не справится с возникшей нагрузкой на свои зависимости).
Реплика, которую поднимают руками после аварии, эту нагрузку снимает. Условия
честные и оба измерены: реплики сами не повышаются (блок 4: три узла в
состоянии fail, ноль мастеров), и переключение теряет то, что не доехало —
под угрозой отставание в момент разрыва (блоки 2 и 9). Для кэша второе
допустимо: потерянные записи пересчитаются при промахе. Именно поэтому — только
для кэша, и только если вы согласны переключать руками.
Когда поток записи редкий. Вторая строка блока 2 — это не оговорка, а отдельный результат: через секунду тишины на реплике оказались все двести записей, потеряно ноль. Блок 9 показывает то же самое ещё раз и точнее: опасен не объём данных, а отставание, которое темп записи успевает создать. Если реплика в среднем догоняет, терять при переключении почти нечего — и проверить это можно, не гадая, по разнице смещений.
Когда операция источника дороже круга. Блок 7 сравнивает кэш в чужом ДЦ с самым дешёвым, что умеет база: чтением по первичному ключу из прогретой таблицы. Для агрегата, который считается сто миллисекунд, сравнение другое, и кэш за сорок миллисекунд его выигрывает. Правило из замера формулируется без чисел и работает в обе стороны: поход в кэш обязан быть дешевле той операции источника, которую он заменяет, на этом расстоянии. Числа для дорогих операций здесь не снимались, и приводить их было бы нечестно; но и запрещать такой кэш этот замер не даёт права.
Общее у всех четырёх — то, чего в них нет. Ни в одном не обещано, что обе половины одного кластера независимо продолжат принимать записи. Как только в требовании появляется это слово — «оба», — из перечисленного не годится ничего, и выбирать надо из другой строки таблицы выше: свой кэш в каждом ДЦ, слоистый кэш или продукт с active-active и явной моделью конфликтов.
Как воспроизвести числа
Восемь скриптов, все на живом Redis, все печатают то, что вернули процессы.
python3 bench/stretched-cache/replication.py # блоки 1-3
python3 bench/stretched-cache/cluster.py # блоки 4-6
python3 bench/stretched-cache/latency.py # блок 7
python3 bench/stretched-cache/readsdown.py # блок 8
python3 bench/stretched-cache/offsets.py # блок 9
python3 bench/stretched-cache/waitcost.py # блок 10
python3 bench/stretched-cache/relaycheck.py # блоки 11-14
python3 bench/stretched-cache/versions.py # блок 15
Нужен redis-server в PATH, для кластера ещё и redis-cli, для замера
задержки — работающий PostgreSQL (адрес в DE_TEST_DATABASE_URL). Другую
сборку можно подсунуть через REDIS_SERVER_BIN и REDIS_CLI_BIN — так снят
блок 15. Ничего после себя скрипты не оставляют: узлы поднимаются во временных
каталогах и убиваются в finally.
Ограничения стенда, названные прямо
Задержка создаётся ретранслятором в пользовательском коде, а не netem.
Ретранслятор добавляет задержку каждой порции, которую вернул recv(), а TCP
границ сообщений не хранит. Это воспроизводимое приближение задержки, а не
эмулятор сети на уровне пакетов: у него нет ни очередей, ни потерь, ни
джиттера, ни ограничения полосы. Простые измерения вида «запрос — ответ» можно
читать напрямую — блок 14 показывает, что там ретранслятор даёт ровно круг.
Потоковую репликацию и WAIT желательно подтверждать отдельно через netem
или сетевые пространства имён; в среде, где снималась эта статья, ядро собрано
без CONFIG_NET_SCH_NETEM, и сделать это было нечем. Вместо сверки измерен сам
прибор — блоки 11–14, — и два его дефекта найдены и устранены.
Разрыв в кластерном замере изображается сигналом STOP. Процесс жив, порт
открыт, но узел не отвечает и не шлёт heartbeat: для оставшейся половины он
неотличим от узла за оборванным каналом. Убить их было нельзя — вторую половину
надо было опросить тоже. Каждая сторона наблюдается на своём свежем
кластере с заданной руками топологией, поэтому последствия первой проверки не
достаются второй; настоящее разделение сети через файрвол или сетевые
пространства имён было бы ещё точнее.
У ретранслятора есть рубильник cut(), и он не декоративный: разрыв между
датацентрами — это не «стало медленно», а «перестало ходить вовсе», и
изображать его увеличенной задержкой нельзя.
Граница версий
Основные свойства сверены с актуальной документацией Redis и Valkey. Числа
основного прогона получены на Redis 7.0.15 и сохранены как воспроизводимая
основа. Redis 8.x заметно развил кластерную часть, поэтому ключевые
утверждения — репликация, WAIT, расклад B, cluster-allow-reads-when-down —
дополнительно прогнаны на Redis 8.10.1 и Valkey 9.1.2 (блок 15) и не
считаются неизменными только потому, что однажды совпали.
Опубликованный прогон: Redis 7.0.15, PostgreSQL 16.13, один хост, канал 20 мс в одну сторону — кроме блоков 10 и 13, где расстояние и есть переменная опыта. Двадцать миллисекунд выбраны не «чтобы было хуже», а как обычное расстояние между датацентрами в пределах одной страны.
Три скрипта появились после первого разбора статьи: они проверяют ровно те три
места, где вывод был сделан из одной точки, — чтения в отказавшем кластере,
окно потерь и цену WAIT. Ещё два появились после второго разбора и проверяют
не Redis, а сам замер. Из пяти проверок четыре нашли ошибку, и все четыре
исправления стоят в тексте на своих местах, а не задним числом.
Чем измерено
Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и как это читать.
bench/stretched-cache/replication.pybench/stretched-cache/cluster.pybench/stretched-cache/latency.pybench/stretched-cache/readsdown.pybench/stretched-cache/offsets.pybench/stretched-cache/waitcost.pybench/stretched-cache/relaycheck.pybench/stretched-cache/versions.pybench/stretched-cache/link.py
Исправления
12 сентября 2026Здесь собрано то, что в этом материале было сказано неверно, и то, чем это снято. Каждая запись держится на прогоне или на документе, а не на том, что мы передумали.
«Под угрозой всё, что записано за последний круг до разрыва».
Под угрозой то, что в момент разрыва лежит между master_repl_offset мастера и смещением реплики. Круг задаёт лишь нижнюю границу того, за какое время отставание может сойти к нулю.
Три темпа записи на ОДНОМ и том же канале: залпом — 104 потерянные записи при отставании 3548 байт, с паузой 5 мс — 3 записи при 99 байтах, через секунду тишины — ноль при нулевом отставании. Круг во всех трёх одинаков, потеря разная (bench/stretched-cache/offsets.py).
У цены WAIT сверх круга находилось «слагаемое в 42–44 мс, одинаковое на всех расстояниях», и происхождение его объявлялось неустановленным.
Слагаемого нет. Оно принадлежало измерительному прибору: канал между ДЦ изображает ретранслятор в пользовательском коде, и на двух его соединениях не был выставлен TCP_NODELAY. Пара «алгоритм Нейгла — отложенное подтверждение» добавляла ровно сорок миллисекунд.
Опыт без расстояния вовсе: через канал с НУЛЕВОЙ задержкой запись с WAIT 1 стоила 44,00 мс без флага и 0,70 мс с ним. Отдельно нашёлся второй дефект того же стенда — сериализация потока добавляла ещё один перелёт, и увидеть её удалось только после того, как убрали залипание вчетверо большее (bench/stretched-cache/relaycheck.py).
Про отказ кластера на чтениях было сказано, что наблюдаемое поведение «шире написанного»: документация говорит про записи, а GET тоже получил CLUSTERDOWN.
Не шире. За чтения отвечает отдельная настройка cluster-allow-reads-when-down со значением no по умолчанию, и она тоже описана. Цепочка: неполное покрытие слотов → кластер помечен отказавшим → узел отказавшего кластера не обслуживает и чтения.
Замер на обеих настройках: с no все 20 ключей из 20 получают CLUSTERDOWN на каждом узле выжившей половины (bench/stretched-cache/readsdown.py).
Обе половины разрезанного кластера проверялись по очереди на одном и том же кластере.
Каждая половина проверяется на своём свежем кластере с одинаковой топологией.
Повышение реплики в первой проверке меняет топологию для второй, и второй ответ относится уже к другому кластеру. Это исправление МЕТОДА, а не числа: вывод после него не изменился, но до него он держался на некорректном опыте (bench/stretched-cache/cluster.py).
«Если кэш обязан быть общим, значит, в нём лежит не кэш».
Общий внешний кэш — нормальная конструкция: он лучше снимает нагрузку с источника, чем набор процессных кэшей, избавляет от расхождения между узлами приложения и переживает выкладку. Красный флаг не «общий», а невосстановимость.
Проверка, которая разводит эти два случая, стоит в тексте: удалите кэш целиком. Стало медленнее, данные восстановились — это кэш. Потерялись сессии, блокировки, счётчики — это хранилище состояния, и разговор о нём другой.
«TTL — это бюджет рассогласования, названный в секундах».
TTL — одно слагаемое из трёх. Под наблюдаемое расхождение ложатся ещё отставание самого источника и задержка инвалидации, и ни одно из них TTL не сокращает.
Отставание источника измерено отдельно: реплика в другом ДЦ видит запись через 20,8 мс против 1,1 мс в своём (bench/stretched-cache/replication.py). Это слагаемое лежит под расхождением кэшей независимо от того, какой TTL выбран.
«Всё, что нельзя пересчитать из базы, в кэше лежать не должно».
Одноразовый кэш должен быть восстановим из авторитетного источника или из повторяемого вычисления. Авторитетным источником бывает база, объектное хранилище, журнал событий, внешний API, другая служба или канонический результат расчёта.
Это сужение формулировки, а не замер: прежняя формулировка исключала конструкции, которые работают, — например, кэш перед внешним API. Важно не то, откуда восстанавливается запись, а то, что путь восстановления есть и известен.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Довод звучит разумно: «база у нас растянута на два датацентра — растянем и кэш». Аналогия ложная, и ломается она в трёх разных местах сразу.
- Кэш держится сравнительным свойством: поход в него должен обходиться дешевле той операции источника, которую он заменяет. На дешёвом чтении по ключу за каналом между ДЦ этого не остаётся: попадание в кэш в чужом ДЦ стоит 41,09 мс, чтение той же строки из базы под рукой — 0,09 мс.
- Мастер не ждёт реплику. Разрыв сразу после залпа записей потерял 93 записи из 200; клиенту на все двести пришёл
OK. Это не сбой, а объявленный договор асинхронной репликации. WAITпокупает не то, за что его принимают: запись дорожает в 171,6 раза, а отменить её нечем — к моменту ответа она уже применена. Цена — ровно круг до реплики, и это проверено на трёх расстояниях.- Окно потерь задаёт отставание, а не круг. На одном и том же канале разный темп записи даёт 104, 3 и 0 потерянных записей; смотреть надо на разницу смещений в
INFO replication. - Главное. Кластер из шести узлов, разложенный по датацентрам поровну, при разрыве отказал в обеих половинах — включая ту, у которой было большинство мастеров. Каждая половина проверена на своём свежем кластере. Большинство голосов необходимо, но недостаточно.
- Проверен и сам измерительный стенд, и это стоило одного числа. В прежней версии у
WAITнаходилось «слагаемое в 42–44 мс неизвестной природы». Природа выяснилась: у ретранслятора, изображавшего канал, не был выставленTCP_NODELAY. Прибор показывал сорок миллисекунд собственного залипания. - Те же утверждения проверены на Redis 8.10.1 и Valkey 9.1.2. Ни одно не разошлось.
- Что делать. Умолчание — свой кэш в каждом ДЦ, наполняется своим приложением. Расхождение между ДЦ складывается из отставания источника, задержки инвалидации, политики свежести и последствий отказов; TTL — одно слагаемое из четырёх.
На самом деле
- Аналогия ломается в трёх местах сразу. Задачи разные: база — источник истины, ради которого не жалко платить круг между ДЦ, а кэш нужен затем, чтобы повторное получение данных обходилось ДЕШЕВЛЕ обращения к источнику. «Дешевле» — это и задержка, и снятая нагрузка, и несделанный пересчёт, и лимит внешнего вызова; свойство в любом случае сравнительное. Замерена одна сторона — задержка — и на самом невыгодном для кэша случае: 41,09 мс в чужом ДЦ против 0,09 мс до базы под рукой. Цена потери разная: потеря строки базы — авария, потеря записи кэша — предусмотренный промах. И само слово «растянуть» означает разное: асинхронная горячая реплика Postgres — отдельная отстающая система, а кластер Redis на два ДЦ — одна система, принимающая решения большинством голосов. Сравнивать надо конкретную модель репликации с конкретной: у Postgres есть и синхронная репликация, бывают и базы с кворумом.
- Замерено обратное: при разрыве канала отказали ОБЕ половины. Шесть узлов, по три в каждом ДЦ, мастера поделены 2 и 1 — и все шесть узлов ответили
CLUSTERDOWN. Каждая половина проверена на своём свежем кластере с той же топологией, а не двумя проверками подряд на одном: последствия первой проверки не могли достаться второй. У ДЦ-1 было большинство мастеров и голосов на повышение хватало, но реплика мастера, оставшегося в ДЦ-2, тоже осталась в ДЦ-2 — повышать было некого. Спецификация называет два требования, а не одно: the majority of the master nodes are reachable and there is at least one reachable replica for every master node that is no longer reachable. Большинство необходимо, но недостаточно — и полное условие состоит даже не из двух частей, а из трёх: третья в том, что реплика должна быть достаточно свежей, иначеcluster-replica-validity-factorне даст ей начать выборы. - Не поднимутся. В раскладе «все мастера в ДЦ-1, все реплики в ДЦ-2» после разрыва в ДЦ-2 оказалось ноль мастеров из трёх узлов, состояние всех трёх —
fail, наGETвсе три вернулиCLUSTERDOWN. Для повышения реплике нужен голос большинства мастеров — Once the replica receives ACKs from the majority of masters, it wins the election, — а все мастера остались по ту сторону обрыва. Вторая половина не запасная: при потере первой она её не заменит, во всяком случае не сама. - Нельзя: на схеме видно число узлов в каждом ДЦ, а не то, чья реплика где стоит. Между блоком 5 (отказали обе половины) и блоком 6 (ДЦ-1 выжил) разница — одна перестановка реплики. Она нигде не записана как требование, не проверяется при старте и не переживает автоматическое переключение: после первого же отказа мастера кластер расставит роли сам, и «правильный» расклад молча станет тем, что в блоке 5.
- Замерено: обычная запись 0,24 мс, запись с
WAIT 1— 41,73 мс, дороже в 171,6 раза. Цена — РОВНО ОДИН круг до реплики: на расстояниях 5, 20 и 40 мс в одну сторону она равна 10,90, 41,52 и 81,40 мс, а отношение к кругу — 1,09, 1,04 и 1,02. Купленное же свойство слабее ожидаемого: к моменту ответа запись на мастере уже применена и уже видна читателям, откатить её нечем. Документация говорит это прямо: it does not turn a set of Redis instances into a CP system with strong consistency: acknowledged writes can still be lost during a failover. Для дёшево восстанавливаемого кэша сделка обычно не окупается: он платит круг на каждую запись за данные, потеря которых предусмотрена промахом. Но не «бессмысленна»: если прогрев кэша стоит часов или источник не переживёт лавины холодного старта, надёжность репликации имеет цену, и сравнивать её надо со стоимостью повторного наполнения и допустимым окном потерь. - Отказ тотальный: в замере
GETвернулCLUSTERDOWNна каждом узле выжившей половины, а не только по слотам потерянного мастера. Отсюда напрашивается вывод, что наблюдаемое поведение «шире написанного». Вывод неверен:cluster-require-full-coverageотвечает за записи (If this is set to yes, as it is by default, the cluster stops accepting writes if some percentage of the key space is not covered by any node), а за чтения отвечает отдельная настройкаcluster-allow-reads-when-downсо значениемnoпо умолчанию, и она тоже описана. Цепочка такая: неполное покрытие слотов → кластер помечен отказавшим → узел отказавшего кластера не обслуживает и чтения. Замерено на обеих настройках: сnoвсе 20 ключей из 20 получаютCLUSTERDOWNна каждом узле. - Не переживёт. Со значением
yesузел отвечает только по ТЕМ ключам, чьи слоты остались за ним: в замере из двадцати ключей выжившая половина отдала четырнадцать (5 на одном мастере, 9 на другом), остальные перенаправила на мастер, который сейчас за оборванным каналом, — и туда клиент не доедет. Реплика в той же половине перенаправила все двадцать. Записи отказаны при обоих значениях настройки. То естьyesпревращает «не работает ничего» в «работает часть чтений на части пространства ключей», а треть пространства — слоты мастера за обрывом — недоступна при любом значении. - Простит, пока реплика остаётся репликой. Отставание становится потерей ровно в тот момент, ради которого реплику и держат, — при переключении. Замерено: разрыв сразу после залпа из двухсот записей потерял 93 из них (46,5%), и ни на одну клиент не получил ошибки — на все пришёл
OK. Разрыв через секунду тишины не потерял ничего. Ходовое правило «под угрозой всё, что записано за последний круг» неверно: круг совпал с потерей случайно. На одном и том же канале три темпа записи дают 104, 3 и 0 потерянных записей при отставании 3548, 99 и 0 байт. Под угрозой то, что в момент разрыва лежит междуmaster_repl_offsetмастера и смещением реплики; оба числа Redis отдаёт вINFO replication. Круг задаёт лишь нижнюю границу того, за какое время отставание может сойти к нулю. - Приложение в двух ДЦ — довод за два кэша, а не за один общий: половина обращений уйдёт в чужой ДЦ и заплатит круг. Но «общий кэш» сам по себе не улика, и ходовое правило «если кэш обязан быть общим, значит, в нём лежит не кэш» неверно. Общий внешний кэш, в который ходят несколько приложений, — нормальная конструкция: он лучше снимает нагрузку с источника, чем набор процессных кэшей, избавляет от расхождения между узлами приложения и переживает выкладку. Красный флаг не «общий», а НЕВОССТАНОВИМОСТЬ. Проверка простая: удалите кэш целиком и посмотрите. Стало медленнее, данные восстановились — это кэш, и растягивать его через ДЦ незачем. Потерялись сессии, блокировки, счётчики — это хранилище состояния, и разговор о нём другой.
- Не обязательно, и эта статья — свой собственный контрпример. У
WAITсверх круга находилось «слагаемое в 42–44 мс, одинаковое на всех расстояниях», и происхождение его долго оставалось неустановленным. Установлено оно оказалось не в Redis, а в измерительном приборе: канал между ДЦ изображает ретранслятор в пользовательском коде, и на двух его соединениях не был выставленTCP_NODELAY. Пара «алгоритм Нейгла — отложенное подтверждение» добавляла к каждой операции ровно сорок миллисекунд. Проверяется это опытом без расстояния вовсе: через канал с НУЛЕВОЙ задержкой запись сWAIT 1стоила 44,00 мс без флага и 0,70 мс с ним. Отдельно поучительно, что дефектов было два и они маскировали друг друга: сериализация потока в ретрансляторе добавляла ещё один перелёт, и увидеть её удалось только после того, как убрали залипание, которое было вчетверо больше.
Что разобрано
- Три способа растянуть кэш
- Аналогия, из которой всё растёт
- Оговорка: на чём снято и что из этого переносится
- Различие первое: у кэша и базы разные задачи
- Различие второе: потеря строки и потеря записи стоят разного
- Проверка прибора: чем на самом деле был канал между ДЦ
- Различие третье: «растянуть» означает разное
- Кульминация: отказали обе половины
- Расклад, который выживает, и что он на самом деле выбирает
- То же самое на текущих сборках
- Что делать вместо этого
- Шесть раскладов рядом, и чего они стоят
- Когда растянутый кэш всё-таки допустим
- Как воспроизвести числа
- Чем измерено
Расхожие заблуждения
База у нас растянута на два ДЦ и работает — значит, и кэш можно
Аналогия ломается в трёх местах сразу. Задачи разные: база — источник истины, ради которого не жалко платить круг между ДЦ, а кэш нужен затем, чтобы повторное получение данных обходилось ДЕШЕВЛЕ обращения к источнику. «Дешевле» — это и задержка, и снятая нагрузка, и несделанный пересчёт, и лимит внешнего вызова; свойство в любом случае сравнительное. Замерена одна сторона — задержка — и на самом невыгодном для кэша случае: 41,09 мс в чужом ДЦ против 0,09 мс до базы под рукой. Цена потери разная: потеря строки базы — авария, потеря записи кэша — предусмотренный промах. И само слово «растянуть» означает разное: асинхронная горячая реплика Postgres — отдельная отстающая система, а кластер Redis на два ДЦ — одна система, принимающая решения большинством голосов. Сравнивать надо конкретную модель репликации с конкретной: у Postgres есть и синхронная репликация, бывают и базы с кворумом.
Разложим узлы поровну между датацентрами — переживём потерю любого из них
Замерено обратное: при разрыве канала отказали ОБЕ половины. Шесть узлов, по три в каждом ДЦ, мастера поделены 2 и 1 — и все шесть узлов ответили CLUSTERDOWN. Каждая половина проверена на своём свежем кластере с той же топологией, а не двумя проверками подряд на одном: последствия первой проверки не могли достаться второй. У ДЦ-1 было большинство мастеров и голосов на повышение хватало, но реплика мастера, оставшегося в ДЦ-2, тоже осталась в ДЦ-2 — повышать было некого. Спецификация называет два требования, а не одно: the majority of the master nodes are reachable and there is at least one reachable replica for every master node that is no longer reachable
(доступно большинство мастер-узлов и для каждого недоступного мастер-узла доступна хотя бы одна реплика). Большинство необходимо, но недостаточно — и полное условие состоит даже не из двух частей, а из трёх: третья в том, что реплика должна быть достаточно свежей, иначе cluster-replica-validity-factor не даст ей начать выборы.
Реплики во втором ДЦ — это запасной кластер: если первый ДЦ упадёт, они поднимутся
Не поднимутся. В раскладе «все мастера в ДЦ-1, все реплики в ДЦ-2» после разрыва в ДЦ-2 оказалось ноль мастеров из трёх узлов, состояние всех трёх — fail, на GET все три вернули CLUSTERDOWN. Для повышения реплике нужен голос большинства мастеров — Once the replica receives ACKs from the majority of masters, it wins the election
(Как только реплика получает подтверждения от большинства мастеров, она выигрывает выборы), — а все мастера остались по ту сторону обрыва. Вторая половина не запасная: при потере первой она её не заменит, во всяком случае не сама.
Правильный расклад реплик можно проверить глазами по схеме
Нельзя: на схеме видно число узлов в каждом ДЦ, а не то, чья реплика где стоит. Между блоком 5 (отказали обе половины) и блоком 6 (ДЦ-1 выжил) разница — одна перестановка реплики. Она нигде не записана как требование, не проверяется при старте и не переживает автоматическое переключение: после первого же отказа мастера кластер расставит роли сам, и «правильный» расклад молча станет тем, что в блоке 5.
WAIT делает репликацию синхронной и закрывает вопрос потерь
Замерено: обычная запись 0,24 мс, запись с WAIT 1 — 41,73 мс, дороже в 171,6 раза. Цена — РОВНО ОДИН круг до реплики: на расстояниях 5, 20 и 40 мс в одну сторону она равна 10,90, 41,52 и 81,40 мс, а отношение к кругу — 1,09, 1,04 и 1,02. Купленное же свойство слабее ожидаемого: к моменту ответа запись на мастере уже применена и уже видна читателям, откатить её нечем. Документация говорит это прямо: it does not turn a set of Redis instances into a CP system with strong consistency: acknowledged writes can still be lost during a failover
(он не превращает набор экземпляров Redis в CP-систему со строгой согласованностью: подтверждённые записи всё ещё могут быть потеряны при переключении). Для дёшево восстанавливаемого кэша сделка обычно не окупается: он платит круг на каждую запись за данные, потеря которых предусмотрена промахом. Но не «бессмысленна»: если прогрев кэша стоит часов или источник не переживёт лавины холодного старта, надёжность репликации имеет цену, и сравнивать её надо со стоимостью повторного наполнения и допустимым окном потерь.
При неполном покрытии слотов перестанут работать только недостающие ключи
Отказ тотальный: в замере GET вернул CLUSTERDOWN на каждом узле выжившей половины, а не только по слотам потерянного мастера. Отсюда напрашивается вывод, что наблюдаемое поведение «шире написанного». Вывод неверен: cluster-require-full-coverage отвечает за записи (If this is set to yes, as it is by default, the cluster stops accepting writes if some percentage of the key space is not covered by any node
(если установлено yes, как по умолчанию, кластер перестаёт принимать записи, если некоторая часть пространства ключей не покрыта ни одним узлом)), а за чтения отвечает отдельная настройка cluster-allow-reads-when-down со значением no по умолчанию, и она тоже описана. Цепочка такая: неполное покрытие слотов → кластер помечен отказавшим → узел отказавшего кластера не обслуживает и чтения. Замерено на обеих настройках: с no все 20 ключей из 20 получают CLUSTERDOWN на каждом узле.
Поставлю cluster-allow-reads-when-down yes — и кэш переживёт разрыв
Не переживёт. Со значением yes узел отвечает только по ТЕМ ключам, чьи слоты остались за ним: в замере из двадцати ключей выжившая половина отдала четырнадцать (5 на одном мастере, 9 на другом), остальные перенаправила на мастер, который сейчас за оборванным каналом, — и туда клиент не доедет. Реплика в той же половине перенаправила все двадцать. Записи отказаны при обоих значениях настройки. То есть yes превращает «не работает ничего» в «работает часть чтений на части пространства ключей», а треть пространства — слоты мастера за обрывом — недоступна при любом значении.
Отставание реплики в 20 мс — мелочь, кэш столько простит
Простит, пока реплика остаётся репликой. Отставание становится потерей ровно в тот момент, ради которого реплику и держат, — при переключении. Замерено: разрыв сразу после залпа из двухсот записей потерял 93 из них (46,5%), и ни на одну клиент не получил ошибки — на все пришёл OK. Разрыв через секунду тишины не потерял ничего. Ходовое правило «под угрозой всё, что записано за последний круг» неверно: круг совпал с потерей случайно. На одном и том же канале три темпа записи дают 104, 3 и 0 потерянных записей при отставании 3548, 99 и 0 байт. Под угрозой то, что в момент разрыва лежит между master_repl_offset мастера и смещением реплики; оба числа Redis отдаёт в INFO replication. Круг задаёт лишь нижнюю границу того, за какое время отставание может сойти к нулю.
Общий кэш на два ДЦ нужен, потому что приложение работает в обоих
Приложение в двух ДЦ — довод за два кэша, а не за один общий: половина обращений уйдёт в чужой ДЦ и заплатит круг. Но «общий кэш» сам по себе не улика, и ходовое правило «если кэш обязан быть общим, значит, в нём лежит не кэш» неверно. Общий внешний кэш, в который ходят несколько приложений, — нормальная конструкция: он лучше снимает нагрузку с источника, чем набор процессных кэшей, избавляет от расхождения между узлами приложения и переживает выкладку. Красный флаг не «общий», а НЕВОССТАНОВИМОСТЬ. Проверка простая: удалите кэш целиком и посмотрите. Стало медленнее, данные восстановились — это кэш, и растягивать его через ДЦ незачем. Потерялись сессии, блокировки, счётчики — это хранилище состояния, и разговор о нём другой.
Если число измерено, оно измеряет то, что мы хотели измерить
Не обязательно, и эта статья — свой собственный контрпример. У WAIT сверх круга находилось «слагаемое в 42–44 мс, одинаковое на всех расстояниях», и происхождение его долго оставалось неустановленным. Установлено оно оказалось не в Redis, а в измерительном приборе: канал между ДЦ изображает ретранслятор в пользовательском коде, и на двух его соединениях не был выставлен TCP_NODELAY. Пара «алгоритм Нейгла — отложенное подтверждение» добавляла к каждой операции ровно сорок миллисекунд. Проверяется это опытом без расстояния вовсе: через канал с НУЛЕВОЙ задержкой запись с WAIT 1 стоила 44,00 мс без флага и 0,70 мс с ним. Отдельно поучительно, что дефектов было два и они маскировали друг друга: сериализация потока в ретрансляторе добавляла ещё один перелёт, и увидеть её удалось только после того, как убрали залипание, которое было вчетверо больше.
Проверьте себя
Кластер из шести узлов разложен поровну: в ДЦ-1 два мастера и одна реплика, в ДЦ-2 один мастер и две реплики. Канал между ДЦ оборван. Что произойдёт с ДЦ-1, у которого большинство мастеров?
Источники и что читать дальше
10 ИСТОЧНИКОВ
- Redis — Redis replicationОфициальная документация. Договор репликации, названный прямым текстом: «Redis uses by default asynchronous replication, which being low latency and high performance, is the natural replication mode for the vast majority of Redis use cases» (Redis по умолчанию использует асинхронную репликацию, которая, будучи низколатентной и высокопроизводительной, является естественным режимом репликации для подавляющего большинства сценариев использования Redis). Оттуда же — про то, что мастер не ждёт: «So the master does not wait every time for a command to be processed by the replicas, however it knows, if needed, what replica already processed what command» (Итак, мастер не ждёт каждый раз, пока команда будет обработана репликами, однако он знает, если это потребуется, какая реплика какую команду уже обработала). И оговорка про WAIT, ради которой страница цитируется в статье третий раз: «However WAIT is only able to ensure there are the specified number of acknowledged copies in the other Redis instances, it does not turn a set of Redis instances into a CP system with strong consistency: acknowledged writes can still be lost during a failover, depending on the exact configuration of the Redis persistence» (Однако WAIT способен лишь обеспечить наличие указанного числа подтверждённых копий на других экземплярах Redis; он не превращает набор экземпляров Redis в CP-систему со строгой согласованностью: подтверждённые записи всё ещё могут быть потеряны при переключении, в зависимости от конкретной настройки постоянного хранения Redis).https://redis.io/docs/latest/operate/oss_and_stack/management/replication/
- Redis — Redis cluster specificationОфициальная документация. Условие выживания кластера при разрыве, в котором два требования, а не одно: «Redis Cluster is able to survive partitions where the majority of the master nodes are reachable and there is at least one reachable replica for every master node that is no longer reachable» (Кластер Redis способен пережить разделения сети, при которых доступно большинство мастер-узлов и для каждого недоступного мастер-узла доступна хотя бы одна реплика). Второе требование в статье и оказывается тем, о котором забывают. Оттуда же — приговор проигравшей половине: «Redis Cluster is not available in the minority side of the partition» (Кластер Redis недоступен на той стороне разделения, где меньшинство). И механика выборов, объясняющая, почему у ровного деления голосов не хватает никому: «Once the replica receives ACKs from the majority of masters, it wins the election» (Как только реплика получает подтверждения от большинства мастеров, она выигрывает выборы).https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/
- Redis — Scale with Redis ClusterОфициальная документация. Страница, где описан параметр, из-за которого отказ распространяется на весь кластер, а не на недостающие ключи: «cluster-require-full-coverage <yes/no>: If this is set to yes, as it is by default, the cluster stops accepting writes if some percentage of the key space is not covered by any node. If the option is set to no, the cluster will still serve queries even if only requests about a subset of keys can be processed» (cluster-require-full-coverage <yes/no>: если установлено yes, как по умолчанию, кластер перестаёт принимать записи, если некоторая часть пространства ключей не покрыта ни одним узлом. Если параметр установлен в no, кластер продолжит обслуживать запросы, даже если может обработать запросы лишь о части ключей). На той же странице описана и настройка, отвечающая за чтения: «cluster-allow-reads-when-down <yes/no>», по умолчанию no. Именно она, а не полнота покрытия, решает, ответит ли узел отказавшего кластера на GET, — и это измерено отдельным прогоном.https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/
- Redis — Active-Active geo-distributed databasesОфициальная документация. Ссылка нужна затем, чтобы строка «active-active» в таблице раскладов не висела без первоисточника. Сама конструкция описана так: «Active-Active databases are geo-distributed databases that span multiple Redis Enterprise Software clusters» (Базы Active-Active — это геораспределённые базы, охватывающие несколько кластеров Redis Enterprise Software), а разрешение конфликтов у неё явное и встроенное: «Conflict resolution is handled by conflict-free replicated data types (CRDTs)» (Разрешение конфликтов выполняется бесконфликтными реплицируемыми типами данных (CRDT)). Это другой продукт и другая модель, и статья его не измеряла — но и делать вид, что «обе половины пишут независимо» невозможно в принципе, после такой страницы нельзя.https://redis.io/docs/latest/operate/rs/databases/active-active/
- PostgreSQL — High Availability, Load Balancing, and ReplicationОфициальная документация. Источник для той половины сравнения, которая в статье до сих пор держалась на общем знании. Про асинхронность по умолчанию: «When using streaming replication, servers will typically be configured as asynchronous» (При использовании потоковой репликации серверы обычно настраиваются как асинхронные), и про то, чем это оборачивается: «If the primary server crashes then some transactions that were committed may not have been replicated to the standby server, causing data loss» (Если основной сервер аварийно завершится, некоторые зафиксированные транзакции могут оказаться не реплицированными на резервный сервер, что приведёт к потере данных). То есть «база растянута» само по себе не означает ни синхронности, ни отсутствия окна потерь, — ровно это статья и утверждает.https://www.postgresql.org/docs/current/high-availability.html
- PostgreSQL — Replication configuration parametersОфициальная документация. Настройки, из-за которых «растянутая база» бывает совсем разной: `synchronous_standby_names` и `synchronous_commit`. Про последнюю документация говорит прямо: «if the standby is the last one in a synchronous group, setting this to on will result in commits waiting for the standby to confirm receipt» (если резервный сервер последний в синхронной группе, значение on приведёт к тому, что фиксации будут ждать подтверждения приёма от резервного). Сравнивать надо конкретную модель репликации с конкретной, а не «базу» с «кэшем»: у одной только базы этих моделей несколько.https://www.postgresql.org/docs/current/runtime-config-replication.html
- Amazon — Caching challenges and strategiesИсточник. Промышленный разбор ровно тех ловушек, которые статья измеряет: «a cache can be a big and highly damaging blast radius» (кэш может быть большим и крайне разрушительным радиусом поражения), и отдельно про то, почему холодный кэш опаснее отсутствия кэша: «the service is likely to be unable to handle the resulting load on its dependencies» (служба, вероятно, не справится с возникшей нагрузкой на свои зависимости). Отсюда же берётся аргумент в пользу тёплой реплики в чужом ДЦ — единственный случай, где растянутая конструкция в статье защищается.https://aws.amazon.com/builders-library/caching-challenges-and-strategies/
- Valkey — ReplicationОфициальная документация. Та же страница у форка, слово в слово с заменой имени: «Valkey uses by default asynchronous replication, which being low latency and high performance, is the natural replication mode for the vast majority of Valkey use cases» (Valkey по умолчанию использует асинхронную репликацию, которая, будучи низколатентной и высокопроизводительной, является естественным режимом репликации для подавляющего большинства сценариев использования Valkey). И про WAIT: «However WAIT is only able to ensure there are the specified number of acknowledged copies in the other Valkey instances, it does not turn a set of Valkey instances into a CP system with strong consistency» (Однако WAIT способен лишь обеспечить наличие указанного числа подтверждённых копий на других экземплярах Valkey; он не превращает набор экземпляров Valkey в CP-систему со строгой согласованностью). Ссылка нужна затем, чтобы утверждения статьи о договоре репликации не приходилось переносить на Valkey по аналогии.https://valkey.io/topics/replication/
- Valkey — Cluster specificationОфициальная документация. Условие выживания у форка сформулировано так же, с той же парой требований: «Valkey Cluster is able to survive partitions where the majority of the primary nodes are reachable and there is at least one reachable replica for every primary node that is no longer reachable» (Кластер Valkey способен пережить разделения сети, при которых доступно большинство первичных узлов и для каждого недоступного первичного узла доступна хотя бы одна реплика). И там же: «Valkey Cluster is not available in the minority side of the partition» (Кластер Valkey недоступен на той стороне разделения, где меньшинство).https://valkey.io/topics/cluster-spec/
- Valkey — HistoryИсточник. Происхождение форка, названное самим проектом: «Valkey is a fork of the open-source Redis (REmote DIctionary Server) database created in 2009 by the Italian hacker Salvatore "antirez" Sanfilippo» (Valkey — форк открытой базы данных Redis (REmote DIctionary Server), созданной в 2009 году итальянским хакером Сальваторе „antirez“ Санфилиппо). Отсюда и правило статьи: общее наследство позволяет переносить на Valkey то, что записано в его собственной документации теми же словами, и не позволяет переносить числа замера.https://valkey.io/topics/history/