Как Discord заменил Cassandra на ScyllaDB и сократил кластер со 177 до 72 серверов

Подробности о сбоях в работе серверов в 3 часа ночи, странной двухминутной ошибке сборки мусора и переработке кода на C++/Rust, которая привела к удалению 105 серверов баз данных за ночь.

Как Discord заменил Cassandra на ScyllaDB и сократил кластер со 177 до 72 серверов

Представьте, что вы — дежурный инженер по базам данных в Discord в 2022 году.

3 часа ночи. Срабатывает пейджер. Вы открываете Grafana, и главный кластер базы данных сообщений, состоящий из 177 огромных узлов Apache Cassandra, хранящих более четырёх триллионов сообщений чата, находится в состоянии критического напряжения. Задержка превышает 125 миллисекунд. Очереди компакции переполнены. Узлы зависают и отключаются от сети один за другим.

Если вам приходится регулярно разбираться в технических задачах, писать тексты или искать идеи для проектов, иногда удобно иметь ИИ-помощника прямо в мессенджере. В таких случаях можно воспользоваться Бесплатным ChatGPT 5, Midjourney, Suno — чат-ботом с ChatGPT в MAX, российском мессенджере.

Его можно найти в поиске: @id250102442943_2_bot. Это простой способ быстро получить подсказку, не переключаясь между приложениями.

Потеряла ли AWS свой центр обработки данных? Совершил ли хакер DDoS-атаку?

Нет. Популярный игровой сервер просто опубликовал объявление в общедоступном канале, и 50 000 подростков одновременно кликнули на него, чтобы заспамить его смайликами.

В течение многих лет Кремниевая долина рассматривала Apache Cassandra и языки программирования с автоматической сборкой мусора, такие как Go и Java, как идеальную формулу для бесконечного горизонтального масштабирования. Нужно хранить больше данных? Просто добавьте ещё 20 узлов в кольцо Cassandra. Нужен быстрый микросервис? Напишите его на Go.

Discord следовал именно этому руководству. Сначала компания переросла MongoDB, когда объём данных достиг 100 миллионов сообщений и индексы перестали помещаться в оперативной памяти. Затем она сделала ставку на Cassandra, масштабировав свой кластер с 12 узлов в 2017 году до 177 узлов, содержащих триллионы строк.

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

То, как Discord избежал операционного коллапса, отказавшись от основанной на Java Cassandra в пользу ScyllaDB на C++, перейдя с Go на Rust и сократив кластер со 177 узлов до всего 72 машин, — одна из величайших историй успеха в области системной инженерии в современной веб-разработке.

Ловушка: как один чат-канал вывел из строя кластер из 177 узлов

На бумаге Apache Cassandra кажется непобедимой. Она использует бессерверную кольцевую архитектуру типа «точка-точка», в которой каждый узел может принимать операции чтения и записи. Единой точки отказа нет.

В процессе эксплуатации Discord обнаружил неприятный секрет Cassandra: операции чтения обходятся значительно дороже, чем операции записи, а один «горячий» раздел может вывести из строя весь кластер.

Чтобы понять почему, взгляните на то, как Discord хранил сообщения. Ключ раздела Cassandra состоял из (channel_id, bucket), где bucket — это статичный временной интервал. Это означало, что каждое сообщение, отправленное в конкретном канале в течение двухнедельного периода, хранилось на одних и тех же физических узлах.

В закрытом групповом чате с десятью друзьями эта архитектура просто восхитительна.

Теперь посмотрите, что происходит на огромном общедоступном сервере с 500 000 участников, когда начинается какое-либо событие:

  • Тысячи пользователей заполняют один канал. Запросы на запись поступают в память Cassandra (memtable) и записываются на диск в виде неизменяемых файлов, называемых SSTables.
  • Одновременно десятки тысяч пользователей открывают этот канал, что приводит к параллельному чтению данных.
  • Поскольку в канал постоянно записываются данные, они фрагментируются по десяткам отдельных файлов SSTable на диске. Для выполнения одной операции чтения Cassandra должна проверить содержимое memtable, обратиться к фильтрам Блума и объединить фрагменты из нескольких файлов SSTable, прежде чем фоновая компакция сможет их объединить.
  • Один конкретный узел подвергается в 100 раз большей нагрузке на дисковый ввод-вывод, чем остальные узлы кластера.
  • И именно в этот момент виртуальная машина Java (JVM) наносит смертельный удар.

Под воздействием экстремальной нагрузки на чтение объём выделяемой памяти в Cassandra резко возрастает. Внезапно JVM запускает сборку мусора (GC). На несколько секунд узел базы данных полностью зависает, пока очищается память. Он перестаёт отвечать на запросы. Тем временем координаторные узлы в остальном кластере из 177 серверов накапливают резервные запросы, ожидающие ответа от зависшего узла. Это вызывает каскадную волну задержек по всей базе данных.

Инженеры Discord больше не тратили дни на разработку новых функций продукта. Теперь они следили за размерами кучи JVM, вручную сжимали файлы SSTable и выводили узлы из ротации, чтобы кластер продолжал работать.

Двухминутный призрак: почему от Go тоже отказались

Паузы JVM в Cassandra были не единственным случаем, когда автоматическое управление памятью подвело Discord.

Наряду с базой данных сообщений работала критически важная служба под названием «Состояния прочтения». Её задача проста, но чрезвычайно важна: отслеживать, какие каналы и сообщения прочитал каждый пользователь, чтобы значок непрочитанных уведомлений оставался актуальным на протяжении миллиардов сессий.

Изначально Discord разработал сервис состояний прочтения на языке Go. В большинстве случаев он работал быстро.

За исключением одной досадной аномалии: каждые две минуты, ровно в назначенное время, задержка p99 резко возрастала.

Инженеры тщательно изучили код, пытаясь найти задачу cron, сброс сетевой карты или блокировку базы данных, срабатывающую каждые 120 секунд. Ничего не нашли. Виновник был спрятан глубоко в исходном коде самой среды выполнения Go.

Для быстрой обработки запросов сервис Go хранил в памяти огромный кэш наименее недавно использованных данных (LRU), содержащий десятки миллионов структур состояния прочтения. Сборщик мусора Go разработан таким образом, чтобы запускаться динамически при удвоении объёма памяти кучи. Однако, если объём кучи растёт недостаточно быстро, среда выполнения Go применяет жёстко заданное правило: если цикл сборки мусора не выполнялся ровно две минуты, он запускается немедленно.

Каждые 120 секунд сборщик мусора Go активировался и сканировал весь многогигабайтный кэш LRU, чтобы проверить, не являются ли какие-либо указатели неактивными. Это расходовало ресурсы процессора и замедляло выполнение пользовательских запросов.

Настройка сборщика мусора не помогла. Единственный способ остановить сканирование памяти сборщиком мусора — отказаться от использования языка программирования с автоматической сборкой мусора.

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

Когда инженеры развернули версию на Rust в рабочей среде, извилистый фиолетовый график в форме пилы, который годами преследовал их панели мониторинга, превратился в совершенно ровную линию.

Кстати, если вы тоже часто работаете с большими объёмами информации, техническими материалами или документацией, ИИ может пригодиться для быстрого разбора сложных тем и подготовки черновиков.

Для этого можно попробовать chatgpt max — чат-бот с ChatGPT внутри российского мессенджера MAX. Удобно, когда хочется быстро уточнить непонятный термин или получить отправную точку для дальнейшей работы.

Представляем ScyllaDB и трюк с объединением запросов

Исправление работы сервиса на Go оказалось крайне важным: в экстремальных масштабах борьба со сборщиком мусора — это проигрышная битва.

Поэтому, когда пришло время спасать их 177-узловую базу данных сообщений Cassandra, Discord не стал ещё год возиться с настройками Java. Инженеры искали движок базы данных, построенный по тому же принципу работы непосредственно с аппаратными ресурсами, что и Rust.

Они выбрали ScyllaDB — совместимую с протоколом Cassandra систему, переписанную с нуля на C++ и построенную поверх асинхронной платформы Seastar.

Вместо работы в ресурсоёмкой JVM и управления тысячами потоков ядром Linux ScyllaDB использует архитектуру с сегментированием по ядрам. Каждое ядро ЦП получает собственный выделенный сегмент оперативной памяти, собственную сетевую очередь и собственный канал дискового ввода-вывода. Это позволяет управлять памятью вручную без пауз сборки мусора.

Но инженеры Discord не просто заменили Cassandra на ScyllaDB и на этом успокоились. Они разместили перед базой данных собственный слой сервиса данных на Rust, используя удивительно умный приём: объединение запросов (Request Coalescing).

Помните кошмар с «горячим разделом», когда 50 000 пользователей одновременно кликают на канал с объявлениями в одну и ту же миллисекунду?

В старой архитектуре к API поступало 50 000 запросов, которые, в свою очередь, отправляли 50 000 идентичных запросов к базе данных, что приводило к перегрузке диска.

С новым сервисом данных на Rust, работающим поверх ScyllaDB, всё изменилось:

  1. Пользователь № 1 запрашивает последние 50 сообщений для Channel_X. Сервис Rust создаёт задачу внутри своей асинхронной среды выполнения и выполняет запрос к ScyllaDB один раз.
  2. Через две микросекунды, пока первый запрос к базе данных ещё выполняется, 49 999 других пользователей запрашивают точно такую же историю для Channel_X.
  3. Вместо того чтобы отправлять ещё 49 999 запросов к базе данных, сервис Rust распознаёт, что запрос для Channel_X уже выполняется. Он просто подписывает все 49 999 ожидающих запросов на результат этого единственного выполняющегося вызова к базе данных.
  4. ScyllaDB считывает данные с диска один раз, передаёт полезную нагрузку сервису Rust, а Rust одновременно рассылает ответ всем 50 000 пользователям.

«Горячие» разделы не просто стали быстрее. Они стали практически безвредными.

Квитанция: из 177 серверов осталось 72

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

Когда Discord впервые попытался перенести исторические сообщения с помощью Apache Spark, расчёты показали, что при высокой нагрузке на кластер процесс займёт недели. Поэтому инженеры отказались от Spark и написали собственный инструмент миграции на Rust, используя функцию ScyllaDB для работы с диапазонами токенов. Инструмент на Rust обработал набор данных со скоростью 3,2 миллиона записей в секунду, переместив триллионы сообщений всего за девять дней, пока пользователи продолжали общаться без единого сбоя.

Когда инженеры наконец вывели из эксплуатации старую сеть Cassandra, показатели «до» и «после» выглядели как опечатка:

  • Количество серверов сокращено на 60%. Discord отключил 177 узлов Cassandra и заменил всю рабочую нагрузку всего 72 узлами ScyllaDB, увеличив при этом объём дискового пространства на каждом узле с 4 ТБ до 9 ТБ.
  • Задержка в хвостовой части распределения устранена. Историческая задержка получения сообщений (p99) снизилась с нестабильных пиков в 40–125 мс в Cassandra до стабильных, предсказуемых 15 мс в ScyllaDB.
  • Стабилизирована скорость записи. Задержка p99 при вставке сообщений снизилась с 5–70 мс до стабильных 5 мс даже во время масштабных глобальных событий, сопровождавшихся всплесками трафика.
  • Ночные аварийные оповещения прекратились. Задержки при обработке данных и зависания JVM, приводившие к остановке всего процесса, исчезли за одну ночь.

Главный вывод

Большинство стартапов никогда не будут иметь дело с четырьмя триллионами сообщений в чате. Для 95% компаний хорошо индексированный экземпляр PostgreSQL (на который Discord до сих пор полагается для основных транзакционных метаданных!) и стандартный язык программирования для бэкенда станут достаточной основой для развития бизнеса вплоть до IPO.

Однако история борьбы Discord выявляет два фундаментальных закона системной инженерии, с которыми рано или поздно приходится сталкиваться каждому архитектору:

У абстракций есть потолок. Сборщики мусора (будь то в JVM или Go) кажутся волшебными, когда объём кучи невелик. Когда же вы храните гигабайты состояния в памяти при высокой параллельности, автоматическое управление памятью превращается в непредсказуемую бомбу замедленного действия.

Не масштабируйте оборудование, чтобы исправить некорректные шаблоны доступа. Прежде чем запускать ещё 100 узлов базы данных для обработки пиковых нагрузок, задайте себе вопрос, почему ваше приложение задаёт базе данных один и тот же вопрос 50 000 раз в секунду.

Когда ваша инфраструктура загорается в 3 часа ночи, добавление большего количества YAML-файлов или увеличение числа облачных экземпляров в Java-кластере вас не спасёт. Уважительное отношение к оборудованию и написание кода, который понимает, как на самом деле работают память, ядра процессора и дисковый ввод-вывод, в конечном итоге всегда побеждают.

И если в процессе работы над техническими проектами вам хочется быстрее разбираться в новых инструментах, формулировать идеи или проверять свои гипотезы, можно держать под рукой chatgpt в max. Это чат-бот с ChatGPT в российском мессенджере MAX, который может стать удобным помощником в повседневных задачах — без необходимости каждый раз искать отдельный сервис.

А как вы считаете: что важнее при масштабировании сервиса — добавлять новые серверы или переписывать архитектуру и оптимизировать код? Смогли бы вы решиться на такую масштабную миграцию, как Discord?

44