У 2017 році ми опублікували статтю про те, як ми зберігаємо мільярди повідомлень. Ми розповіли про те, як починали використовувати MongoDB, але потім перенесли дані до Cassandra, оскільки шукали масштабовану, відмовостійку базу даних з відносно низькими витратами на обслуговування. Ми знали, що будемо рости, і так і сталося!
Ми хотіли базу даних, яка б росла разом з нами, але сподівалися, що її потреби в обслуговуванні не будуть рости разом з нашими потребами в зберіганні. На жаль, ми виявили, що це не так - наш кластер Cassandra мав серйозні проблеми з продуктивністю, і вимагав все більших зусиль лише для підтримки, не кажучи вже про покращення.
Майже шість років потому ми дуже змінилися, і спосіб зберігання повідомлень також змінився.
Наші проблеми з Кассандрою
Ми зберігали повідомлення в базі даних під назвою cassandra-messages. Як випливає з назви, вона працювала під управлінням Cassandra. У 2017 році ми запустили 12 вузлів Cassandra, в яких зберігалися мільярди повідомлень.
На початку 2022 року в ній було 177 вузлів з трильйонами повідомлень. На превеликий жаль, це була дуже складна система - наша чергова команда часто отримувала повідомлення про проблеми з базою даних, затримки були непередбачуваними, і нам доводилося скорочувати операції з технічного обслуговування, які стали занадто дорогими.
Що спричиняло ці проблеми? Спочатку розглянемо повідомлення.
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);
Наведений вище CQL-запит є мінімальною версією нашої схеми повідомлень. В якості ідентифікатора ми використовуємо Snowflake, який є відсортованим хронологічно. Ми розділяємо наші повідомлення за каналом, яким вони надсилаються, а також за бакетом, який є статичним часовим вікном. Таке розбиття означає, що в Cassandra всі повідомлення для даного каналу і бакета будуть зберігатися разом і реплікуватися на три вузли (або на будь-який інший, який ви встановили в налаштуваннях коефіцієнта реплікації).
У Cassandra операції зчитування є дорожчими: сервер з невеликою групою друзів, як правило, надсилає на порядок менше повідомлень, ніж сервер з сотнями тисяч користувачів.
У Cassandra операції зчитування коштують дорожче, ніж операції запису. Записи додаються до журналу коммітів і записуються в структуру пам'яті, яка називається memtable, і зрештою скидається на диск. Читання, однак, вимагає запиту до memtable і, можливо, до декількох SSTables (файлів на диску), що є значно дорожчою операцією. Велика кількість одночасних зчитувань під час взаємодії користувачів із серверами може призвести до «гарячої точки» розділу, яку ми умовно називаємо «гарячим розділом». Розмір нашого набору даних у поєднанні з цими моделями доступу призвів до проблем з нашим кластером.
Коли ми стикалися з гарячим розділом, це часто впливало на затримку в усьому кластері баз даних. Один канал і пара bucket отримували великий обсяг трафіку, і затримка у вузлі зростала, оскільки вузол намагався обслуговувати все більше трафіку і все більше відставав від інших вузлів.
Це впливало на інші запити до цього вузла, оскільки він за ними не встигав. Оскільки ми виконуємо читання і запис з підтримкою кворуму, всі запити до вузлів, які обслуговують гарячий розділ, зазнають збільшення затримок, що призводить до значного впливу на кінцевого користувача.
Завдання з обслуговування кластерів також часто викликали проблеми. Ми часто відставали у процесах стиснення, де Кассандра стискала SSTables на диску для швидшого зчитування. Мало того, що зчитування було дорожчим, ми ще й спостерігали каскадні затримки, коли вузол виконував стиснення даних.
Ми часто виконували операцію, яку називали “танець госсіпу”, де виводили вузол з обробки запитів, щоб дати йому можливість виконати ущільнення (compaction) без навантаження, повертали його, щоб він зміг отримати підказки з запасного буфера Cassandra, і повторювали це, доки черга ущільнення не була очищена. Ми також витратили багато часу на налаштування параметрів збирача сміття та купи (heap) в JVM, оскільки паузи в GC призводили до значних стрибків у затримці.
Зміна архітектури
Наш кластер повідомлень не був єдиною базою в Cassandra. У нас було ще декілька кластерів, і в кожному з них були схожі (хоча, можливо, не такі серйозні) проблеми.
У попередній частині цієї статті ми згадували, що нас заінтригувала ScyllaDB, сумісна з Cassandra база даних, написана на C++. Вона обіцяла кращу продуктивність, швидше відновлення, сильнішу ізоляцію робочих навантажень завдяки архітектурі шард на ядро та відсутність збирання сміття, і це виглядало достатньо привабливо.
Хоча ScyllaDB, безумовно, не позбавлена проблем, у неї немає збирача сміття, оскільки вона написана на C++, а не на Java. Історично наша команда мала багато проблем зі збиранням сміття на Cassandra, від пауз GC, що впливають на затримки, до дуже довгих послідовних пауз GC, які ставали настільки серйозними, що оператору доводилося вручну перезавантажувати вузол і няньчитись з ним до повного відновлення роботи. Ці проблеми були величезною проблемою і причиною багатьох проблем зі стабільністю в нашому кластері повідомлень.
Поекспериментувавши зі ScyllaDB і помітивши покращення в тестуванні, ми прийняли рішення про міграцію всіх наших баз даних. Хоча це рішення може бути окремою статтею в блозі, якщо коротко, то до 2020 року ми перенесли всі бази даних, крім однієї, на ScyllaDB.
Останню? Вже знайому нам cassandra-messages.
Чому ми не перенесли її раніше? Почнемо з того, що це великий кластер. З трильйонами повідомлень і майже 200 вузлами, будь-яка міграція потребувала б значних зусиль. Крім того, ми хотіли переконатися, що наша нова база даних буде найкращою, оскільки ми працювали над налаштуванням продуктивності. Ми також хотіли отримати більше досвіду роботи зі ScyllaDB у продакшені, використовуючи її в умовах підвищеної навантаженості та вивчаючи її підводні камені.
Ми також працювали над покращенням продуктивності ScyllaDB у наших кейсах. Під час тестування ми виявили, що продуктивність зворотних запитів була недостатньою для наших потреб. Ми виконуємо зворотний запит, коли намагаємося сканувати базу даних у зворотному порядку сортування таблиці, наприклад, коли ми скануємо повідомлення в порядку зростання. Команда ScyllaDB визначила пріоритети вдосконалення і впровадила ефективні зворотні запити, усунувши останній блокатор в нашому плані міграції.
Ми підозрювали, що додавання нової бази даних до нашої системи не зробить все чарівним чином краще. Гарячі розділи все ще можуть бути проблемою в ScyllaDB, і тому ми також хотіли інвестувати в поліпшення наших систем, що знаходяться перед базою даних, щоб допомогти захистити та сприяти кращій продуктивності бази даних.
Сервіси даних
З Cassandra ми боролися з гарячими розділами. Високий трафік до певного розділу призводив до необмеженого паралелізму, що призводило до каскадної затримки, при якій час відповіді на запити продовжував зростати. Якби ми могли контролювати об'єм трафіку до гарячих розділів, ми могли б захистити базу даних від перевантаження.
Щоб виконати це завдання, ми написали те, що ми називаємо сервісами даних - сервіси-посередники, які знаходяться між нашим монолітом API та кластерами бази даних. При написанні сервісів даних ми обрали мову, яку все частіше використовуємо в Discord: Rust! Ми вже працювали з нею в кількох проєктах раніше, і вона виправдала наші сподівання. Вона дала нам високу швидкість C/C++ без необхідності жертвувати безпекою.
Rust рекламує безстрашний паралелізм як одну з головних своїх переваг - мова повинна полегшити написання безпечного паралельного коду. Окрім того, вона має бібліотеки, які чудово підходять для наших цілей. Екосистема Tokio є чудовою основою для побудови системи на асинхронному вводі/виводі, а мова має підтримку драйверів для Cassandra та ScyllaDB.
Крім того, нам було дуже приємно писати код завдяки допомозі компілятора, зрозумілим повідомленням про помилки, конструкціям мови та акценті на безпеці. Нам дуже сподобалося те, як після компіляції вона працює. Але найголовніше - це те, що ми можемо сказати, що переписали його на Rust (це дуже важливо).
Наші сервіси даних знаходяться між API та кластерами ScyllaDB. Вони містять приблизно одну кінцеву точку gRPC на запит до бази даних і навмисно не містять бізнес-логіки. Важливою функцією, яку надають наші сервіси даних, є об'єднання запитів. Якщо кілька користувачів одночасно запитують один і той самий рядок, ми зробимо запит до бази даних лише один раз. Перший користувач, який робить запит, запускає робочу задачу в сервісі. Наступні запити перевірятимуть існування цього завдання і підписуватимуться на нього. Ця робоча задача зробить запит до бази даних і поверне рядок усім підписникам.
Це і є сила Rust в дії: він спростив написання безпечного паралельного коду.
Уявімо собі оголошення на великому сервері, яке сповіщає @everyone: користувачі відкривають додаток і читають повідомлення, надсилаючи тонни трафіку до бази даних. Раніше це могло призвести до перегріву розділу і, можливо, довелося б залучити персонал, аби допомогти системі відновитися. Використовуючи наші сервіси передачі даних, ми можемо значно зменшити сплески трафіку до бази даних.
Друга частина магії знаходиться перед нашими службами передачі даних. Ми впровадили послідовну маршрутизацію на основі хешування для наших сервісів даних, щоб забезпечити ефективніше об'єднання. Для кожного запиту до нашого сервісу даних ми надаємо ключ маршрутизації. Для повідомлень це ідентифікатор каналу, тому всі запити на один і той самий канал надходять до одного і того ж екземпляра сервісу. Така маршрутизація додатково допомагає зменшити навантаження на нашу базу даних.
Ці покращення дуже допомагають, але не вирішують усіх наших проблем. Ми все ще бачимо гарячі розділи та підвищену затримку на кластері Cassandra, просто не так часто. Це дало нам трохи часу, щоб підготувати новий оптимальний кластер ScyllaDB і виконати міграцію.
Дуже велика міграція
Наші вимоги до міграції досить прості: нам потрібно перенести трильйони повідомлень без простоїв, і нам потрібно зробити це швидко, тому що, хоча ситуація з Cassandra дещо покращилася, нам часто доводиться гасити пожежі.
Перший крок простий: ми створюємо новий кластер ScyllaDB, використовуючи нашу топологію супердискового сховища. Використовуючи локальні SSD для швидкості та дзеркальний RAID-масив, ми отримуємо швидкість підключених локальних дисків та довговічність постійного диска. Після створення кластера ми можемо розпочати міграцію даних.
Наш перший проєкт плану міграції був розроблений для отримання швидкого результату. Ми почали б використовувати наш блискучий новий кластер ScyllaDB для нових даних, а потім мігрували б старі. Це додає складності, але кожен великий проєкт потребує додаткової складності, чи не так?
Ми починаємо подвійний запис нових даних до Cassandra та ScyllaDB і паралельно починаємо створювати мігратор Spark для ScyllaDB. Це вимагає багато налаштувань, і після того, як ми його налаштуємо, у нас є приблизний час до завершення: три місяці.
Ці терміни не викликають у нас особливого захоплення, і нам би хотілося отримати результат швидше. Ми сідаємо всією командою і влаштовуємо мозковий штурм, щоб прискорити процес, аж поки не згадуємо, що написали швидку і продуктивну бібліотеку баз даних, яку ми можемо розширити. Ми вирішили взяти участь у розробці на основі мемів і переписати мігратор даних на Rust.
У другій половині дня ми розширили нашу бібліотеку сервісів даних для виконання великомасштабної міграції даних. Вона зчитує діапазони токенів з бази даних, перевіряє їх локально за допомогою SQLite, а потім пересилає їх в ScyllaDB. Ми підключаємо наш новий покращений мігратор і отримуємо нову оцінку: дев'ять днів! Якщо ми можемо мігрувати дані так швидко, то можемо забути про складний попередній план, а натомість перемкнути все одразу.
Ми запускаємо зі швидкістю до 3,2 мільйона на секунду. Через кілька днів ми збираємося, щоб подивитися, чи досягнуто 100%, і розуміємо, що міграція застрягла на позначці 99,9999% (о, ні). Наш мігратор вибиває тайм-аут на зчитуванні останніх кількох діапазонів даних, тому що вони містять гігантські діапазони tumbstone, які так і не були ущільнені в Кассандрі. Ми ущільнюємо ці діапазони токенів, і через кілька секунд міграція завершується!
Ми провели автоматизовану перевірку даних, відправивши невеликий відсоток зчитувань в обидві бази даних і порівнявши результати, і все виглядало чудово. Кластер добре справлявся з повним робочим трафіком, тоді як Кассандра все частіше страждала від проблем із затримками. Ми зібралися разом з нашою командою і перемкнулися на ScyllaDB в ролі основної бази даних, а потім з'їли святковий торт!
Декілька місяців потому...
Ми змінили нашу базу даних повідомлень у травні 2022 року, але як вона працювала з того часу?
Це була спокійна, добре керована база даних (я можу так сказати, тому що я не чергую на цьому тижні). Нам не доводиться гасити пожежі на вихідних, і ми не жонглюємо вузлами в кластері, намагаючись зберегти час безвідмовної роботи. Це набагато ефективніша база даних - ми переходимо від 177 вузлів Cassandra до 72 вузлів ScyllaDB. Кожен вузол ScyllaDB має 9 ТБ дискового простору, порівняно з 4 ТБ на вузол Cassandra.
Наші хвостові затримки також значно покращилися. Наприклад, на Cassandra затримка при отриманні історичних повідомлень становила 40-125 мс, на ScyllaDB - 15 мс, а при вставці повідомлень - 5-70 мс на Cassandra і стабільні 5 мс на ScyllaDB. Завдяки вищезазначеним покращенням продуктивності ми відкрили нові можливості використання продукту, тепер, коли ми впевнені в нашій базі даних повідомлень.
Наприкінці 2022 року люди по всьому світу налаштувалися на перегляд Чемпіонату світу з футболу. Ми дуже швидко виявили, що на наших графіках моніторингу з'явилися забиті голи. Це було дуже круто, тому що не тільки приємно бачити, як реальні події відображаються у ваших системах, але це дало нашій команді привід подивитися футбол під час зустрічей. Ми не «дивилися футбол під час нарад», ми «проактивно відстежували продуктивність наших систем».
Ми можемо розповісти історію фіналу Чемпіонату світу з футболу за допомогою нашого графіка відправлення повідомлень. Матч був приголомшливим. Ліонель Мессі намагався зробити останнє досягнення у своїй кар'єрі, закріпити своє звання найкращого футболіста всіх часів і народів і привести Аргентину до чемпіонства, але на його шляху стали надзвичайно талановитий Кіліан Мбаппе та Франція.
Кожна з дев'яти позначок на цьому графіку відображає певну подію в матчі.
- Мессі б'є пенальті, і Аргентина веде в рахунку 1:0.
- Аргентина забиває ще раз і веде в рахунку 2:0.
- Настав перерва. П'ятнадцятихвилинне плато зберігається, поки користувачі обговорюють матч у чаті.
- Великий сплеск тут пов'язаний з тим, що Мбаппе забиває за Францію, а через 90 секунд забиває ще раз, щоб зрівняти рахунок!
- Це кінець основного часу, і цей грандіозний матч переходить у додатковий час.
- У першій половині додаткового часу мало що відбувається, але ось настає перерва, і користувачі спілкуються в чаті.
- Мессі знову забиває, і Аргентина виходить вперед!
- Мбаппе завдає удару у відповідь, щоб зрівняти рахунок!
- Це кінець додаткового часу, ми переходимо до пенальті!
- Хвилювання і напруга зростають протягом всієї серії, поки Франція не промахується, а Аргентина - ні! Аргентина перемагає!
Люди по всьому світу напружено спостерігають за цим неймовірним матчем, а тим часом Discord і база даних повідомлень спокійно працюють. Ми вже давно перевершили всі очікування щодо надсилання повідомлень і чудово з ними справляємось. Завдяки нашим сервісам даних на основі Rust і ScyllaDB ми можемо впоратися з цим трафіком і надати нашим користувачам платформу для спілкування.
Коментарі (3)