Обратное заполнение (Backfill) унаследованных данных без риска для боевой базы данных
Вы только что применили новую миграцию, которая добавила колонку last_login_at в таблицу user. Изменение схемы прошло гладко. Но теперь вы смотрите на данные: у каждого существующего пользователя в этой колонке стоит NULL. Вся их история входов за прошлую неделю, месяц, год — невидима для нового поля.
Это тот самый момент, когда вам нужен backfill.
Что на самом деле означает Backfill
Backfill — это процесс заполнения данными, которые существовали до применения миграции. Речь не о переносе данных в новую структуру — этим занимаются скрипты миграции. Backfill приводит старые данные в соответствие с новыми правилами, которым теперь следует ваша система.
Ситуации, когда backfill становится необходимым, встречаются часто:
- Вы добавили новую колонку, но у существующих строк нет для неё значений.
- Вы изменили способ хранения адресов: вместо одного текстового поля появились отдельные колонки для улицы, города и почтового индекса.
- Вы внедрили новый расчет, например, скоринг риска для транзакций, но прошлые транзакции никогда не оценивались.
В каждом случае данные существуют, они валидны, но неполны. Система знает, что делать с новыми данными, но старые данные застряли в прежнем формате.
Почему нельзя обработать всё сразу
Наивный подход — выполнить один запрос, обновляющий все строки разом. Для небольшой таблицы из нескольких сотен строк это сработает. Для таблицы с миллионами строк это катастрофа.
Одно массовое обновление блокирует строки, потребляет логи транзакций и замедляет все остальные запросы к той же таблице. Если ваше приложение обслуживает пользователей во время backfill, они столкнутся с тайм-аутами, медленными ответами или ошибками. База данных может даже исчерпать память или место на диске, пытаясь выполнить операцию.
Решение — обрабатывать данные небольшими контролируемыми порциями.
Пакетная обработка: основная техника
Вместо обновления миллиона строк за один раз вы обновляете десять тысяч строк, делаете паузу, а затем обрабатываете следующий пакет. Это называется пакетной обработкой, и это основа безопасного backfill.
Вот как это работает на практике:
Следующая блок-схема иллюстрирует полный цикл backfill, включая проверки идемпотентности и троттлинг:
-- Обработка одного пакета строк, которые всё ещё нуждаются в backfill
UPDATE users
SET last_login_at = (
SELECT MAX(login_time)
FROM login_history
WHERE login_history.user_id = users.id
)
WHERE last_login_at IS NULL
LIMIT 10000;
После выполнения вы проверяете, сколько строк было затронуто. Если количество совпадает с размером пакета, вы ждете несколько секунд и запускаете снова. Если строк меньше — backfill почти завершен.
Выбор правильного размера пакета
Не существует универсального размера пакета, который подходит для любой базы данных. Правильный размер зависит от:
- Мощности вашего сервера базы данных.
- Нагрузки, которую приложение создает на базу данных.
- Сложности логики обновления.
- Доступного пространства в журнале транзакций.
Начните с консервативного размера, например, 5 000 строк. Выполните несколько пакетов и следите за метриками базы данных: загрузка CPU, дисковый I/O, задержка запросов со стороны приложения. Если база данных справляется легко, удвойте размер пакета. Если вы видите скачки задержки или конфликты блокировок, уменьшите размер вдвое.
Цель — найти такой размер пакета, который выполняется за несколько секунд, не оказывая заметного влияния на другие запросы. Пакет, выполняющийся тридцать секунд, вероятно, слишком велик для production-системы под нормальной нагрузкой.
Троттлинг: даем базе данных передышку
Размер пакета контролирует объем работы за одну единицу. Троттлинг контролирует время между этими единицами.
После завершения каждого пакета делайте намеренную паузу перед началом следующего. Эта пауза позволяет базе данных сбросить ожидающие записи, освободить блокировки и обслужить другие запросы без конкуренции с вашим backfill.
Типичный троттлинг — от двух до пяти секунд между пакетами. В часы пик вы можете увеличить его до десяти или пятнадцати секунд. Во время окон обслуживания вы можете уменьшить его до одной секунды или убрать вовсе.
Троттлинг — это ваш предохранительный клапан. Если что-то пошло не так — внезапный всплеск трафика приложения, медленный запрос от другой команды, предупреждение о задержке репликации — вы можете увеличить паузу и дать системе стабилизироваться, прежде чем продолжить.
Идемпотентность backfill
Скрипт backfill должен быть безопасным для многократного запуска. Если пакет не выполнился до конца или вам нужно перезапустить весь процесс, повторный запуск того же скрипта не должен приводить к дублированию данных или ошибкам.
Идемпотентность для backfill обычно означает одно из двух:
- Проверка перед записью: обновляйте только строки, которые всё ещё имеют NULL или старые значения.
- Используйте логику upsert: вставка или обновление в зависимости от того, есть ли у строки новые данные.
Для примера с last_login_at запрос выше уже идемпотентен, потому что он нацелен только на строки, где колонка всё ещё NULL. Если пакет не выполнился после обновления 5 000 строк, следующий запуск пропустит эти строки и продолжит с оставшимися.
Для более сложных backfill, например, пересчета производного значения, вы можете добавить колонку с меткой времени processed_at. Скрипт backfill проверяет, является ли processed_at NULL, перед обработкой каждой строки. После обработки метка времени устанавливается, и последующие запуски пропускают эту строку.
Логирование: деталь, о которой никто не думает, пока всё не сломается
Когда backfill выполняется часами, вам нужно знать, на каком он этапе и работает ли он корректно. Логируйте каждый пакет:
- Номер пакета и временной диапазон.
- Количество обработанных строк.
- Длительность выполнения пакета.
- Все возникшие ошибки.
- Текущий прогресс в процентах или количестве строк.
Этот лог служит двум целям. Во-первых, если backfill неожиданно остановится, вы сможете возобновить его с последнего завершенного пакета, а не начинать заново. Во-вторых, когда backfill завершится, у вас будет запись того, что именно произошло, что помогает при отладке и аудите.
Пример простой записи в логе:
2025-03-15 14:32:01 | Пакет 47 | Обработано 10 000 строк | Длительность 3.2с | Ошибок нет
2025-03-15 14:32:06 | Пакет 48 | Обработано 10 000 строк | Длительность 3.1с | Ошибок нет
2025-03-15 14:32:11 | Пакет 49 | Обработано 10 000 строк | Длительность 3.5с | Ошибок нет
Практический чек-лист для Backfill
Прежде чем запускать backfill в production, пройдитесь по этому списку:
- Размер пакета протестирован на staging-окружении с аналогичным объемом данных.
- Интервал троттлинга настроен и может быть изменен без изменения кода.
- Скрипт идемпотентен — повторный запуск дает тот же результат.
- Логирование фиксирует прогресс пакетов, ошибки и время выполнения.
- Существует план отката: вы можете отменить backfill, если что-то пошло не так.
- Настроен мониторинг для обнаружения деградации производительности базы данных.
- Выполнен пробный прогон на копии production-данных.
Резюме
Backfill — это не одноразовый скрипт, который вы написали и забыли. Это контролируемая операция, которая учитывает, что ваша база данных обслуживает пользователей в то время, как вы изменяете их данные. Пакетная обработка и троттлинг — это не оптимизации, а минимальные требования для безопасного выполнения этой работы. Без них вы находитесь в одном большом запросе от инцидента в production.