Может ли миграция без простоя остаться обратимой?
Спланируйте миграцию без простоя: Strangler-маршрутизация, двойная запись, сверка, теневое чтение и обратимое переключение.

Миграция без простоя возможна только тогда, когда старая система остается рабочей точкой назначения, пока новая не докажет, что готова принять нагрузку. Это кажется очевидным, но многие планы незаметно уничтожают такую возможность. Команда копирует данные, развертывает замену, назначает окно обслуживания и называет остановку «переключением». Простой не предопределен. Он возникает, когда связывают четыре разных действия: смену маршрута, смену записывающей стороны, передачу власти над данными и удаление старого пути.
Более безопасная схема разделяет эти действия, позволяет наблюдать и отменять каждое из них. Запросы проходят через единую точку маршрутизации. Операции записи получают стабильные идентификаторы и допускают повторное воспроизведение. Историческая загрузка занимает известную позицию в потоке изменений. Сверка сравнивает бизнес-смысл, а не форму байтов. Последняя смена маршрута становится небольшой правкой конфигурации, а не прыжком через пропасть.
Это не означает, что пользователь никогда не увидит ошибку. Система может оставаться доступной, хотя отдельный запрос завершился с ошибкой по той же причине, что и до миграции. Обещание уже и полезнее: сама миграция не требует периода, когда сервис отвергает всю работу, а операторы могут вернуть трафик без восстановления вчерашней базы данных.
Отсутствие простоя зависит от маршрутизации
Приложение остается доступным, если у каждого запроса есть рабочая точка назначения на всем протяжении миграции. Репликация данных помогает, но сама по себе доступность не дает. Если клиенты подключаются прямо к заменяемому серверу, решение о маршруте распределено по сотням клиентов, и команда не контролирует переключение.
Поставьте перед обеими реализациями одну управляемую точку принятия решения. Это может быть API-шлюз, обратный прокси, правило балансировщика, назначение потребителя сообщений или адаптер внутри существующего процесса. Технология менее важна, чем договор: операторы должны менять назначение узкой части работы без повторного развертывания всех вызывающих программ.
Выбирайте эту часть по устойчивой бизнес-границе. Маршрутизируйте чтение счета отдельно от его изменения, экспорт счета-фактуры отдельно от создания или одного арендатора отдельно от остальных. Не режьте систему по файлу контроллера, который проще заменить. Маршрут, смешивающий чтение, запись, плановые задания и обратные вызовы, может показать зеленый канареечный тест, пока незаметный путь меняет старые данные.
Минимальная запись маршрута должна быть достаточно простой для проверки во время инцидента:
{
"capability": "invoice.read",
"cohort": "tenant-042",
"destination": "new",
"fallback": "old",
"revision": 17,
"changed_by": "change-1842"
}
capability называет поведение, а не URL. cohort ограничивает охват. fallback указывает, куда маршрутизатор может отправить запрос при сбое новой стороны. Ревизия выявляет одновременные правки, а ссылка на изменение объясняет руководителю инцидента причину переноса. Храните запись с историей аудита, а маршрутизатору велите сохранять последнюю рабочую конфигурацию при недоступности управляющего контура.
Проверка работоспособности должна тестировать именно маршрутизируемую возможность. Процесс может отвечать на /health, но не иметь миграции базы, ключа расшифровки или доступной зависимости. Выполняйте небольшое чтение или синтетическую операцию по той же цепочке, что и настоящий трафик, на специально выделенных данных. Осторожно применяйте автоматический возврат для записи. Повтор чтения на старой стороне обычно безопасен, а повтор принятой записи может создать второй заказ, платеж или дело.
Точка Strangler должна охватывать все входы
Маршрутизация Strangler работает, только когда точка перехвата контролирует все способы входа в бизнес-возможность. Мартин Фаулер в описании шаблона Strangler Fig говорит о постепенной замене вокруг старой системы. Команды запоминают «постепенно» и забывают «вокруг». Если ночное задание, настольный клиент, файловая загрузка или очередь обходят точку, две реализации начинают работать без общей политики.
Составляйте список входов по данным исполнения, а не по архитектурной схеме. Изучите журналы доступа, привязки очередей, настройки планировщика, сетевые потоки, управляющий язык пакетных заданий, вызывающие программы хранимых процедур и исходящие обратные вызовы, которые возвращаются входящей работой. Старая система часто открывает одну операцию через HTTP, терминальную транзакцию и ночной файл. Считайте их одной возможностью с несколькими адаптерами.
Точка перехвата должна нормализовать личность и контекст до отправки. Обеим сторонам нужны одинаковые ID запроса, пользователь, арендатор, решение об авторизации, срок и ключ идемпотентности. Если реализации выводят поля независимо, сверка обвинит бизнес-логику в различиях, возникших на границе. Сохраняйте исходное тело для аудита, если политика разрешает, но передавайте обоим путям версионированный канонический конверт.
Не начинайте маршрутизацию работы с состоянием со случайного процента. Десятипроцентный канареечный маршрут может отправить первый шаг процесса новой стороне, а следующий старой. Выберите детерминированный ключ: арендатор, счет, дело или ID процесса. Маршрутизатор должен отвечать одинаково для этого ключа, пока оператор не изменит правило группы.
Аутентификация тоже служит входом. Если старая система создает сеансы, которые новая не проверяет, первый перенаправленный запрос вынудит пользователя войти заново. Проверяйте существующий сеанс в общей точке и передавайте короткоживущее подписанное утверждение личности либо временно научите обе стороны общему формату сеанса. Не переносите хеши паролей через принудительный сброс, если бизнес сознательно не принял такое нарушение работы.
Показывайте оценку маршрута в каждой трассировке и строке журнала. Записывайте ревизию правила, выбранное назначение, решение о возврате и ключ группы. Если пользователь сообщил, что вчерашний счет отличается от сегодняшнего, надо знать, какая реализация обслужила каждый запрос. Панель только с общими процентами этого не покажет.
Двойная запись требует одного долговечного намерения
Безопасная двойная запись фиксирует одно долговечное бизнес-намерение, после чего независимые обработчики применяют его к каждой модели данных. Опасный вариант вызывает из потока запроса базу A, затем базу B и надеется на два успеха. Между ними неизбежен разрыв: первый commit проходит, второй истекает, а клиент не знает, создаст ли повтор дубликат.
Пусть текущая авторитетная сторона фиксирует бизнес-изменение и событие outbox в одной локальной транзакции. Relay публикует событие, потребитель идемпотентно применяет проекцию в новом хранилище. На раннем этапе старое хранилище остается главным, даже если новая проекция отстает на несколько секунд. После смены маршрута направление можно развернуть, чтобы сохранить возврат.
Конверт записи должен позволять отклонять дубликаты и замечать нарушение порядка:
{
"event_id": "01J7M6R2K8N4T3Q9V5X1",
"aggregate_type": "invoice",
"aggregate_id": "inv-90318",
"aggregate_version": 44,
"operation": "invoice.adjusted",
"occurred_at": "2026-08-14T10:42:31Z",
"payload": {"line_id": "ln-8", "amount_minor": 1250}
}
Потребитель сохраняет event_id в таблице обработанных событий с ограничением уникальности. Версия 44 применяется только после 43, иначе событие ждет отсутствующую версию. Полезная нагрузка использует бизнес-единицы, например минимальные денежные единицы, а не форматированные строки. Повторная доставка возвращает прежний результат и не выполняет операцию снова.
Документация Microsoft о повторах прямо описывает риск: сервис может закончить работу и потерять ответ, поэтому повтор заново выполнит неидемпотентную операцию. Во время миграции это регулярно случается из-за перезапуска relay и изменения сетевых путей. Ключ идемпотентности нужен не только для удобства API. Он доказывает, что две доставки выражают одно намерение.
Некоторые записи нельзя безопасно воспроизвести, особенно вызовы внешней стороны без поддержки идемпотентности. Во время сосуществования держите такой эффект за действующей авторитетной стороной. Реплицируйте получившееся состояние, а не породившую его команду. Одна платежная инструкция из двух систем означает два платежа.
Следите за возрастом outbox, а не только за числом строк. Маленькая очередь с событием, застрявшим на шесть часов, может быть хуже большой очереди, которая нормально сокращается. Показывайте самое старое неопубликованное и непримененное событие по типу агрегата, число повторов, причину dead letter и разрыв версий. Эти сигналы говорят, актуальны ли данные для возврата.
Историческая загрузка должна встретиться с живым потоком
Историческая загрузка закончена, когда ее снимок присоединился к потоку изменений в известной позиции. Копирование строк при работающем производстве преследует движущуюся цель. Если копировщик прочитал счет до обновления, а записал после того, как живой потребитель применил обновление, старый снимок затрет новое состояние.
Есть два надежных приема. Создайте согласованный снимок, связанный с позицией журнала, загрузите его и примените последующие изменения. Либо разрешайте каждый upsert загрузки только при подходящей версии источника, чтобы он не заменил новую проекцию. Синтаксис средств захвата изменений различается, правило общее: у каждой скопированной записи и живого события должен быть сравнимый порядок.
Логическая репликация PostgreSQL показывает пользу и ограничения. Документация говорит, что после начального снимка таблицы изменения идут в порядке издателя внутри одной подписки. Она также предупреждает, что определения схемы и DDL в распространенных версиях не реплицируются, а состояние последовательностей долго требовало отдельной подготовки к переключению. Руководство для точной версии базы входит в runbook. Фраза «репликация догнала» не доказывает готовность цели принимать записи.
Регулируйте загрузку по задержке производства, а не по фиксированному числу строк в секунду. Читайте диапазоны первичного ключа, сохраняйте завершенный диапазон и позицию снимка, делайте каждый блок перезапускаемым. Большие транзакции удерживают журналы, продлевают блокировки и скрывают прогресс. Крошечные тратят ресурсы. Измерьте влияние на источник и выберите блок, который укладывается в эксплуатационные пределы.
Для ошибок преобразования нужно отдельное хранилище. Если старый статус содержит значение, которого нет в новом enum, не превращайте его молча в UNKNOWN. Сохраните ключ и версию источника, версию преобразователя, исходное значение и ошибку. Ответственный за бизнес решит, сопоставить ли значение существующему состоянию, добавить новое или признать старое повреждение.
Не загружайте вычисляемые итоги, пока не определили, кто их пересчитывает. Если новая система выводит баланс счета-фактуры из проводок, а старая хранит изменяемый столбец баланса, сравнивайте проводки и конечный бизнес-баланс, но не копируйте столбец постоянно. Иначе в новой схеме останутся две авторитетные стороны.
Сверка сравнивает инварианты, а не строки
Сверка должна доказать, что обе системы делают одинаковые бизнес-утверждения при разных схемах. Число строк и контрольные суммы находят пропуски, но не годятся как главный приемочный тест после перестройки архитектуры. Нормализованная модель Postgres не повторяет строки COBOL, а клиент TypeScript сериализует поля не так, как настольная программа.
Начните с инвариантов, на которые уже опирается бизнес: каждая проведенная запись принадлежит счету, дебет и кредит сходятся в границе книги, сумма счета-фактуры равна позициям и налогу, у закрытого дела есть событие закрытия, внешняя ссылка уникальна. Выполняйте каждый инвариант на обеих сторонах и сравнивайте по стабильному бизнес-ID.
Для совпадающих полей нормализуйте только различия без бизнес-смысла. Приведите время к одной точности, Unicode к согласованной форме, пустые строки и null к старому поведению, деньги к целым минимальным единицам. Не меняйте регистр идентификаторов и не округляйте числа ради зеленого отчета. Каждое правило способно скрыть дефект, поэтому версионируйте и проверяйте правила.
Практический запрос возвращает расхождения, а не число успешных строк:
SELECT account_id, old_balance_minor, new_balance_minor,
old_balance_minor - new_balance_minor AS delta_minor
FROM migration_account_balance
WHERE old_balance_minor <> new_balance_minor
ORDER BY ABS(old_balance_minor - new_balance_minor) DESC;
Форма вывода важна: оператору нужны бизнес-ID, оба значения и разница. Храните ID запуска сверки, позиции источника и цели, версию запроса, время начала и конца, число расхождений и ограниченный пример. Отчет без позиций невоспроизводим, потому что обе базы продолжают меняться.
Разделяйте расхождения по причине. Транспортный разрыв означает, что событие не пришло. Разрыв порядка означает раннее или позднее прибытие. Дефект преобразования создает неверное целевое состояние из верного источника. Ожидаемые отличия возникают из-за намеренных изменений, например удаления конечных пробелов. Любое необъясненное отличие блокирует расширение группы, даже если оно одно.
Не требуйте нулевого отличия, когда ответ меняется со временем. Истекающее предложение или живой остаток различаются между последовательными чтениями. Заморозьте нужные часы, сравнивайте в одинаковых позициях журнала или проверяйте одобренный бизнесом допуск. «Достаточно близко» по решению команды миграции не считается приемочным критерием.
Теневое чтение показывает поведение до передачи власти
При теневом чтении новая реализация обрабатывает настоящий запрос, а пользователь получает старый ответ. Так находятся семантические дефекты, которые пропускает проверка данных: порядок сортировки, крайние случаи авторизации, округление, локальный формат, отсутствующие записи и другая обработка ошибок. Новый результат отбрасывается, поэтому способ безопасен только без побочных эффектов.
Клонируйте нормализованный запрос в общей точке и задайте жесткий срок короче пользовательского бюджета. Медленная тень не должна задерживать авторитетный ответ. Уберите ненужные секреты и пометьте запрос, чтобы нижележащий код не отправлял письма, не менял кэш, не продлевал сеансы и не создавал платные вызовы.
Сравнивайте структурированный смысл, а не байты ответа. Игнорируйте ID трассировки и созданные отметки времени. Сравнивайте класс статуса, упорядоченные по договору коллекции, решение об авторизации, выбранные поля, категорию ошибки и итоги. Храните обезличенный пример для каждой новой сигнатуры расхождения, а не каждый ответ. Иначе хранилище сравнения станет второй копией чувствительных данных.
Теневая запись с откатом транзакции обычно небезопасна. Внешние вызовы, выделение последовательности, публикация в очередь и триггеры могут выйти за ее пределы. Проверяйте запись на записанном трафике в изолированном стенде или выполняйте чистую логику решения по снимку, отключив адаптер commit. Точно укажите, что проверено.
Сравнение производительности требует такой же аккуратности. Теневой запрос после старого может получить прогретый кэш, а параллельный добавляет нагрузку, которой системы по отдельности не видят. Измеряйте задержку и ресурсы обеих сторон, но не выбирайте победителя без контроля состояния кэша, состава запросов и дополнительной нагрузки.
Рабочий критерий выхода объединяет покрытие и чистое поведение. Отслеживайте проверенные операции, роли авторизации, формы данных и пути ошибок. Десять миллионов обычных поисков счета не доказывают редкий путь отмены. Маршрутизируйте группу после наблюдения или намеренного теста ее реального набора поведения.
Переключение должно быть машиной состояний
Обратимое переключение проводит бизнес-возможность через именованные состояния с защищенными переходами. Запись в календаре может разрешить переход, но часы не решают, безопасен ли он. Операторы должны видеть на одной странице власть, маршрут, направление репликации, задержку и действие возврата.
Используйте состояния, описывающие факты:
old_only: старая сторона обслуживает и пишет, новая может быть пустой.old_authority: обе стороны получают текущие данные, пользователи обращаются к старой.new_canary: детерминированная группа идет к новой стороне, старая остается актуальной.new_primary: весь подходящий трафик идет к новой стороне, старая готова к возврату.new_only: окно возврата закрыто, старые пути изменения отключены.
Каждому переходу нужны машиночитаемые условия. Для new_canary можно требовать отсутствия необъясненных расхождений, событий старше бюджета задержки, полного теневого покрытия важных операций и проверенного возврата маршрута. new_primary добавляет запас мощности, единственного владельца фоновых заданий, готовность поддержки и подтверждение общей власти для входов вне HTTP.
Команда перехода должна быть маленькой и идемпотентной. Она обновляет версионированную запись маршрута, а не развертывает код, меняет схему, очищает очереди и перезапускает обработчики. Если пять действий обязаны произойти в одну минуту, одно опоздает, а возврат станет неоднозначным.
Разделяйте остановку и откат. Остановка замораживает расширение группы при текущих ролях сторон. Откат возвращает трафик старой стороне из-за нарушенного условия. Исправление данных меняет состояние после понимания сбоя. Автоматическое копирование цели назад во время тревоги может распространить повреждение быстрее диагностики.
Отрепетируйте обратный переход в производстве до широкого переключения. Отправьте контрольную группу новой стороне, создайте и измените характерные записи, верните группу старой стороне и убедитесь в доступности и правильности записей. Документ отката, который не перемещал реальное состояние, остается теорией.
Откат прекращается, когда старая сторона перестает учиться
Старая система пригодна для отката, только пока получает все изменения, нужные для возврата власти. Работающие серверы бесполезны, если база отстала после первой записи новой стороны. Определите окно через данные: какие операции реплицируются назад, какая задержка допустима и какие изменения старая модель не выражает.
Обратная репликация усложняется, когда новая архитектура допускает состояния вне старой схемы. Отложите такие функции на время сосуществования или добавьте совместимое представление до переключения. Если новая система поддерживает несколько корректировок, а старая запись только одну сумму, нельзя обещать откат после второй корректировки без возможности сохранить ее в старом пути.
Применяйте последовательность расширения и сжатия схемы. Добавьте поля и читателей, понимающих обе формы. Заполните новую форму. Переключите запись. Наблюдайте. Удалите старую форму после закрытия окна. Разрушительные изменения, повторное использование enum и укороченные поля стирают обратный путь, хотя маршрутизация еще кажется обратимой.
У плановой работы должен быть один владелец. Маршрут может отправить интерактивный трафик новой стороне, пока два планировщика формируют выписки или закрывают дела. Дайте каждому заданию аренду или признак власти из того же состояния и записывайте, какая реализация забрала запуск. При откате перенесите аренду раньше маршрута, если задание меняет данные, читаемые старым путем.
CodeHero включает договор сосуществования в переработку: новая система на Go, Rust или TypeScript проверяется на записанном производственном трафике стендом паритета, а проект поставляется менее чем за 30 дней. Короткое обещание не отменяет приемочные условия клиента и политику отката. Оно не дает спрятать эти решения внутри длинной программы.
Укажите условие, которое заканчивает обратимость. Это может быть первое состояние только для новой системы, удаление обратной репликации, разрушительная правка схемы или окончание соглашения о поддержке двух сторон. Ответственный владелец должен одобрить границу. Если никто ее не назовет, команда обнаружит ее во время инцидента, когда откат уже потерян.
Последняя смена маршрута должна пройти буднично
Финальное переключение безопасно, если меняет только маршрут по умолчанию для возможности, чьи группы уже работали на новой стороне. Код, данные, задания, доступ, панели, инструкции дежурным и механизм отката уже находятся в производстве. Оставшийся риск связан с масштабом, поэтому мощность и очереди важнее функциональной правильности.
До смены сохраните решение с точной ревизией маршрута, позициями репликации, запуском сверки, открытыми исключениями, утверждающим лицом и порогом отката. Проверьте, что старая сторона немедленно принимает полную нагрузку. Сокращение мощности до закрытия окна мало экономит и превращает обратимую правку в восстановление мощности.
В записи решения укажите также владельца каждого фонового задания и известные исключения из паритета. Оператор не должен искать эти сведения в переписке, когда показатель пересек порог. Снимок позиции, версия правила и отчет сверки должны относиться к одному моменту, иначе документ соединит три правильных факта, которые вместе описывают состояние, никогда не существовавшее в системе.
Меняйте маршрут этапами по области отказа. Один арендатор выявляет особенности его данных. Один регион показывает размещение зависимостей. Процент подходит лишь при гарантированной привязке процесса. Между этапами ждите самый медленный значимый job или callback, а не произвольный пятиминутный график.
Следите за тем, что видит пользователь: категорией ошибок, хвостовой задержкой, возрастом очереди, нарушенными инвариантами, отказами авторизации и обращениями поддержки из группы. CPU и память могут быть спокойны, пока счета-фактуры исчезают из поиска из-за остановленного потребителя индекса. Свяжите каждый порог с именованным сигналом и окном наблюдения.
Один оператор должен отвечать за команду перехода, второй за чтение условий. Второй вправе остановить переход без спора в канале инцидента. Автоматически записывайте оба решения. Разделение обнаруживает устаревшие панели, неверно понятые группы и привычную ошибку, когда молчание принимают за согласие.
При срабатывании порога выполните заранее проверенный возврат маршрута и сохраните доказательства. Не импровизируйте исправление вперед, пока копятся ошибки. После стабилизации на старой стороне при необходимости заморозьте целевую запись, сохраните позиции и начните диагностику. Обратимость дает время, только если операторы ею пользуются.
После закрытия окна удалите механизм осознанно. Отключите старых писателей, отзовите учетные данные, остановите обратных потребителей, архивируйте доказательства по политике и сохраните историю маршрута. Забытый relay способен воскресить устаревшие данные через месяцы. Забытый fallback может отправлять редкие запросы в приложение без наблюдения.
Миграция завершена, когда старая система больше не нужна, а не когда первый запрос дошел до новой. До этого считайте состояние маршрута, позицию события и доказательства сверки производственными данными. Если чего-то нет, переключение полагается на память, которая особенно ненадежна во время громкой тревоги.
Вопросы
Возможна ли миграция совсем без простоя?
Да, если у каждого запроса остается рабочее назначение, пока маршрут и власть над данными меняются независимо. Речь идет об отсутствии общей остановки из-за миграции, а не о гарантии успеха любого запроса.
Что такое Strangler-маршрутизация?
Она ставит управляемую точку решения вокруг старой и новой реализаций, а затем переносит по одной возможности или группе. Схема ломается, если задания, очереди, клиенты или callbacks обходят эту точку.
Безопасна ли двойная запись при миграции?
Две прямые записи из потока запроса небезопасны: одна может пройти, а другая нет. Зафиксируйте изменение и outbox вместе, затем идемпотентно примените событие к другому хранилищу.
Как загрузить историю, пока пользователи продолжают запись?
Свяжите снимок с позицией потока и примените последующие события по порядку. Либо разрешайте upsert только по версии источника, чтобы старые данные не затерли новое событие.
Что должна сравнивать миграционная сверка?
Сравнивайте бизнес-инварианты и стабильные идентификаторы, а не сырые строки. Добавьте оба значения, разницу, позиции, версию сверки и доказательства для воспроизведения расхождений.
Когда теневое чтение безопасно?
Для операций без эффектов, если у тени свой срок и она не задерживает пользователя. Пометьте запрос, чтобы нижележащий код не отправлял сообщения, не менял сеанс, не публиковал события и не делал платные вызовы.
Сколько трафика давать канареечной группе?
Начните с детерминированной бизнес-группы, а не случайного процента. Она должна легко возвращаться и покрывать полные процессы, роли, фоновые задания и callbacks.
Что делает переключение обратимым?
Старая сторона должна оставаться актуальной, работоспособной и готовой принять всю нагрузку. Нужны обратный поток данных, совместимые схемы, один владелец задания и проверенный на реальном состоянии возврат.
Когда закрывать окно отката?
На явной и одобренной границе, например при первом состоянии только для новой системы или разрушительной правке схемы. Несколько спокойных часов новой маршрутизации такой границей не считаются.
Что делать после финального переключения?
Отключите старую запись, отзовите учетные данные, остановите обратных потребителей, сохраните доказательства и удалите fallback. Старое приложение выведено из работы, когда ни один поддерживаемый путь не может писать или возвращать туда работу.