> Серия «Что внутри?» — выпуск о Redis Cluster: как данные раскладываются по слотам, как узлы узнают друг о друге и почему при падении мастера реплика становится новым мастером без потери доступности.
Redis — это in-memory хранилище «ключ-значение», которое многие используют как кэш, очередь или даже базу. Одиночный Redis умеет только один узел: вся память одного процесса, один поток событий. Но когда данных и трафика становится больше одного сервера, на помощь приходит Redis Cluster — распределённый режим, в котором десятки узлов делят данные между собой, переживают падения мастеров и позволяют менять состав кластера на лету.
В этом выпуске разберём три главных механизма Redis Cluster: как данные раскладываются по слотам, как узлы обмениваются состоянием (госсип) и как при сбое мастера выбирается новый лидер. А заодно посмотрим, почему здесь не используется классический консенсус вроде Raft.
Большая картина: кластер как «декартово» пространство из 16384 слотов
Начнём с главной идеи. Redis Cluster не шардирует данные по имени или по порядку — он делит всё ключевое пространство на фиксированное число слотов: 16384.
ВСЕ КЛЮЧИ КЛАСТЕРА
┌────────────────────────────────────────────────────────────────┐
│ slot 0 │ slot 1 │ … │ slot 5460 │ … │ slot 10922 │ … │ 16383 │
└────────────────────────────────────────────────────────────────┘
└─────── node A ────────┘ └───── node B ────┘ └─ node C ─┘
(0..5460) (5461..10922) (10923..16383)Какой слот принадлежит ключу? Вычисляется детерминированно:
slot = CRC16(key) mod 16384CRC16 — хэш с хорошим распределением, а mod 16384 даёт номер слота. Важные следствия:
- один и тот же ключ всегда попадает в один и тот же слот, где бы его ни вычислили;
- слот — это единица перемещения данных: при добавлении или удалении узла переносятся не «куски ключей», а целые слоты;
- один узел владеет диапазоном слотов (не обязательно непрерывным, но обычно так и настраивают).
Почему именно 16384? Это компромисс: слотов должно быть достаточно много, чтобы кластер мог переживать добавление узлов без полного перераспределения, но достаточно мало, чтобы карту «слот → узел» можно было хранить целиком в памяти каждого узла (массив из 16384 записей — это килобайты, которые рассылаются в госсипе без проблем).
Каждый узел знает всю карту
Ключевой архитектурный факт: в Redis Cluster нет «диспетчера» и нет выделенного координатора. Каждый узел хранит полную карту кластера — какие слоты какому узлу принадлежат, кто является мастером, а кто репликой.
clusterState на каждом узле:
┌────────────────────────────────────────────┐
│ myself: node 7f9c (master) │
│ slots: [10923..16383] │
│ │
│ node 3a41 (master): slots [0..5460] │
│ └ replica e0d2 │
│ node 9c88 (master): slots [5461..10922] │
│ └ replica f1ab │
│ epoch / configEpoch / currentEpoch … │
└────────────────────────────────────────────┘Из-за этого любой узел может ответить клиенту, где лежит ключ: посмотрел в свою карту слотов — и сразу знает владельца. Именно поэтому клиент не обязан держать отдельный каталог: он спрашивает один узел, получает либо данные, либо «MOVED <узел>», и дальше идёт напрямую.
Такая «полная карта на каждом узле» возможна только потому, что карта маленькая и меняется редко (добавление/удаление узла, фейловер). Изменения распространяются не через центральный сервис, а через госсип — об этом дальше.
Cluster bus: отдельный канал для служебного трафика
Каждый узел Redis Cluster слушает два порта:
- порт данных (обычно 6379) — запросы клиентов;
- cluster bus (обычно 16379 = порт данных + 10000) — бинарный протокол для общения узлов между собой: пинги, госсип, голосование, редиректы.
node A node B
┌──────────────┐ data port ───► ┌──────────────┐
│ clients │ 6379 │ clients │
│ cluster bus │◄════════════════► │ cluster bus │
│ 16379 │ PING/PONG/gossip │ 16379 │
└──────────────┘ └──────────────┘Зачем отдельный порт? Чтобы служебный обмен не смешивался с клиентским трафиком и не влиял на измерение задержек: по данным узлы могут быть перегружены, но bus обязан оставаться живым для обнаружения сбоев.
Госсип: как состояние распространяется по кластеру
Узлы не соединены «все со всеми» постоянными соединениями и не имеют центрального координатора. Вместо этого каждый узел:
- Периодически пингует случайный узел — фоновая задача
clusterCronпросыпается каждые ~100 мс и отправляет PING. - Добавляет в пакет сведения о других узлах — госсип-секцию: «а вот, кстати, что я знаю про узлы X, Y, Z». Получатель обновляет свою карту из чужих сообщений.
- Полученные сведения передаются дальше в следующих пакетах — так новость о любом изменении (узел упал, появился, поменялась эпоха) рано или поздно доходит до всех.
Случайный PING + gossip-секция:
node A ── PING, gossip: [B: alive], [C: ?] ──► node B
◄─ PONG, gossip: [B: alive], [D: epoch 42] ─ node BГоссип не гарантирует мгновенную доставку — он гарантирует итоговую согласованность: любое событие со временем узнают все. Цена — задержка: чтобы кластер «узнал» о падении узла, нужно время на распространение слухов. Именно эта задержка и формирует ощущение, что Redis Cluster «не сразу замечает» падение — это не баг, а свойство gossip-дизайна.
Как узлы узнают друг о друге: кластер собирается без центра
В Redis Cluster нет «конфигурационного сервера», который вёл бы реестр узлов, и нет автодискавери по сети. Как же узел вообще узнаёт о существовании других? Ответ: кластер «заводится» через одну известную точку входа, а дальше состав распространяется тем же госсипом.
Первый узел просто запускается с cluster-enabled yes — он сам по себе кластер из одного узла. Чтобы добавить второй, администратор выполняет команду на любом уже существующем узле:
CLUSTER MEET <ip> <port>Дальше происходит вот что:
- Узел A, получивший команду, отправляет новому узлу B по cluster bus сообщение MEET — «давай познакомимся».
- B добавляет A в свою таблицу узлов и отвечает PONG.
- В следующих PING/PONG узлы обмениваются полной gossip-информацией: A рассказывает B обо всех, кого знает, B — обо всех, кого знает он.
- Теперь B знает полный состав кластера и сам начинает пинговать остальных — никакого центрального реестра больше не нужно.
ADMIN: CLUSTER MEET B
│
▼
A ── MEET ─────────────► B
A ◄──── PONG ─────────── B
A ── PING + gossip: знаю C, D, E ─► B
A ◄─ PONG + gossip: знаю F ──────── B
│
▼
B сам пинует C, D, E, F → полный список узлов у всехТо есть вход в кластер — это ровно один известный узел (seed): нового члена знакомит с остальными тот, кому сказали MEET. Инструменты вида redis-cli --cluster create <ip1> <ip2> <ip3> ... — это просто обёртка: они по очереди выполняют CLUSTER MEET между перечисленными адресами.
Что важно для эксплуатации:
- Никакой «мастер-узел знакомств» не нужен — но и автодискавери вроде mDNS нет. При добавлении узла вы обязаны выполнить MEET от какого-то живого члена кластера.
- После MEET новый узел ещё не владеет слотами: ему нужно ресхардиться (передать слоты), иначе данные на нём не появятся.
- Узлы помнят состав кластера между перезапусками: Redis хранит список узлов, их роли и эпохи в файле конфигурации кластера (
nodes.conf). При старте узел читает его, восстанавливает соединения с известными соседями и снова «прогревает» госсип. Поэтому для постоянного кластера достаточно, чтобы хотя бы часть узлов была жива: перезапущенный узел найдёт остальных по памяти. - Новые узлы, добавленные «правильно», попадают в
nodes.confвсех членов после распространения госсипа — и переживают перезапуски.
Именно эта схема и даёт ответ на вопрос «откуда кластер знает, кто в нём есть»: начальный «знакомство» — через MEET от живого узла, дальнейшая жизнь — через госсип и персистентный nodes.conf, без единой точки отказа в виде центрального реестра.
Обнаружение сбоя: PFAIL → FAIL
Как узел понимает, что сосед умер? Поэтапно.
- PFAIL (possible failure) — субъективная оценка одного узла. Если узел A не получает ответов от узла B дольше
cluster-node-timeout(по умолчанию 15 секунд), A помечает B как PFAIL «у себя в голове». - A включает слух о PFAIL(B) в госсип.
- Когда большинство мастеров, которые могут связаться с B, тоже считают его PFAIL, состояние B становится FAIL — и это уже «объективное» решение кластера.
- Состояние FAIL распространяется по госсипу, и все узлы исключают B из обслуживания.
node A: B недоступен > timeout → PFAIL(B) локально
│
└─ gossip: "я думаю B упал"
│
▼
узлы, получившие достаточно голосов «PFAIL от большинства мастеров»
│
▼
FAIL(B) — официально, рассылается всемКлючевая идея — переход от частного мнения к коллективному: один узел может ошибаться (сеть дёрнулась именно у него), а вот согласие большинства мастеров — это уже сигнал. Мастера голосуют, потому что именно они держат данные и отвечают за выборы реплики (см. следующий раздел).
Выбор нового мастера: голосование реплик
Когда мастер признан FAIL, его реплики должны выбрать, кто из них станет новым мастером. Redis Cluster делает это голосованием реплик среди мастеров — без центрального координатора.
Механика:
- Одна из реплик упавшего мастера «решает» выдвинуться. Чтобы разные реплики не стартовали одновременно, они ждут случайную задержку, пропорциональную их «рангу» (чем дольше реплика синхронизирована с мастером, тем раньше она стартует).
- Реплика увеличивает
currentEpochи рассылает по cluster bus сообщение FAILOVER_AUTH_REQUEST. - Мастера, получившие запрос, отвечают FAILOVER_AUTH_ACK, если в этой эпохе ещё никому не давали голос. Один мастер = один голос на эпоху.
- Реплика становится новым мастером, когда собирает голоса большинства мастеров кластера.
- Победитель получает новый
configEpoch, объявляет себя мастером для слотов упавшего, и новость разлетается по госсипу.
реплика R1 упавшего мастера M мастера A, B, C
FAILOVER_AUTH_REQUEST (epoch=+1)
─────────────────────────────►
◄────────────── FAILOVER_AUTH_ACK (A)
◄────────────── FAILOVER_AUTH_ACK (B)
◄────────────── FAILOVER_AUTH_ACK (C) ← большинство (3 из 3)
R1: я новый мастер для слотов M
(новый configEpoch), рассылаю по госсипуПочему это безопасно? Два ключевых инварианта:
- Мастер голосует только один раз за эпоху — две реплики не могут одновременно получить большинство в одной эпохе.
- Новый мастер имеет больший
configEpoch, чем старый — если старый мастер «оживёт» и попробует продолжить работу, кластер увидит его эпоху как устаревшую и предпочтёт новую конфигурацию. Оживший старый мастер превращается в реплику нового.
Заметьте тонкость: Redis Cluster не использует Raft. Здесь нет жёсткого консенсуса на каждую операцию, нет единого журнала и лидера «на всё». Вместо этого — распределённая эпоха, госсип и голосование только в момент фейловера. Записи по-прежнему идут на один мастер без кворума на каждую запись; отсюда и известное ограничение: при сетевом разрыве кластер может предпочесть доступность согласованности (разные мастера продолжат работу в разделённых частях).
Как клиент попадает на нужный узел: MOVED и ASK
У клиента есть два способа узнать, где живёт ключ.
- MOVED — «ключ принадлежит слоту X, а слот X теперь у узла Y». Это жёсткий редирект: слот переехал, и клиент (умный клиент) запоминает новую карту и идёт к Y напрямую.
CLIENT → node A: GET key
node A: slot(key)=100 → владелец node C
node A → CLIENT: MOVED 100 <node C:port>
CLIENT → node C: GET key → OK- ASK — временный редирект во время ресхардинга. Когда слот мигрирует с узла на узел, ключи переезжают не мгновенно, а по одному. Пока часть ключей уже уехала, а часть ещё на старом узле, владелец слота формально — новый узел, но старый всё ещё может хранить остаток. Поэтому при обращении к мигрирующему слоту клиент может получить ASK: «сходи на новый узел, но спроси его, нет ли ключа у старого». ASK — разовый, на один ключ, и не меняет клиентскую карту.
Во время миграции слота 100:
CLIENT → node A (старый): GET key
node A: часть ключей уже переехала, у меня его нет
CLIENT → node C (новый): ASKING + GET key
node C: ключ у меня (или у старого) → OKРесхардинг: перенос слотов на лету
Добавление или удаление узла — это перемещение слотов между узлами. Перемещение слота — онлайн-операция:
- Инструмент (
redis-cli --cluster reshard) выбирает, какие слоты и куда переедут. - Для каждого слота на исходном узле запускается перенос ключей через команду
MIGRATE: ключ сериализуется, отправляется целевому узлу, атомарно удаляется со старого. - Пока слот мигрирует, клиенты получают ASK (см. выше) и корректно добираются до данных.
- Когда слот полностью переехал, карта слотов обновляется, и дальше работает обычный MOVED.
Данные не «переписываются с нуля» — переезжает каждый ключ, но операция идёт фоном и не останавливает обслуживание. Именно поэтому кластер можно расширять и ужимать без даунтайма.
Ещё пара важных деталей
- Только один уровень репликации данных на уровне кластера. Master-slave репликация в Redis — асинхронная: мастер отвечает клиенту, не дожидаясь подтверждения реплики. Поэтому при падении мастера возможна потеря последних записей — это важное свойство, которое нужно помнить, выбирая Redis Cluster для данных, которые «нельзя терять».
- Мультиключевые операции работают только в пределах одного слота:
MSET a b cсработает, только если все ключи попадают в один слот (или вы используете хэш-теги{user1}:nameи{user1}:age— Redis хэширует только часть ключа в фигурных скобках). - Replica может обслуживать чтения (readonly), что позволяет масштабировать чтение, но не запись.
- cluster-node-timeout — главная ручка настройки: меньше — быстрее обнаруживаем падение, но больше ложных фейловеров при «дёрганой» сети.
Слоты детальнее: почему 16384, CRC16 и хэш-теги
Почему Redis Cluster не использует, например, консистентное хэширование с «виртуальными узлами», как многие другие системы? Ответ в дизайне перемещения данных. В консистентном хэшировании ключи «прилипают» к участкам кольца, и перенос данных при изменении состава кластера определяется абстрактной геометрией. Redis выбрал явные слоты: их конечное число, они перенумерованы, и кластер всегда может сказать точно: «слот N принадлежит узлу X». Это делает две вещи простыми:
- карту слотов можно целиком хранить в памяти каждого узла как плоский массив из 16384 записей «слот → узел», а не как сложное кольцо;
- перенос данных становится дискретной операцией: «передать слот N целиком», а не «разобраться, какие ключи геометрически сместились».
Хэш ключа считается как CRC16(key) mod 16384. CRC16 выбран не потому, что он «криптографический», а потому что он быстрый и хорошо перемешивает похожие строки: ключи user:1, user:2, …, user:1000 разойдутся по слотам почти равномерно.
Из этого следует два практических правила:
- Порядок ключей внутри слота не гарантирует ничего — слотам всё равно, «похожи» ли ключи. Для диапазонных запросов Redis Cluster не подходит без дополнительных структур.
- Одна логическая сущность должна жить в одном слоте. Если ваши данные — «пользователь + его сессии + его корзина», и вы хотите выполнять мультиключевые операции над ними, ключи должны попасть в один слот. Для этого в Redis есть хэш-теги:
slot = CRC16("{user:42}:profile") mod 16384
└─────────┘ ← хэшируется только часть в {…}
slot("user:42") = slot("{user:42}:profile") = slot("{user:42}:session")Всё, что внутри {...}, используется для вычисления слота; остальное — просто суффикс. Так MSET, MGET и мультиключевые Lua-скрипты работают корректно, пока все ключи разделяют один тег.
Протокол cluster bus и типы сообщений
Служебный трафик идёт по отдельному бинарному протоколу на порту «порт данных + 10000». Каждое сообщение имеет заголовок: длину, тип, номер узла-отправителя, номер текущей эпохи и «вес» (для приоритета при спорах). Основные типы сообщений:
| Сообщение | Назначение |
|---|---|
MEET | «познакомься» — добавляет узел в кластер при старте/масштабировании |
PING / PONG | проверка живости + обмен gossip |
FAIL | официальное объявление о падении узла |
FAILOVER_AUTH_REQUEST | реплика просит голоса мастеров |
FAILOVER_AUTH_ACK | мастер голосует за реплику |
UPDATE | «твоя карта устарела, вот актуальная конфигурация» |
ASK | временная помощь при миграции слота |
Фоновый цикл clusterCron просыпается каждые ~100 мс и выполняет несколько задач: пингует случайный узел, рассылает gossip-секции, проверяет таймауты, обрабатывает запросы на фейловер. Gossip-секция — это «визитная карточка» узла: состояние, эпоха, какие узлы он считает упавшими. Получатель вшивает чужие сведения в свою картину и передаёт их дальше в следующих пакетах.
Такой обмен создаёт задержку распространения состояния. Чтобы она не превращалась в вечный «туман», у каждого узла есть таймеры: если от узла давно нет ничего (дольше cluster-node-timeout), узел начинает считать его подозрительным.
Обнаружение сбоя по шагам с таймингами
Разложим обнаружение сбоя по времени. Пусть cluster-node-timeout = 15000 мс (по умолчанию).
t=0 узел A последний раз услышал B
t=15s A не получил ответа → A помечает B как PFAIL (у себя)
t=15s.. A включает в каждый PING/PONG gossip: «я думаю, B упал»
t≤30s другие мастера, получив слух, проверяют B сами
t=~15-30s B признан FAIL, если это подтвердило большинство мастеров
t=FAIL состояние разлетается, мастера B-слотов выбываютПочему два шага (PFAIL, потом FAIL)? Это защита от «ложных тревог»:
- PFAIL — локальная гипотеза. Она нужна, чтобы не рассылать по кластеру каждый чих: один узел может временно потерять связь, и это не повод менять топологию.
- FAIL — коллективное решение. Когда большинство мастеров, способных связаться с проблемным узлом, считают его упавшим, вероятность «у всех одновременно дёрнулась сеть» уже низкая.
Если сеть «дёрнулась» у всех (кластер распался на части), каждый осколок может объявить своих соседей FAIL и начать фейловеры. Это прямое следствие выбора в пользу доступности: Redis Cluster предпочитает продолжать обслуживание в разделённом кластере, а не останавливаться ради согласованности. Помните об этом, проектируя систему поверх кластера.
Выборы реплики: эпохи, задержки и гарантии
Когда мастер официально FAIL, его реплики «просыпаются». Чтобы две реплики не выдвинулись одновременно и не устроили «переворот вдвоём», работает следующий протокол.
- Реплика ждёт случайную задержку, к которой добавлен бонус для более «свежих» реплик (тех, что дольше всех были в синхронизации с мастером). Смысл — дать шанс самой актуальной реплике выдвинуться первой и получить голоса, пока остальные ещё ждут.
- Реплика увеличивает
currentEpochна 1 и шлёт мастерамFAILOVER_AUTH_REQUESTпо cluster bus. - Каждый мастер, который в этой эпохе ещё никому не голосовал, отвечает
FAILOVER_AUTH_ACK. - Как только реплика собрала голоса большинства мастеров, она провозглашает себя новым мастером: получает новый
configEpoch, берёт слоты упавшего и рассылает новость.
Тут два разных «эпохальных» счётчика, и их важно не путать:
- currentEpoch — «номер текущей конфигурации»: растёт на каждое изменение (например, каждые выборы). Голосовать можно один раз на
currentEpoch. - configEpoch — «версия владения слотами» у конкретного узла. Когда реплика побеждает, её
configEpochстановится больше старого мастера.
Почему так безопасно? Инвариант «один мастер — один голос на эпоху» не даёт двум репликам получить большинство одновременно. А когда старый мастер «оживает» после сетевого разрыва и пытается обслуживать свои старые слоты, кластер видит его configEpoch ниже, чем у нового мастера, и считает его устаревшим: он становится репликой нового.
Схема голосования:
мастера кластера
A ──► B ──► C ──► D ──► E
│
▼
реплика R мастера C (упал)
ждёт случайную задержку (с бонусом за свежесть)
R: currentEpoch++ ; шлю FAILOVER_AUTH_REQUEST
A,B,D,E — голосуют один раз за эту эпоху
R получила A,B (2 голоса)… большинство из 4 → R — новый мастер слота CКлиентская сторона: как клиент узнаёт, куда идти
Умный клиент Redis Cluster (redis-py-cluster, go-redis и др.) держит локальную карту слотов, которую обновляет по ответам серверов. Обычный запрос выглядит так:
клиент: GET user:42
──► слот = slot(user:42)
──► мой кэш говорит: слот у узла C → иду к C
──► C отвечает данными (или MOVED, если кэш устарел)MOVED — это не ошибка, а часть протокола: «слот теперь у меня — обнови карту». Ошибкой считается только слишком частое получение MOVED от «правильного» узла: это значит, что кластер только что перенастроили.
ASK во время ресхардинга работает иначе: он не меняет карту. Ключ, возможно, уже переехал, а возможно, ещё нет, поэтому клиент делает специальный запрос ASKING к новому владельцу, чтобы тот разрешил чтение ключа, который формально ещё «в стадии переезда».
нормальный режим: клиент ──► C (карта говорит C) ──► данные
после ресхардинга: клиент ──► A (карта устарела)
A: MOVED слот → C; клиент обновляет карту
во время миграции: клиент ──► C; C: ключа нет, спроси у старого
клиент ──► A + ASKING ──► данныеРесхардинг и ручной фейловер
Добавление узла в кластер — это CLUSTER MEET + перемещение слотов. Само перемещение слота:
- Инструмент (например,
redis-cli --cluster reshard) вычисляет, какие слоты и на какой узел переедут. - На исходном узле слот помечается как
migrating, на целевом — какimporting. - Ключи переезжают по одному через команду
MIGRATE(сериализация ключа → отправка → атомарное удаление на источнике). - Пока слот «в полёте», обращения к нему обрабатываются через ASK.
- Когда ключей не осталось, карта обновляется, состояние
migrating/importingснимается — дальше работает обычный MOVED.
Ручной (graceful) фейловер — CLUSTER FAILOVER: администратор просит реплику стать мастером без «боевого» обнаружения сбоя. Реплика дожидается, пока мастер пришлёт ей последние данные (чтобы не потерять хвост), а затем инициирует те же выборы. Это то, что происходит при плановом обслуживании узла.
Границы и безопасность: что Redis Cluster даёт и чего не обещает
Честный список ограничений, который стоит держать перед глазами:
- Записи не кворумные. Redis Cluster не ждёт подтверждений от реплик на каждую запись (если вы сами не вызвали
WAIT). Поэтому при падении мастера последние записи могут потеряться. - Мультиключевые операции ограничены слотом.
MSET/MGET/транзакции по нескольким ключам работают только в пределах одного слота; хэш-теги{...}— единственный способ «собрать» ключи вместе. - Доступность при разрыве выше согласованности. Распавшийся кластер продолжает обслуживать свои осколки, что может привести к расхождению данных, которые потом «победит» более новый
configEpoch. - Один уровень реплик. Архитектура кластера предполагает master + реплики; глубокая иерархия репликации — вне дизайна.
- Настраиваемые тайминги — главные рычаги.
cluster-node-timeoutуправляет и скоростью обнаружения сбоя, и порогом ложных срабатываний: уменьшение ускоряет фейловер, но повышает риск «фейловера по чиху».
Для данных, которые нельзя терять, Redis Cluster часто используют как быстрый слой, а «источник правды» держат в другой системе. Для кэшей, очередей и сессий, где потеря последних миллисекунд некритична, он — отличный инструмент.
Что дальше
Мы разобрали, как Redis Cluster решает главные задачи распределённости: раскладку данных по фиксированным слотам, распространение состояния через госсип, обнаружение сбоя через PFAIL→FAIL и выбор нового мастера голосованием реплик. Ключевой вывод: Redis Cluster построен не на едином консенсусе, а на эпохах, слухах и голосовании только в момент фейловера — это делает его живучим и простым, но переносит ответственность за согласованность на разработчика.
В следующих выпусках серии «Что внутри?» — как устроен планировщик горутин Go, а затем почему Кафка не теряет данные.