Почему «окно обслуживания» — плохой план
Задача возникает всякий раз, когда компания переезжает со старой учётной системы на новую (CRM или ERP) или когда платформа перерастает своё хранилище. Инфраструктурная часть — на странице Cloud и DevOps. План «выключим на четыре часа ночью» выглядит просто и почти всегда проваливается одинаково: перенос идёт дольше расчётного, на середине выясняется, что часть записей не проходит валидацию, а откатываться уже поздно — старая система остановлена, новая недозаполнена. К шести утра решение принимает не инженер, а усталость.
Хуже другое: окно обслуживания заставляет проверять результат в момент максимального стресса. Ошибка в сопоставлении полей обнаружится через неделю, когда данные уже изменились, и восстановить их будет неоткуда.
Промышленный приём называется параллельным изменением: сначала расширяем систему, чтобы старое и новое сосуществовали, потом переносим, потом убираем старое (expand — migrate — contract).
Пять фаз перехода
- 01
Двойная запись
Приложение пишет в оба хранилища, читает по-прежнему из старого. Новое хранилище с этого момента получает все свежие изменения — и мы можем спокойно заниматься историей.
- 02
Перенос истории
Фоновый процесс переносит старые записи пакетами, от новых к старым. Перенос обязан быть идемпотентным и прерываемым: его остановят, и не один раз.
- 03
Сверка
Сравниваем содержимое: контрольные суммы по диапазонам, затем построчно по расхождениям. Параллельно включаем теневое чтение — читаем из обоих хранилищ и сравниваем ответы, отдавая пользователю старый.
- 04
Переключение чтения
Чтение переводится на новое хранилище флагом, сначала для доли трафика. Запись по-прежнему идёт в оба — именно это делает откат мгновенным.
- 05
Отключение старого
Только после того, как новое отработало под полной нагрузкой достаточно долго, чтобы застать месячные и квартальные сценарии. Затем удаляется код двойной записи — иначе он останется навсегда.
Двойная запись: где она ломается
Наивная двойная запись выглядит как два вызова подряд и содержит ошибку: если вторая запись упала, у вас уже расхождение, а пользователю вернулась ошибка при успешно сохранённых данных. Ответ на вопрос «какое хранилище главное» должен быть однозначным на всём протяжении перехода.
async function save(record: Record) {
// Источник правды на время перехода — старое хранилище.
await legacy.save(record);
try {
await next.save(record);
} catch (err) {
// Не роняем запрос: пользователь не должен страдать от миграции,
// о которой он не знает. Расхождение фиксируем и чиним фоном.
metrics.increment('migration.dual_write.failed');
await outbox.enqueue({ type: 'migration.resync', id: record.id });
}
}Сверка: контрольные суммы, а не выборочная проверка
Проверка «открыли десять записей, всё совпало» ничего не доказывает. Работающий приём — контрольные суммы по диапазонам: делим данные на интервалы, считаем агрегат по каждому в обоих хранилищах и сравниваем. Расхождение сразу локализуется, и построчно сравнивать нужно только подозрительный интервал.
select
date_trunc('day', created_at) as bucket,
count(*) as rows,
sum(amount) as total,
md5(string_agg(id::text || ':' || amount::text, ','
order by id)) as checksum
from operation
where created_at < :cutoff
group by 1
order by 1;Теневое чтение добавляет второй, независимый сигнал: система обслуживает запрос из старого хранилища, параллельно делает тот же запрос к новому и сравнивает результаты. Различия пишутся в лог. Это ловит то, чего не видит сверка данных, — расхождения в логике выборки: другой порядок сортировки, другое поведение при NULL, другое округление.
Что ломается на самом деле
Записи почти никогда не теряются целиком — теряется их смысл. Список ниже собран из того, что реально всплывает на сверке.
- Пустая строка против `NULL`. В старой базе «нет значения» записывалось как
'', в новой — какNULL. Фильтры перестают находить записи, а отчёты тихо меняют цифры. - Часовые пояса. Время хранилось без зоны и подразумевало локальную; в новой базе —
timestamptz. Операции сдвигаются на несколько часов, и это видно только на границах суток — то есть в отчётах. - Округление денег.
floatв старой системе иnumericв новой дают расхождение в копейки, которое накапливается и не сходится в итогах. - Автоинкременты. Новое хранилище начинает нумерацию с единицы и наезжает на перенесённые идентификаторы. Счётчик выставляется явно, до включения записи.
- Уникальность. В старой базе накопились дубликаты, которых новая схема не допускает. Их нужно разобрать до переноса — это решение бизнеса, а не разработчика.
Как выглядит план отката
План отката — это не «вернём из бэкапа». Пока идёт двойная запись, откат означает переключение флага чтения обратно: старое хранилище всё это время оставалось полным и актуальным. Именно поэтому фаза двойной записи не сворачивается сразу после переключения — она стоит недорого и покупает возможность вернуться.
- Флаг, переключающий чтение, и проверенная процедура его отката.
- Сверка сходится на всех интервалах, а не на выборке.
- Теневое чтение отработало под реальной нагрузкой без расхождений.
- Мониторинг сравнивает ключевые бизнес-показатели до и после — не только технические метрики.
- Назначен человек, который принимает решение об откате, и порог, при котором он это делает.
Частые вопросы
Сколько времени занимает такой переход?
От двух недель для одной таблицы среднего размера до нескольких месяцев для системы с историей в сотни миллионов записей. Основное время уходит не на перенос, а на сверку и на разбор расхождений, которые она находит: именно там всплывают несовпадения смысла полей.
Можно ли обойтись без двойной записи?
Если систему допустимо остановить и объём данных небольшой — да, и это будет дешевле. Двойная запись нужна там, где остановка невозможна или где объём делает перенос многочасовым. Финтех, здравоохранение и любые системы с внешними обязательствами почти всегда попадают в эту категорию.
Что делать с расхождениями, которые нашла сверка?
Разбирать по типам, а не чинить по одному. Единичное расхождение — почти всегда следствие правила, которое сработало иначе; исправив причину, вы закроете сотни строк сразу. Ручная правка отдельных записей до выяснения причины гарантирует повторение.
Как понять, что старое хранилище можно отключать?
Когда новое отработало под полной нагрузкой достаточно долго, чтобы захватить редкие сценарии — закрытие месяца, квартальные отчёты, всплески. Мы не советуем отключать раньше полного цикла отчётности: именно на нём обычно и вскрывается несовпадение округлений.
Источники
Утверждения из статьи можно проверить: ниже первоисточники, а не пересказ.
- 01Martin Fowler — Parallel Change (expand/contract) — приём, на котором строится вся схема перехода
- 02PostgreSQL — Transaction Isolation — что гарантирует база при параллельной записи в обе схемы
Материалы пишут инженеры, работающие на проектах, — но без подписи именем. Причина та же, по которой на сайте нет логотипов клиентов: почти все проекты идут под NDA или white-label, и авторство статьи о платёжном ядре указывает на заказчика не хуже логотипа. Взамен мы отвечаем за текст правилами, а не именами — кроме материалов о собственном открытом коде: те подписаны автором.
Как мы пишем и что проверяем