Назад к блогу

Что внутри Redis Cluster: слоты, госсип и выбор лидера при сбое

Что внутри Redis Cluster: слоты, госсип и выбор лидера при сбое

> Серия «Что внутри?» — выпуск о 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 16384

CRC16 — хэш с хорошим распределением, а 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 обязан оставаться живым для обнаружения сбоев.

Госсип: как состояние распространяется по кластеру

Узлы не соединены «все со всеми» постоянными соединениями и не имеют центрального координатора. Вместо этого каждый узел:

  1. Периодически пингует случайный узел — фоновая задача clusterCron просыпается каждые ~100 мс и отправляет PING.
  2. Добавляет в пакет сведения о других узлах — госсип-секцию: «а вот, кстати, что я знаю про узлы X, Y, Z». Получатель обновляет свою карту из чужих сообщений.
  3. Полученные сведения передаются дальше в следующих пакетах — так новость о любом изменении (узел упал, появился, поменялась эпоха) рано или поздно доходит до всех.
Случайный 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>

Дальше происходит вот что:

  1. Узел A, получивший команду, отправляет новому узлу B по cluster bus сообщение MEET — «давай познакомимся».
  2. B добавляет A в свою таблицу узлов и отвечает PONG.
  3. В следующих PING/PONG узлы обмениваются полной gossip-информацией: A рассказывает B обо всех, кого знает, B — обо всех, кого знает он.
  4. Теперь 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

Как узел понимает, что сосед умер? Поэтапно.

  1. PFAIL (possible failure) — субъективная оценка одного узла. Если узел A не получает ответов от узла B дольше cluster-node-timeout (по умолчанию 15 секунд), A помечает B как PFAIL «у себя в голове».
  2. A включает слух о PFAIL(B) в госсип.
  3. Когда большинство мастеров, которые могут связаться с B, тоже считают его PFAIL, состояние B становится FAIL — и это уже «объективное» решение кластера.
  4. Состояние FAIL распространяется по госсипу, и все узлы исключают B из обслуживания.
node A:  B недоступен > timeout  →  PFAIL(B) локально
   │
   └─ gossip: "я думаю B упал"
        │
        ▼
  узлы, получившие достаточно голосов «PFAIL от большинства мастеров»
        │
        ▼
  FAIL(B) — официально, рассылается всем

Ключевая идея — переход от частного мнения к коллективному: один узел может ошибаться (сеть дёрнулась именно у него), а вот согласие большинства мастеров — это уже сигнал. Мастера голосуют, потому что именно они держат данные и отвечают за выборы реплики (см. следующий раздел).

Выбор нового мастера: голосование реплик

Когда мастер признан FAIL, его реплики должны выбрать, кто из них станет новым мастером. Redis Cluster делает это голосованием реплик среди мастеров — без центрального координатора.

Механика:

  1. Одна из реплик упавшего мастера «решает» выдвинуться. Чтобы разные реплики не стартовали одновременно, они ждут случайную задержку, пропорциональную их «рангу» (чем дольше реплика синхронизирована с мастером, тем раньше она стартует).
  2. Реплика увеличивает currentEpoch и рассылает по cluster bus сообщение FAILOVER_AUTH_REQUEST.
  3. Мастера, получившие запрос, отвечают FAILOVER_AUTH_ACK, если в этой эпохе ещё никому не давали голос. Один мастер = один голос на эпоху.
  4. Реплика становится новым мастером, когда собирает голоса большинства мастеров кластера.
  5. Победитель получает новый 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

Ресхардинг: перенос слотов на лету

Добавление или удаление узла — это перемещение слотов между узлами. Перемещение слота — онлайн-операция:

  1. Инструмент (redis-cli --cluster reshard) выбирает, какие слоты и куда переедут.
  2. Для каждого слота на исходном узле запускается перенос ключей через команду MIGRATE: ключ сериализуется, отправляется целевому узлу, атомарно удаляется со старого.
  3. Пока слот мигрирует, клиенты получают ASK (см. выше) и корректно добираются до данных.
  4. Когда слот полностью переехал, карта слотов обновляется, и дальше работает обычный 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 разойдутся по слотам почти равномерно.

Из этого следует два практических правила:

  1. Порядок ключей внутри слота не гарантирует ничего — слотам всё равно, «похожи» ли ключи. Для диапазонных запросов Redis Cluster не подходит без дополнительных структур.
  2. Одна логическая сущность должна жить в одном слоте. Если ваши данные — «пользователь + его сессии + его корзина», и вы хотите выполнять мультиключевые операции над ними, ключи должны попасть в один слот. Для этого в 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, его реплики «просыпаются». Чтобы две реплики не выдвинулись одновременно и не устроили «переворот вдвоём», работает следующий протокол.

  1. Реплика ждёт случайную задержку, к которой добавлен бонус для более «свежих» реплик (тех, что дольше всех были в синхронизации с мастером). Смысл — дать шанс самой актуальной реплике выдвинуться первой и получить голоса, пока остальные ещё ждут.
  2. Реплика увеличивает currentEpoch на 1 и шлёт мастерам FAILOVER_AUTH_REQUEST по cluster bus.
  3. Каждый мастер, который в этой эпохе ещё никому не голосовал, отвечает FAILOVER_AUTH_ACK.
  4. Как только реплика собрала голоса большинства мастеров, она провозглашает себя новым мастером: получает новый 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 + перемещение слотов. Само перемещение слота:

  1. Инструмент (например, redis-cli --cluster reshard) вычисляет, какие слоты и на какой узел переедут.
  2. На исходном узле слот помечается как migrating, на целевом — как importing.
  3. Ключи переезжают по одному через команду MIGRATE (сериализация ключа → отправка → атомарное удаление на источнике).
  4. Пока слот «в полёте», обращения к нему обрабатываются через ASK.
  5. Когда ключей не осталось, карта обновляется, состояние 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, а затем почему Кафка не теряет данные.

Источники

Похожее