Прямой ответ
Миграция, которая длится часы, обязана уметь продолжаться после сбоя. Без сохранённой позиции прерывание означает повторный перенос всего объёма, а при объёме в десятки миллионов строк это дополнительные часы.
Контрольная точка хранится в отдельной таблице:
CREATE TABLE migration_progress (
migration_name text PRIMARY KEY,
last_id bigint NOT NULL,
rows_done bigint NOT NULL DEFAULT 0,
rows_total bigint,
status text NOT NULL,
started_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL
);
Запись контрольной точки выполняется в той же транзакции, что и перенос пакета:
with db.transaction():
write_batch(target, rows)
db.execute(
"UPDATE migration_progress SET last_id = %s, rows_done = rows_done + %s, "
"updated_at = now() WHERE migration_name = %s",
(rows[-1].id, len(rows), name),
)
Общность транзакции принципиальна: если позиция сохраняется отдельно, сбой между записью данных и сохранением позиции приводит к пропуску либо повтору пакета; о пакетной обработке — в статье про миграцию больших таблиц.
Возобновление читает последнюю позицию:
last_id = db.scalar(
"SELECT last_id FROM migration_progress WHERE migration_name = %s", name
) or 0
Три требования к повторяемости пакета.
1. Идемпотентность записи. Повторная обработка пакета не создаёт дубликаты. Запись выполняется по первичному ключу с обновлением при конфликте.
2. Детерминированность порядка. Пакеты выбираются по упорядоченному ключу, поэтому позиция однозначно определяет необработанную часть.
3. Устойчивость к изменению данных. Строки, добавленные после начала миграции, обрабатываются отдельным механизмом, а не поиском по позиции.
Метрики прогресса дают оценку завершения:
Gauge("migration_rows_done", "Перенесено строк", ["migration"]).labels(name).set(done)
Gauge("migration_rows_total", "Всего строк", ["migration"]).labels(name).set(total)
Gauge("migration_rows_per_second", "Скорость переноса", ["migration"]).labels(name).set(rate)
Время до завершения вычисляется делением оставшегося объёма на скорость. Оценка нужна для планирования переключения чтения; о метриках — в статье про инструментирование приложения.
Что ломает учёт прогресса
Система учёта даёт корректную картину при выполнении четырёх условий, и нарушение любого из них делает метрики бесполезными.
Таблица состояния обновляется атомарно с переносом данных. Если отметка о переносе записывается отдельной транзакцией, сбой между двумя записями оставляет несогласованное состояние: данные перенесены, а отметка отсутствует либо наоборот.
Пакеты имеют устойчивые границы. Граница по диапазону идентификатора воспроизводима, а граница «первые сто незаполненных строк» смещается при каждой выборке, поэтому учёт по номеру пакета теряет смысл.
Учтённый объём соответствует реально перенесённому. Учёт пакетов вместо строк даёт грубую оценку при неравномерном заполнении, поскольку последний пакет может содержать одну строку.
Прогресс проверяется независимо. Совпадение числа обработанных строк и объёма в целевой структуре подтверждает учёт, а расхождение указывает на пропуск либо дублирование.
Отдельная проблема — учёт при параллельном переносе. Несколько потоков обрабатывают разные диапазоны, и общий прогресс складывается из независимых счётчиков. Отсутствие единой точки сведения делает оценку завершения неточной, поэтому счётчики сводятся в одну таблицу с записью по диапазону.
Практическое следствие: учёт проектируется вместе с механизмом переноса, а не добавляется после. Добавленная позже таблица состояния не отражает реально перенесённые пакеты, и перенос приходится начинать заново для получения корректной картины.
Практика: организация отслеживания
1. Сохраняйте позицию в транзакции с данными. Отдельное сохранение создаёт окно, в котором сбой приводит к пропуску либо повтору.
2. Используйте упорядоченный ключ для выборки пакетов. Идентификатор либо монотонная метка времени обеспечивают однозначность позиции.
3. Делайте запись идемпотентной. Повтор пакета при возобновлении не должен изменять результат.
4. Фиксируйте статус миграции. Значения «выполняется», «завершено», «приостановлено» и «ошибка» позволяют отличить остановку от завершения; о процессе — в статье про миграцию данных.
5. Публикуйте метрики прогресса. Число перенесённых строк, скорость и оценка завершения видны на панели вместо чтения журнала.
6. Логируйте ошибки пакетов отдельно. Сбой одного пакета не должен останавливать весь перенос, а ошибочные строки фиксируются списком.
7. Проверяйте возобновление до начала. Тест остановки и продолжения подтверждает, что позиция сохраняется и читается корректно.
8. Различайте остановку по решению и сбой. Приостановка оператором и аварийное прерывание требуют разных действий при продолжении.
Рекомендации:
- Не полагайтесь на журнал как на источник позиции. Журнал предназначен для разбора, а позиция хранится структурированно.
- Учитывайте изменение схемы во время миграции. Добавление колонки в исходную таблицу может потребовать перезапуска с новой логикой преобразования.
- Сохраняйте число обработанных исключений. Строки, которые не удалось перенести, учитываются отдельно от успешных.
- Проверяйте, что позиция монотонна. Возврат позиции назад означает повторную обработку и требует проверки идемпотентности.
- Документируйте процедуру возобновления. Команда продолжения описывается заранее, поскольку выполняется в стрессовой ситуации.
Практический сценарий: повторный перенос всего объёма после сбоя
Контекст: миграция 18 миллионов строк выполнялась несколько часов. После перезапуска узла процесс начался с начала, поскольку позиция не сохранялась.
- Диагностика. Прогресс отслеживался по журналу, а позиция последнего обработанного пакета не сохранялась. После перезапуска процесс не мог определить, где остановился, и начал с первой строки.
- Причина. Контрольная точка отсутствовала, а идемпотентность записи не проверялась. Повторный перенос выполнялся несколько часов, прежде чем был остановлен вручную.
- Действия. (а) Добавили таблицу прогресса с сохранением позиции в транзакции с записью пакета; (б) запись перевели на вставку с обновлением при конфликте, чтобы повтор пакета не создавал дубликаты; (в) добавили метрики числа перенесённых строк, скорости и оценки завершения; (г) проверили возобновление остановкой процесса и продолжением; (д) добавили фиксацию статуса миграции для отличия остановки от завершения.
- Результат. Последующий сбой привёл к потере одного пакета вместо нескольких часов работы, а оценка завершения видна на панели вместо расчёта по журналу.
Вывод: миграция без контрольной точки не переживает сбой. Сохранение позиции в транзакции с данными и идемпотентность записи делают перенос возобновляемым, а метрики прогресса дают оценку завершения для планирования переключения.
FAQ
Как часто сохранять контрольную точку? С каждым пакетом, в той же транзакции. Отдельное сохранение создаёт окно несогласованности.
Что делать, если пакет обработан частично? Возобновление с позиции последнего завершённого пакета повторяет частично обработанный. Идемпотентность записи делает повтор безопасным.
Как оценить время до завершения? Делением оставшегося числа строк на измеренную скорость. Оценка уточняется по мере выполнения.
Можно ли приостановить миграцию? Да, если статус сохраняется. Продолжение выполняется с последней контрольной точки после возобновления.
Источники: PostgreSQL docs (CREATE TABLE), Prometheus docs (типы метрик), Python docs (logging) — ссылки в шапке.