Предстоящий релиз Valkey 9.0 несёт в себе значительные улучшения в отказоустойчивости больших кластеров, позволяя масштабироваться до 2000 узлов и достигать производительности свыше 1 миллиарда запросов в секунду, и всё это с гарантированно ограниченным временем восстановления. В этой статье мы рассмотрим, как работает система кластеризации Valkey, а также архитектурные усовершенствования и тщательное тестирование, которые сделали возможным такой уровень масштабирования.

Автономная конфигурация Valkey (standalone) — это установка с одним сервером и опциональными репликами для обеспечения доступности, но все операции записи направляются на один первичный узел (primary). Это один процесс, один набор данных, ноль координации — молниеносно и просто в эксплуатации, когда CPU, памяти и сетевой карты одной машины достаточно для всей нагрузки. Однако при больших масштабах Valkey выходит за пределы ограничений одного узла.

В режиме кластера (cluster mode) Valkey разделяет пространство ключей на 16 384 хеш-слотов и распределяет их по нескольким первичным узлам с репликами для избыточности. Клиенты осведомлены о кластере: они направляют команды непосредственно на узел, владеющий слотом, и следуют перенаправлениям во время решардинга или аварийного переключения (failover). Результатом является горизонтальная масштабируемость, сбалансированная пропускная способность и встроенная отказоустойчивость без центрального координатора. За кулисами шина кластера (cluster bus) поддерживает когерентность этой распределённой системы, координируя членство, gossip-протокол и аварийное переключение.

Диаграмма, демонстрирующая различные режимы работы
Диаграмма, демонстрирующая различные режимы работы

Обзор шины кластера (Cluster Bus)

Под капотом узлы координируются через шину кластера — постоянную TCP-сетку (mesh) с легковесным протоколом на основе gossip. Она обрабатывает обнаружение (MEET), PING/PONG heartbeats с передачей топологии кластера «в нагрузку», принятие решений о сбое (FAIL) на основе кворума, выборы для продвижения реплики и разрешение конфликтов на основе эпох, чтобы кластер чисто сходился после разделений. Поскольку информация о членстве, состоянии и владении слотами распространяется через gossip, это позволяет Valkey масштабироваться до большого количества узлов, оставаясь устойчивым к сбоям узлов и сети.

Обнаружение членства и распространение информации (Gossip)

Узлы присоединяются к кластеру Valkey, отправляя сообщение MEET любому существующему члену (seed-узлу). С этого момента шина кластера, представляющая собой сеть постоянных TCP-соединений, распространяет информацию о членстве и топологии с помощью легковесного gossip-протокола, передавая данные о соседях «в нагрузку» к периодическим PING/PONG heartbeats. Этот peer-to-peer обмен быстро приводит к тому, что каждый узел в кластере получает актуальную информацию о том, какие слоты принадлежат каждому первичному узлу, и всё это без центрального координатора.

Диаграмма, демонстрирующая обнаружение узлов и формирование mesh-топологии
Диаграмма, демонстрирующая обнаружение узлов и формирование mesh-топологии

Шина кластера: обнаружение сбоев, аварийное переключение и обновление эпохи

Когда узел пропускает heartbeats дольше настраиваемого node-timeout, другие узлы помечают его как возможно отказавший (PFAIL — suspect). Если кворум первичных узлов фиксирует тайм-аут, они объявляют узел отказавшим (FAIL) и инициируют аварийное переключение: реплика(и) отказавшего первичного узла становится кандидатом на то, чтобы взять на себя владение шардом, и рассылает запросы на голосование всем первичным узлам. Первичные узлы отвечают, и победитель «продвигается» для обслуживания затронутых слотов. Клиенты, обращающиеся к старому владельцу, получают ответы MOVED/ASK и прозрачно повторяют запрос к новому первичному узлу.

Диаграмма, демонстрирующая обнаружение сбоев и аварийное переключение
Диаграмма, демонстрирующая обнаружение сбоев и аварийное переключение

Ключевые улучшения для масштабирования шины кластера в Valkey

За несколько мажорных и минорных версий сообщество значительно улучшило стабильность кластера и внесло множество изменений для обработки различных сценариев сбоев, чтобы система восстанавливалась стабильно и без ручного вмешательства. Вот несколько интересных улучшений:

  • Множественные сбои первичных узлов. Операция аварийного переключения в кластере выполняется строго последовательно. Таким образом, в любой момент времени только один шард может проходить через failover. Когда в кластере происходило несколько сбоев первичных узлов, это приводило к коллизиям запросов на голосование от реплик-кандидатов затронутых шардов. Из-за коллизии голоса разделялись, и кластер не мог достичь консенсуса по продвижению реплики за один цикл. При большом количестве отказавших первичных узлов кластер не восстанавливался автоматически, и требовалось ручное вмешательство администратора. Эту проблему решил Binbin Zhu в Valkey 8.1, введя механизм ранжирования для каждого шарда на основе лексикографического порядка ID шарда. С этим алгоритмом шард с наивысшим рангом начинает процедуру выборов раньше, а шард с более низким рангом добавляет задержку перед началом выборов. Это помогло добиться стабильного времени восстановления при множественных сбоях первичных узлов.

  • Шторм попыток переподключения к недоступным узлам. Профилирование показало, что при множественных сбоях узлов значительная часть вычислительных ресурсов уходит на попытки переподключения к уже отказавшим узлам. Каждый узел пытается переподключиться ко всем отказавшим узлам каждые 100 мс. Это приводило к значительному потреблению CPU, когда в кластере отказывали сотни узлов. Чтобы предотвратить перегрузку кластера, Sarthak Aggarwal реализовал механизм троттлинга (throttling), который допускает достаточное количество попыток переподключения в пределах настроенного тайм-аута узла кластера, при этом гарантируя, что серверный узел не будет перегружен.

  • Оптимизированное отслеживание отчётов о сбоях. Профилирование также выявило, что при одновременном отказе сотен узлов выжившие узлы тратят значительное время на обработку и очистку избыточных отчётов о сбоях. Например, после «убийства» 499 из 2000 узлов, оставшийся 1501 узел продолжал обмениваться gossip-сообщениями о каждом отказавшем узле, даже после того, как эти узлы уже были помечены как отказавшие. Seungmin Lee оптимизировал добавление/удаление отчётов о сбоях, используя префиксное дерево (radix tree) для хранения информации о времени отчёта, округлённого до секунды, и группировки нескольких отчётов вместе. Это также помогает эффективно очищать устаревшие отчёты о сбоях. Были внесены и другие оптимизации для предотвращения дублирующей обработки отчётов и экономии циклов CPU.

  • Система Pub/Sub. Шина кластера также используется для операций pub/sub, предоставляя упрощённый интерфейс, где клиент может подключиться к любому узлу для публикации данных, а подписчики, подключённые к любому другому узлу, получат эти данные. Данные передаются через шину кластера. Это довольно интересный способ использования шины. Однако накладные расходы на метаданные каждого пакета составляют примерно 2 КБ, что довольно много для небольших pub/sub-сообщений. Было замечено, что заголовок пакета был большим из-за информации о владении слотами (16384 бит = 2048 байт). Эта информация была нерелевантна для pub/sub-сообщений. Поэтому Roshan Khatri ввёл легковесный заголовок сообщения (около 30 байт) для эффективной передачи сообщений между узлами. Это позволило pub/sub лучше масштабироваться в больших кластерах. В Valkey также есть шардированная система pub/sub, которая ограничивает трафик каналов шарда рамками одного шарда, что является значительным улучшением по сравнению с глобальной системой pub/sub в режиме кластера. Эта система также была переведена на использование легковесного заголовка.

Valkey 9.0 содержит множество других улучшений для повышения общей стабильности системы кластеризации. Все эти усовершенствования позволили нам масштабироваться до 2000 узлов с ограниченным временем восстановления во время сетевых разделений. Ниже мы задокументировали конфигурацию бенчмарка и достигнутую пропускную способность.

Бенчмаркинг

Чтобы масштабировать кластер Valkey до 1 миллиарда запросов в секунду (RPS) для команд записи, мы решили выбрать команду типа SET для точного отражения масштаба. Мы видели предыдущие эксперименты, где один экземпляр мог достигать более 1 миллиона RPS, поэтому целью было достичь 1 миллиарда RPS с кластером из 2000 узлов, где каждый шард имеет 1 первичный узел и 1 реплику. Реплика добавляется к каждому шарду для повышения доступности.

Конфигурация оборудования

Для этого эксперимента кластер Valkey был развёрнут на инстансах AWS типа r7g.2xlarge — это инстансы, оптимизированные для работы с памятью, с 8 ядрами и 64 ГБ памяти на архитектуре ARM (aarch64). Чтобы сгенерировать достаточный трафик по всем слотам, мы использовали 750 инстансов AWS c7g.16xlarge.

Конфигурация системы

Примечание: Назначения ядер, используемые в этом руководстве, являются примерами. Оптимальный выбор ядер может варьироваться в зависимости от вашей конкретной конфигурации системы и рабочей нагрузки.

Каждый серверный узел Valkey имел 8 ядер, поэтому мы решили закрепить 2 ядра для привязки прерываний (interrupt affinity).

# определяем сетевой интерфейс, в нашем случае это был ens5
IFACE=$(ip route | awk '/default/ {print $5; exit}')

# открываем две комбинированные очереди Rx/Tx
sudo ethtool -L "$IFACE" combined 2

# получаем номера IRQ, которые принадлежат этим очередям
IRQS=($(grep -w "$IFACE" /proc/interrupts | awk '{gsub(":","",$1); print $1}'))
IRQ0=${IRQS[0]}
IRQ1=${IRQS[1]}

# привязываем прерывание очереди 0 к CPU0
echo 0 | sudo tee /proc/irq/$IRQ0/smp_affinity_list   # привязать IRQ0 → CPU 0
echo 1 | sudo tee /proc/irq/$IRQ1/smp_affinity_list   # привязать IRQ1 → CPU 1
sudo systemctl stop irqbalance

Мы увеличили стандартный лимит файловых дескрипторов на инстансе, чтобы узел мог обрабатывать соединения от узлов кластера Valkey и клиентов.

# увеличиваем максимальное количество соединений
ulimit -n 1048544

Оставшиеся 6 ядер на узле мы решили выделить под valkey-server, чтобы 6 io-threads (включая основной поток) могли максимально использовать ресурсы инстанса.

# привязываем 6 ядер к valkey-server
CPUSET=2-7
sudo cset shield --cpu=$CPUSET --kthread=on
sudo cset shield --exec taskset -- -c $CPUSET ./valkey-server valkey-cluster.conf --daemonize yes

Конфигурация сервера

Мы запускали каждый процесс Valkey с минимальными изменениями в конфигурации по умолчанию.

cluster-enabled yes # Включить режим кластера
cluster-config-file nodes.conf # Сохранять информацию о топологии
cluster-require-full-coverage no # Приоритет доступности
cluster-allow-reads-when-down yes # Разрешить чтение и частичные операции даже при потере слотов
save "" # Отключить периодические снимки
io-threads 6 # Разрешить выгрузку операций ввода-вывода в отдельные io-threads
maxmemory 50gb # Ограничить максимальное использование памяти процессом

Конфигурация бенчмарка

Мы запускали valkey-benchmark с 750 клиентских инстансов для генерации необходимого трафика. Для каждого из этих инстансов мы использовали следующие параметры.

  • Количество запросов (-n): 100M

  • Количество клиентов (-c): 1000

  • Тест (-t): SET

  • Размер данных (-d): 512

  • Потоки: 20

Диаграмма, демонстрирующая пропускную способность в 1 миллиард запросов в секунду
Диаграмма, демонстрирующая пропускную способность в 1 миллиард запросов в секунду

Общее наблюдение заключается в том, что пропускная способность растёт почти линейно с общим количеством первичных узлов, а шина кластера имеет минимальные накладные расходы при стандартном тайм-ауте узла.

Восстановление

В той же среде мы протестировали время восстановления кластера при отказе нескольких первичных узлов. Мы выбрали до 50% первичных узлов, «убивали» их и позволяли автоматическому процессу аварийного переключения устранить перерыв в обслуживании записи для данного шарда. Для имитации сбоев первичных узлов мы отправляли сигнал SIGKILL, чтобы жёстко остановить процесс Valkey, и позволяли реплике взять на себя управление через автоматическое аварийное переключение. Измеренное здесь время восстановления — это время с момента, когда любой из узлов обнаружил первичный узел в состоянии PFAIL, до момента, когда кластер сообщил о своём нормальном состоянии и все слоты снова были покрыты.

Диаграмма, демонстрирующая время восстановления при различных сценариях отказа первичных узлов
Диаграмма, демонстрирующая время восстановления при различных сценариях отказа первичных узлов

Заключение

Благодаря всем этим улучшениям в Valkey, кластер теперь может масштабироваться до 1 миллиарда RPS на 2000 узлах, что является выдающимся достижением. Однако есть ещё много возможностей для дальнейшего совершенствования. Накладные расходы на CPU в установившемся режиме от передачи/обработки сообщений шины кластера можно дополнительно сократить, внедрив протокол SWIM или вынеся обработку сообщений шины из основного потока в отдельный независимый поток. Логику аварийного переключения также можно сделать умнее, учитывая размещение узлов по зонам доступности (AZ). Мы также хотели бы добавить в систему больше метрик и логов для улучшения наблюдаемости и управляемости. Всё это связано в задаче Support Large Cluster. Не стесняйтесь ознакомиться с ней и добавлять свои предложения.

Комментарии (0)