Remote Jobs
Close

The $4,000 month ghost storage optimizing the Outbox Pattern on Aurora MySQL

Posted 1 week ago

Имат ли значение за вас разходите за хостинг? В Yotpo се полагат усилия всеки цент да бъде похарчен разумно. Ето защо темата става приоритетна, когато Yotpo трябва плати допълнителни $150 за сутрешното кафе.


За повечето фирми това е бюджетно неудобство. За Системата за поръчки – гръбнакът на компанията, обработващ 100 000 заявки в минута в пиковите моменти – това е системна заплаха. Тази система представлява „точката, от която няма връщане“. Евентуален срив би спрял бизнеса: спира обработката на поръчки, изпращането на заявки за ревюта и начисляването на точки за лоялност. Клъстерът Aurora MySQL е сърцето на системата, където се запазва състоянието на поръчките и се генерират outbox събития за останалите услуги.

След анализ на метриките е открита тревожна тенденция. Само за шест седмици Aurora MySQL е нараснал с 15 терабайта. Този ръст съвпада с периода около Black Friday/Cyber Monday, което води до първоначалното му неглижиране на фона на scale up на услугите и увеличения в бюджети.

Фигура 1: Графика на използваното дисково пространство. Постоянен ръст от 28TB до 43TB
Фигура 2: Графика на „инфаркта“. Разходите за съхранение са скочили с $4000 на месец единствено поради увеличеното дисково пространство.

Екипът проследява увеличението на съхранението до една-единствена таблица, използвана от Outbox Pattern за генериране на събития.

Забавен факт: Тази таблица винаги е празна и въпреки това заема 15 TB.

Ето как Yotpo успява да разкрие мистерията на ,,призрачните данни”, защо стандартните DELETE операции не помогат и как е решен проблемът.

Разследването: The „InnoDB is Not a Queue“ Anti-Pattern

Първоначалната теория на екипа е за заседнала транзакция, блокираща undo логовете. Прегледът на логовете обаче показва, че най-дългите транзакции завършват за под 60 секунди.

Реалността е по-проста и по-болезнена: Несъответствие в скоростта.

Aurora MySQL се използва като опашка с висока пропускателна способност. В Outbox модела всяко съобщение преминава през жизнен цикъл от INSERT -> SELECT -> DELETE. Опитът базата данни да замести Kafka често води до архитектурен хаос.

В InnoDB операцията DELETE не освобождава дисковото пространство веднага. Тя маркира реда като изтрит и записва нов запис в undo лога.

Проблемът: Нишките за почистване (Purge Threads) трябва физически да преминат през тези undo логове, за да възстановят пространството. Базата маркира данни за изтриване (undo логове) по-бързо, отколкото процесът на почистване може физически да ги освободи от диска.

Фигура 3: Доказателство. Дължина на списъка с история (History List Length) > 1M е лошо. Техният е 4,7 милиарда. Скокът на 02/12 е заради смяната на инстанцията на базата към по-малка, който те правят след Black Friday/Cyber Monday.

Дължината на списъка с история от 4,7 милиарда не е внезапен скок; това е дефицит, който се е натрупва в продължение на месец и половина – като вана с широко отворен кран и запушен сифон.

Днес те питаме…

Кой въпрос задаваш най-често по време на интервю?

Loading ... Loading …

Борбата: Четири решения, които се провалят

Екипът прекарва седмици в опити да възстанови това пространство, без да повлияе на системата по никакъв начин. Повечето опити не проработват.

1. Настройка на InnoDB Purge Threads

Първото решение е да увеличат стойностите на параметрите innodb_purge_threads и innodb_purge_batch_size.

Нищожно въздействие. Не може да се реши чрез настройки фундаментално архитектурно несъответствие. Множество нишки не могат да почистват една и съща таблица едновременно.

2. Percona Online Schema Change

Второто решение е да се пресъздаде таблицата наново.

Неуспешно. Инструментът не успява да се справи с конкуренцията за заключване (lock contention), породена от високата скорост на записване/изтриване.

3. Стратегията „Table Swap“

Третото решение, което екипът изпозва, е да създаде нова таблица и да направи атомарно заместване (atomic swap). Така старата таблица се заменя с идентична нова, за да може старата да бъде изтрита.

Временна илюзия. Новата таблица веднага започва да натрупва същия проблем.

4. MySQL Engine BLACKHOLE

Четвъртото решение е да се използва друга подсистема за дисковото пространство BLACKHOLE. Предимството тук е, че тя записва само в BinLog и не записва данни на диска.

Отхвърлено. Макар и теоретично перфектно за outbox, това решение нарушава Debezium/CDC процесите, многократно отчитайки грешка „Invalid position at binlog file“. Файлът и позицията са валидни, след като екипът от Yotpo ги верифицира с mysqlbinlog, а рестартирането на конектора решава проблема временно. Има възможност Debezium да не поддържа тази комбинация.

Explore more

Виж

Azure Data Factory обявите

Събрани на едно място

Right Arrow


Виж

CircleCI обявите

Събрани на едно място

Right Arrow


Виж

Azure Data Lake обявите

Събрани на едно място

Right Arrow


Виж

Ruby on Rails обявите

Събрани на едно място

Right Arrow


Решението: Стратегическо превключване

Екипът осъзнава, че трябва да спре да изисква от базата данни да трие редове един по един и че се нуждае от начин за масово изтриване на данни, който да не използва Purge Threads в InnoDBи отговорът е Partitioning.

Чрез разделяне на таблицата на дневни сегменти, екипът може да премахва стари данни с DROP PARTITION. Това е атомарна операция – тя е мигновена и не генерира undo логове. Purge Thread дори не се намесва.

Ето как става това:

1. Промяна на схемата: Справяне със сложността на PK

Partitioning изисква ключът на сегмента (в техния случай created_at) да бъде част от главния ключ (primary key).

CREATE TABLE outbox_events_partitioned (

    id VARCHAR(36) NOT NULL, -- UUID

    event_payload JSON,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- The partition key must be part of the PK

    PRIMARY KEY (id, created_at))

    PARTITION BY RANGE (UNIX_TIMESTAMP(created_at)) (

    PARTITION p_2024_01_01 VALUES LESS THAN (UNIX_TIMESTAMP(’2024-01-02 00:00:00’)),

    PARTITION p_future VALUES LESS THAN MAXVALUE

);

Един риск при composite primary keys (id, created_at) е загубата на уникалност на самото ID. Това не е проблем за тях, тъй като техните ID-та са UUID-та, генерирани от приложението. Уникалността е гарантирана от приложението, така че може безопасно да се добавят timestamp към главния ключ на таблицата без да рискуват колизия.

2. The Downstream Config: Saving the Consumers

Промяната на Primary Key обикновено засяга консуматорите, но в конфигурацията на Debezium е скрито работещо решение.

Екипът е конфигурирал Debezium с transforms.outbox.table.field.event.id = id. Това отделя техните ключове на Kafka съобщения от главните ключове на базата данни. Въпреки че схемата на таблицата се променя на (id, created_at), събитията (events), изпращани към Kafka, запазват оригиналния id като ключ, гарантирайки, че консуматорите стават напълно незасегнати.

3. The Migration: The “Replay” Safety Net

Тъй като не могат да копират 15TB данни в новата Partition таблица, защото това ще отнеме твърде много време, а и без това после трябва да ги изтрият, те извършват смяна по време на прозорец за поддръжка, използвайки стратегия за „Повторение“ (Replay).

  • Спиране на записващите процеси: Първо екипът спира всички консуматори, които записват в БД, като оставя само API-то, тъй като то извършва малък обем записи. Това е с цел да се минимизира броят на събитията, които ще трябва да се „преиграят“ впоследствие.
  • Спиране на „кървенето“: Debezium конекторите са паузирани.
  • Атомарна замяна: Таблиците са разменени чрез RENAME TABLE.
  • „Повторението“: Екипът идентифицира всички активни поръчки, създадени по време на прозореца за поддръжка, и извършва „no-op“ актуализация върху тях чрез API-то (напр. „UPDATE orders SET updated_at = NOW()“).

Защо това е безопасно:

Екипът се уповава на строгите гаранции при консуматорите:

  • Тригерът: „No-op“ актуализацията генерира ново събитие със същите данни, но с по-нов времеви маркер.
  • Ограничаване на обхвата: Outbox таблицата проследява само евенти за поръчки.
  • Логиката на консуматора: Консуматорите проследяват времевия маркер на последното обработено събитие. Те автоматично отхвърлят всяко събитие, по-старо от тяхната текуща граница (watermark).

Ако консуматорът вече е обработил поръчката, той получава новото събитие, вижда, че състоянието съвпада, и го третира като идемпотентна актуализация (без промяна). Ако консуматорът е пропуснал поръчката (рядко), той обработва новото събитие като възстановяване. Това превръща рискованата миграция на данни в безопасен цикъл за синхронизация на състоянието.

4. The Maintenance: Automating the Cleanup

Partitioning решава проблема със съхранението, но въвежда допълнителни процеси по поддръжка. Екипът не иска да управляват сегменти ръчно и затова създава Java-базиран Kubernetes CronJob, изпълняващ се два пъти дневно по политика ,,7+7“:

  • Поглед напред: Предварително създава сегменти за следващите 7 дни, за да гарантира, че винаги има сегмент за запис. В противен случай записите в таблицата ще се провалят, което ще доведе до неуспех на CRUD операциите за поръчки.
  • Поглед назад: Изтрива сегменти, по-стари от 7 дни (периодът на задържане).

Екипът създава и аларма, ако Cron задачата се провали два поредни пъти в рамките на 24 часа. Тази проверка казва на OpsGenie да се обади на дежурния инженер по време на работните часове. Това им дава 6-дневен буфер за поправка на CronJob, преди да изчерпят предварително създадените сегменти. Също така те винаги могат да създадат сегменти ръчно, докато не решат проблема.

Резултатът: 24-часовият прозорец

Резултатите са огромни, но не идват мигновено.

Докато командата DROP TABLE върху старата таблица е мигновена, възстановяването на дисковото пространство не е. На Storage Engine на Aurora му отнема около 24 часа, за да освободи 15TB пространство. Екипът следи DB CPU, Disk Queue Depth и бавните заявки. Заедно с това наблюдават и тяхното API, за да са сигурни, че няма регресия в бързината му.

След като всичко приключи:

Фигура 4: Решението в действие. След 24-часовия период на възстановяване, нивото на съхранение се стабилизира.
  • Разходи за съхранение: Стабилизирани.
  • Дължина на списъка с история: Спадна от 4,4 милиарда до почти нула.
Урокът?

При голям мащаб DELETE е скъпа операция. Понякога най-добрият начин за почистване е премахването на цели сегменти, а най-сигурният път за миграция – повторното записване на данните от приложението.