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

Это большой объем данных, скорость и размер которых постоянно увеличиваются, и они должны оставаться доступными. Как они это делают? Кассандра!

Что они делают?

  • Пожалуй, одной из лучших баз данных для быстрого выполнения итераций является MongoDB.
  • Все в Discord хранилось в одном наборе реплик MongoDB, и это было сделано намеренно, но они также планировали все для простого перехода на новую базу данных (они знали, что не собираются использовать сегментирование MongoDB, потому что оно сложно в использовании и не отличается стабильностью). ).
  • Сообщения хранились в коллекции MongoDB с одним составным индексом для Channel_id и Create_at. Примерно в ноябре 2015 года они достигли 100 миллионов сохраненных сообщений и в это время начали замечать появление ожидаемых проблем.
  • Данные и индекс больше не помещались в ОЗУ, и задержки стали непредсказуемыми.

Выбор правильной базы данных

Прежде чем выбрать новую базу данных, им нужно было понять особенности чтения/записи и причины возникновения проблем с текущим решением.

  • Чтения были чрезвычайно случайными, а соотношение чтения/записи составляло примерно 50/50.
  • Серверы Discord, использующие голосовой чат, почти не отправляют сообщений. Это означает, что они отправляют одно или два сообщения каждые несколько дней. За год такой сервер вряд ли достигнет 1000 сообщений.
  • Серверы Discord с большим количеством личных текстовых чатов отправляют приличное количество сообщений, легко достигая от 100 тысяч до 1 миллиона сообщений в год.
  • Крупные общедоступные серверы Discord отправляют много сообщений. У них есть тысячи участников, отправляющих тысячи сообщений в день, и они легко набирают миллионы сообщений в год. Они почти всегда запрашивают сообщения, отправленные за последний час, и запрашивают их часто. Из-за этого данные обычно находятся в дисковом кеше.
  • Они знали, что в следующем году добавят пользователям еще больше способов случайного чтения: просмотр, переход к закрепленным сообщениям и полнотекстовый поиск. Все это приводит к более случайному чтению!!

Далее они определили свои требования:

  • Линейная масштабируемость.Они не хотят пересматривать решение позже или повторно сегментировать данные вручную.
  • Автоматическое переключение при отказе.Они любят спать по ночам и максимально использовать Discord для самовосстановления.
  • Низкие эксплуатационные расходы.Он должен работать сразу после его настройки. Им придется добавлять больше узлов только по мере роста данных.
  • Доказано, что работает.Они любят пробовать новые технологии, но не слишком новые.
  • Предсказуемая производительность У них есть оповещения, когда время ответа их API 95-го процентиля превышает 80 мс. Они также не хотят кэшировать сообщения в Redis или Memcached.
  • Не хранилище больших двоичных объектов. Написание тысяч сообщений в секунду было бы неэффективным, если бы приходилось постоянно десериализовать большие двоичные объекты и добавлять их к ним.
  • Открытый исходный код.Они верят в возможность управлять своей судьбой и не хотят зависеть от сторонней компании.

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

Моделирование данных

  • Лучший способ описать «Кассандру» новичку — это то, что это магазин ККВ. Два K составляют первичный ключ. Первый K — это ключ раздела, который используется для определения того, на каком узле находятся данные и где они находятся на диске.
  • Раздел содержит несколько строк, и строка внутри раздела идентифицируется вторым K, который является ключом кластеризации. Ключ кластеризации действует как первичный ключ внутри раздела и как сортировка строк.
  • Помните, что сообщения индексировались в MongoDB с использованием Channel_id и Create_at? Channel_id стал ключом раздела, поскольку все запросы выполняются на одном канале, но созданный_at не стал отличным ключом кластеризации, поскольку два сообщения могут иметь одинаковое время создания.
  • К счастью, каждый идентификатор в Discord на самом деле является Снежинкой (с возможностью сортировки в хронологическом порядке), поэтому они смогли использовать их вместо этого. Первичным ключом стал (channel_id, message_id), где message_id — это снежинка.
  • Вот упрощенная схема их таблицы сообщений (без около 10 столбцов).
CREATE TABLE messages ( channel_id bigint, message_id bigint, author_id bigint, content text, PRIMARY KEY (channel_id, message_id))
WITH CLUSTERING ORDER BY (message_id DESC)
  • Хотя Cassandra имеет схемы, мало чем отличающиеся от реляционной базы данных, их легко изменить, и они не оказывают никакого временного влияния на производительность.
  • Когда они начали импортировать существующие сообщения в Cassandra, они сразу же начали видеть в журналах предупреждения о том, что были обнаружены разделы размером более 100 МБ. Что дает?! Cassandra заявляет, что поддерживает разделы размером 2 ГБ!
  • Видимо, то, что это можно сделать, не означает, что это следует делать. Большие разделы оказывают большую нагрузку на Cassandra со стороны GC во время сжатия, расширения кластера и т. д. Стало понятно, что нужно как-то привязывать размеры разделов, ведь один канал Discord может существовать годами и постоянно увеличиваться в размерах.
  • Они решили распределить свои сообщения по времени. Они просмотрели крупнейшие каналы Discord и определили, что если они хранят сообщения за 10 дней в корзине, то их размер может комфортно оставаться ниже 100 МБ. Бакеты должны были быть получены из message_id или метки времени.
  • Ключи разделов Cassandra могут быть составными, поэтому их новым первичным ключом станет ((channel_id, Bucket), message_id).
CREATE TABLE messages ( channel_id bigint, bucket int, message_id bigint, author_id bigint, content text, PRIMARY KEY ((channel_id, bucket), message_id))
WITH CLUSTERING ORDER BY (message_id DESC);
  • Чтобы запросить последние сообщения в канале, они генерируют диапазон сегментов от текущего времени до идентификатора канала (он также является снежинкой и должен быть старше первого сообщения). Затем они последовательно опрашивают разделы, пока не будет собрано достаточно сообщений.
  • Недостатком этого метода является то, что редко активным Discord придется запрашивать несколько сегментов, чтобы со временем собрать достаточно сообщений. На практике это оказалось нормально, потому что для активных Discord достаточно сообщений обычно находится в первом разделе, и их большинство.

Темный запуск

  • Внедрять новую систему в производство всегда страшно, поэтому рекомендуется попробовать протестировать ее, не затрагивая пользователей. Они настроили свой код для двойного чтения/записи в MongoDB и Cassandra.
  • Сразу после запуска они начали получать ошибки в своем трекере ошибок, сообщающие нам, чтоauthor_id имеет значение null. Как оно может быть нулевым? Это обязательное поле!
  • Конечная последовательность. Так как же это их задело? Пример изменения/удаления состояния гонки
  • В сценарии, когда пользователь редактирует сообщение одновременно с тем, как другой пользователь удаляет то же сообщение, в итоге получается строка, в которой отсутствуют все данные, кроме первичного ключа и текста, поскольку все записи Cassandra являются обновлениями. Существовало два возможных решения этой проблемы:
  1. Напишите все сообщение обратно при редактировании сообщения. Это давало возможность воскресить удаленные сообщения и повысить вероятность конфликта при одновременной записи в другие столбцы.
  2. Выяснение того, что сообщение повреждено, и удаление его из базы данных.

Они выбрали второй вариант, выбрав необходимый столбец (в данном случаеauthor_id) и удалив сообщение, если оно было нулевым.

  • Решая эту проблему, они заметили, что пишут очень неэффективно. Поскольку Cassandra в конечном итоге является согласованной, она не может просто немедленно удалить данные. Он должен реплицировать удаления на другие узлы и делать это, даже если другие узлы временно недоступны. -
  • Кассандра делает это, рассматривая удаление как форму записи, называемую «надгробием». При чтении он просто пропускает встреченные надгробия. Надгробия живут в течение настраиваемого периода времени (по умолчанию 10 дней) и безвозвратно удаляются во время уплотнения по истечении этого времени.
  • Удаление столбца и запись в него значения null — это одно и то же. Они оба создают надгробие.
  • Поскольку все записи в Cassandra являются upserts, это означает, что вы создаете надгробие даже при первой записи нуля. На практике вся их схема сообщений содержит 16 столбцов, но в среднем сообщении установлено только 4 значения. Большую часть времени они писали Кассандре 12 надгробий без всякой причины.
  • Решение этой проблемы было простым: записывать в Cassandra только ненулевые значения.

Производительность

  • Известно, что Cassandra записывает быстрее, чем читает, и они именно это и заметили. Запись занимала доли миллисекунды, а чтение — менее 5 миллисекунд.
  • Они наблюдали это независимо от того, к каким данным осуществлялся доступ, и производительность оставалась стабильной в течение недели тестирования.

Большой сюрприз

  • Все прошло гладко, поэтому они сделали ее основной базой данных и в течение недели отказались от MongoDB. Он продолжал работать безупречно… около 6 месяцев, пока однажды Кассандра не перестала отвечать на запросы.
  • Они заметили, что Кассандра постоянно запускала 10-секундный GC остановить мир, но понятия не имели, почему. Они начали копать и нашли канал Discord, который загружался 20 секунд. Виновником стал публичный сервер Discord Puzzles & Dragons Subreddit.
  • Поскольку это было публично, они присоединились к нему, чтобы посмотреть. К их удивлению, на канале было всего одно сообщение. Именно в этот момент стало очевидно, что они удалили миллионы сообщений с помощью своего API, оставив в канале только 1 сообщение.
  • Возможно, вы помните, как Cassandra обрабатывает удаления с помощью надгробий (упоминается в разделе Согласованность результатов). Когда пользователь загружал этот канал, несмотря на то, что там было только одно сообщение, Cassandra приходилось эффективно сканировать миллионы сообщений-захоронений (генерируя мусор быстрее, чем JVM могла его собрать).

Они решили эту проблему, выполнив следующие действия:

  • Они сократили срок службы надгробий с 10 до 2 дней, потому что каждую ночь запускали восстановление Кассандры (антиэнтропийный процесс) в своем кластере сообщений.
  • Они изменили свой код запроса, чтобы отслеживать пустые сегменты и избегать их в будущем для канала. Это означало, что если пользователь снова вызовет этот запрос, то в худшем случае Cassandra будет сканировать только самую последнюю корзину.

Заключение

  • За год Discord увеличил общее количество сообщений с более чем 100 миллионов до более чем 120 миллионов сообщений в день, при этом производительность и стабильность остались неизменными.

Не забудьте нажать кнопки «Хлопнуть» и «Подписаться», чтобы помочь мне писать больше подобных статей.

Рекомендации

А если вы ищете обобщенные статьи, вы также можете просмотреть мои предыдущие статьи, такие какПочему Redis чудесным образом оптимизирован и Что делает шлюз API в архитектуре микросервисов Секретный соус NoSQL: LSM-дерево — проектирование системы

ИнтервьюNoodle Community