Получение согласованных больших наборов данных быстро, безопасно и без простоев

За последние несколько месяцев в carwow небольшая команда переработала способ обработки и распространения изменений цен, внесенных дилерскими центрами, производителями и правительствами, а также предложений для пользователей, которые зависят от них.
Таблица базы данных, которая содержит котировки, обращенные к пользователю (известная как таблица factory_order_quotes), является одной из самых больших, старых и наиболее часто используемых на carwow. Часть работы, которую нам пришлось выполнить, заключалась в выполнении многочисленных «обратных засыпок» для переноса конкретных enum значений для столбца status. Данные, необходимые нам для того, чтобы знать, как перенести эти записи, поступили из трех разных баз данных, доступных только при выполнении нескольких внутренних вызовов HTTP API.
Нам нужно было разработать решение, которое позволило бы нам обновлять ~ 50 миллионов записей, полагаясь на данные, поступающие из трех внешних источников, быстро, безопасно, с нулевым временем простоя и без значительного влияния на взаимодействие с пользователем и другие производственные системы. И мы сделали! 😄
Как мы это сделали?
Основным препятствием, с которым мы столкнулись, была возможность получить доступ к данным, которые нам нужны, чтобы решить, в какую новую status запись следует переместить. Выполнение дополнительных запросов или запросов для получения этих данных в режиме реального времени, когда мы выполняли обратное заполнение, не могло быть и речи: это привело бы к значительным накладным расходам, замедляющим обратное заполнение (мы были немного нетерпеливы), а также внесло бы значительную дополнительную нагрузку на другие наши приложения.
Одним из важных атрибутов этой обратной засыпки было то, что мы уже выполнили работу по обновлению действующей логики для соответствующего обновления столбца status, поэтому для переноса существовали только старые записи. Мы также знали, что «пункт назначения» status для любой записи, которую мы хотели заполнить, был стабильным и не изменился в зависимости от того, когда мы запускали миграцию. Это было ключевое свойство обратной засыпки, которую мы выполняли. Знание этого означало, что мы могли предварительно рассчитать все записи, которые нужно было переместить в новый статус, и каков этот статус. И, что лучше всего, мы могли рассчитать это сопоставление с помощью нашего хранилища данных, сведя к нулю влияние на пользователей и производственные системы 🥳
Это также позволило нам избавиться от необходимости в запросах API между приложениями и устранить необходимость в дополнительных запросах к базе данных в хост-приложениях, вместо этого мы могли запускать только UPDATE запросов, уменьшая объем работы, которую нам приходилось выполнять «вживую», на порядок величина!
Так как же выглядел этот процесс?
- Запустите запрос в Snowflake (наше хранилище данных), чтобы получить все необходимые данные из баз данных, обеспечивающих работу нескольких отдельных производственных приложений.
- Выгрузить данные во временную корзину S3, разделенную на 10 тыс. Строк
- В фоновом режиме загрузите файл, загрузите идентификаторы и запустите
UPDATE
Вы можете увидеть скрипт, используемый для выполнения запроса, разделения и загрузки в S3 здесь.
Разделение файлов и постановка в очередь в качестве фоновых заданий были важны. Это означало, что мы могли легко и быстро остановить и запустить обратную засыпку, если нам нужно (если база данных загорелась) с возможностью восстановления. Фактически у нас были «контрольные точки» на каждые 10 тысяч записей. Это также означало, что мы могли запускать несколько потоков заданий, каждый из которых работал с определенным файлом или последовательностью файлов.
Если вам интересно, реализация, используемая для запуска этих заданий, выглядела немного (очень) похожей на эту.
Резюме
Метод, который мы использовали, позволил нам обновить ~ 48,7 миллиона записей (на самом деле дважды, потому что мы реплицируем эту конкретную таблицу в другую базу данных) чуть менее чем за 6 часов! В основном это связано с устранением необходимости в нескольких пакетных чтениях из базы данных и вызовах API в другие службы перед обновлениями. Вместо этого мы выполняли все эти чтения в упреждающем режиме за пределами нашей производственной среды, что позволило нам более или менее последовательно перекачивать обновления в базу данных.
Есть несколько важных предостережений, которые следует принять во внимание, прежде чем пытаться применить аналогичный подход:
- Убедитесь, что все записи в исходном наборе, извлеченном из вашего склада, всегда будут оставаться в этом наборе (если потенциально можно добавить больше записей, это нормально), т.е. набор записей, которые вы хотите перенести, должен быть исправлено в момент, когда вы генерируете отображение из вашего хранилища данных (если есть возможность удалить записи из этого набора, этот метод напрямую не применим!).
- Вам может потребоваться регулировка между запусками записи, возможно, добавление некоторого периода ожидания между постановкой последующих заданий в очередь, чтобы обеспечить распространение записи в следующие базы данных.
- Убедитесь, что выполняемые вами записи обычно не вызывают каких-либо других событий (заданий, публикации событий и т. Д.).
- Если есть какие-то события, которые должны произойти или могут быть инициированы автоматически, убедитесь, что нижестоящие сервисы способны обрабатывать дополнительную нагрузку, вызванную обратной засыпкой, или требуется другое решение (нам пришлось запустить вторичную засыпку на другом из наших приложения, следующие за этим).
- Убедитесь, что у вас есть хороший мониторинг и наблюдение за вашей системой, чтобы убедиться, что она хорошо справляется - хорошо знать, когда дать вашей системе передышку; пока он не сломался 😅
- Мы обнаружили значительные улучшения производительности в наших прогонах обратной засыпки при упорядочивании сброшенного набора идентификаторов, которые мы хотели обновить (т. Е.
ORDER BY id). Я предполагаю, что это связано с тем, что это приводит к меньшей перегрузке (сбоям страниц) памяти в базе данных, хотя мне не удалось найти подтверждающую документацию. YMMV.


В целом мы обнаружили, что этот метод значительно улучшил предыдущие стратегии обратной засыпки, которые мы разработали, и с тех пор использовали его несколько раз (на одной и той же таблице и в других), чтобы мы могли развивать наши модели данных по мере роста бизнеса, изменения требований, и мы столкнулись с проблемами масштабирования.
Были некоторые незначительные проблемы (которые вы, возможно, уяснили из приведенных выше графиков), связанные с загрузкой базы данных, которые нам пришлось внимательно следить и на некоторое время приостановить обратную засыпку, чтобы позволить нашим базам данных / репликам наверстать упущенное. совершает. Благодаря идемпотентному характеру выполняемых заданий и доступному нам мониторингу, мы могли заранее приостановить обратную засыпку, прежде чем оказывать какое-либо влияние на сервисы, ориентированные на пользователей.
Спасибо, что нашли время прочитать это. Я надеюсь, что, возможно, вы или ваша команда сочтете вышеупомянутый метод применимым, чтобы облегчить эволюцию модели данных вашей компании по мере роста вашего проекта и изменения требований (функциональных и нефункциональных)! Если у вас есть какие-либо вопросы или отзывы, напишите мне в комментариях или в твиттере @ CGA1123
До следующего раза! 👋