1. Данные и конфигурация — отдельный класс риска
На этой теме многие впервые понимают, почему миграции данных и конфигурации пугают даже сильных разработчиков. Код откатывается коммитом, а проблему видно глазами компилятора. Данные и настройки ломаются молча, без спецэффектов, но с дорогими последствиями. Это отдельный класс риска.
И здесь важно не склеить этот разговор с первым migration/boot3-pilot на reports/*. Там был маленький read-only Boot 3 pilot без изменений схемы и конфигурационных прыжков. Здесь пример другой — со своей веткой, backup и дисциплиной отката. Задача не расширить тот pilot до опасного масштаба, а показать другой класс риска.
На примере CashFlow Dashboard это видно особенно хорошо. При переходе с Spring Boot 2.7 и Java 8 на Boot 3.x и Java 21 громкие проблемы почти честные: javax.* не компилируется, тесты падают. Коварнее — данные. В модуле отчётов часть событий в локальной зоне, часть — в UTC, а расчёт MRR по-разному трактует одно поле времени. Проект соберётся идеально, а отчёты за месяц разойдутся с реальностью на пару процентов. Компилятор молчит.
То же самое с конфигурацией. Старый код читал REPORT_TZ, новый ждёт cashflow.reports.default-zone, staging живёт на старых переменных — локально всё зелёное, а в staging поведение неожиданное. Не авария, а «что-то странно считает».
Полезно держать перед глазами простое сравнение:
| Что мигрируем | Что обычно помогает поймать ошибку | Чем опасно |
|---|---|---|
| Код | компилятор, тесты, diff | проблема обычно заметна сразу |
| Данные | backup, сверка с baseline, validation report | можно тихо исказить бизнес-результат |
| Конфигурацию | инвентаризация настроек, dry-run, owner review | проект может «работать», но в неправильном режиме |
Именно поэтому для большинства студентов это прежде всего тема на понимание риска. Нет явного доступа, ответственности и согласования — задача не «героически выполнить миграцию», а распознать, почему её нельзя делать наскоком. Иногда профессионализм — это просто не нажать Enter.
2. Границы безопасности до первого изменения
Если кодовую миграцию иногда ещё пытаются начать с духом «разберёмся по дороге», то для данных и конфигурации такой подход почти всегда заканчивается ночным сообщением в чат и знакомством с дежурным инженером. Границы выставляют заранее.
| Граница | Что это означает на практике |
|---|---|
| Явное согласование | кто-то назначенный знает, что миграция начинается и зачем |
| Backup | у вас есть снимок базы и текущей конфигурации до изменений |
|
откат описан до старта, а не после первой аварии |
| Owner review | известен владелец модуля или окружения, который подтверждает шаг |
| Test data first | сначала проверяем на тестовых данных или staging, не на production |
| Production boundary | никакого “быстренько проверим на бою” без отдельного решения |
Здесь полезно думать не категориями бюрократии, а категориями цены ошибки. Добавили поле — терпимо. Перезаписали кривым скриптом даты биллинга у подписок — и уже неважно, насколько красив ваш pull request.
Именно поэтому даже маленький pilot живёт в отдельной ветке и с отдельными backup-артефактами:
mkdir -p backups
pg_dump -Fc cashflow_dashboard > backups/cashflow-pre-utc.dump # снимок базы до миграции
cp config/application.yml backups/application-pre-boot3.yml # снимок текущих настроек
git switch -c migration/reports-utc-pilot # отдельная ветка под pilot
./gradlew test --tests 'reports.*' # базовая проверка до изменений
Этот набор команд не делает ничего волшебного — он просто оставляет вам путь назад. И в миграциях это уже очень много.
Важно и ещё одно правило: если rollback или backup «не нужен» — не пропускайте его молча, а заполните как none, because ...: «data rollback: none, потому что данные не изменяются». Пустота слишком часто означает не «не требуется», а «забыли подумать».
3. Схема expand → backfill → switch → contract
Когда разговор доходит до миграции данных, у новичков часто возникает желание либо «сразу всё переписать», либо бесконечно откладывать. Практика любит середину — схему expand → backfill → switch → contract, где система какое-то время живёт и со старым, и с новым представлением данных.
Возьмём реалистичный сценарий CashFlow Dashboard. В legacy часть событий лежит в legacy_ts, а зона — отдельно в legacy_timezone. Цель — единое поле normalized_ts_utc, чтобы отчёты и MRR не зависели от локальных поясов и трактовок старого формата.
Логика фаз выглядит так:
| Фаза | Что происходит | Насколько реален откат |
|---|---|---|
| Expand | добавляем новое поле, ничего не ломая в старом чтении | откат простой |
| Backfill | переносим старые данные в новый формат пакетами | откат ещё простой |
| Switch reads | чтение переводим на новый столбец через флаг | откат очень дешёвый |
| Switch writes | новые записи пишем и в старое, и в новое представление | откат всё ещё возможен |
| Contract | удаляем старое поле после стабилизации | откат дорогой или невозможный без backup |
Первый шаг — не «переключить всё на новое», а расширить схему:
ALTER TABLE subscription_payments
ADD COLUMN normalized_ts_utc TIMESTAMPTZ; -- новый столбец под UTC
UPDATE subscription_payments
SET normalized_ts_utc = legacy_ts AT TIME ZONE COALESCE(legacy_timezone, 'UTC')
WHERE normalized_ts_utc IS NULL; -- первичный backfill старых записей
CREATE INDEX idx_sub_payments_utc
ON subscription_payments(normalized_ts_utc); -- чтобы чтение не стало медленнее
Дальше начинается самая интересная часть. Старое поле не вырезаем сразу: новое заполняется и проверяется, приложение живёт по старым правилам. Затем чтение переводят на новый столбец через флаг — заметили расхождение, выключили флаг, вернулись к старой логике.
Например, в конфигурации pilot это может выглядеть так:
cashflow:
reports:
use-utc-column: false # сначала читаем старое поле
migration:
dual-write-utc: false # пока не пишем одновременно в два формата
Позже переключаете use-utc-column: true, потом включаете двойную запись. И только после стабилизации, когда validation report подтверждает feature parity, а мониторинг молчит, думают о contract — удалении столбца.
И вот здесь у команды обычно возникает соблазн: «раз всё работает, давайте сразу удалим legacy». Не надо. Удаление — не награда за поведение, а отдельный рискованный шаг. Хочется немедленно — встаньте, налейте чаю и дайте желанию пройти. Старое поле никого не убьёт за одну-две итерации. А ранний DROP COLUMN убивает выходные.
4. Миграция конфигурации
Конфигурация коварна тем, что её ошибки редко выглядят драматично: приложение стартует, отвечает на health-check, проходит часть smoke-проверок — и ведёт себя не так, как вы думаете. Разбирайте её как отдельный мини-проект, а не приложение к коммиту.
В CashFlow Dashboard legacy-настройки легко расползаются по проекту: application.yml, переменные окружения, старый флаг в CI, staging-конфиг, секретный путь к внешнему сервису. После перехода на новую версию Boot часть ключей меняется, часть устаревает, часть читается иначе.
Например, вместо разрозненных REPORT_TZ и USE_LEGACY_TZ разумнее собрать одну точку правды:
cashflow:
reports:
timezone-source: utc-column # теперь отчёты читают новый столбец
default-zone: UTC # единая временная зона по умолчанию
management:
endpoints:
web:
exposure:
include: health,info,prometheus # проверяем после обновления Boot
Главный принцип здесь тот же, что в данных: не прыгать резко. На один релиз держат и старый ключ, и новый: приложение ищет cashflow.reports.default-zone, нет его — временно подхватывает legacy-ключ. Не красиво, зато практичная страховка.
Есть ещё один нюанс: конфигурация — не только прикладные параметры, но и операционные: пути к логам, включённые endpoint’ы, флаги dual-write, параметры планировщика, настройки экспортов, названия секретов. Не путайте «задокументировать конфигурацию» и «раскрыть секреты»: в POST_MIGRATION_NOTES.md указывают имена переменных, источники и поведение — не реальные токены и пароли.
Проще всего думать так: компилятор видит код, тесты видят поведение, а конфигурацию в полном объёме не видит никто, кроме вас и окружения. Поэтому inventory настроек, owner review и dry-run на staging здесь значат не меньше тестов в кодовой миграции.
5. Post-migration documentation
Миграция заканчивается не тогда, когда ветка слилась и CI зелёный, а когда следующий инженер сам во всём разберётся. Именно поэтому post-migration documentation — не бонус, а последняя обязательная часть работы.
Полезно видеть весь пакет артефактов как одну цепочку:
RISK_MAP.md
↓
CHARACTERIZATION_TESTS.md
↓
COMPATIBILITY_MATRIX.md
↓
MIGRATION_PLAN.md → ROLLBACK.md → MIGRATION_VALIDATION_REPORT.md
↓
POST_MIGRATION_NOTES.md + обновлённый runbook/операционная инструкция
POST_MIGRATION_NOTES.md не должен повторять MIGRATION_VALIDATION_REPORT.md построчно — его задача другая: дать короткий, читаемый человеком контекст. Обычно хватает шести секций:
| Секция | Зачем нужна |
|---|---|
| Что изменилось | чтобы через месяц не гадать, что именно мигрировали |
| Как запускать | чтобы локальный и staging-запуск не зависели от памяти автора |
| Как проверяли | чтобы validation evidence было связано с понятным сценарием |
| Known issues | чтобы команда не принимала ограничения за новые баги |
| Rollback path | чтобы откат искали в одном месте, а не по чату |
| Owners и follow-up cleanup | чтобы было ясно, кто наблюдает и что ещё осталось доделать |
Шаблон начала такого документа может быть очень простым:
# POST_MIGRATION_NOTES.md
Owner: @dashboard-tech-lead
Date: 2026-05-24
Ссылки:
- MIGRATION_PLAN.md
- ROLLBACK.md
- MIGRATION_VALIDATION_REPORT.md
## Что изменилось
## Как запускать и проверять
## Known issues и следующий cleanup
Заметьте, здесь нет попытки снова пересказать весь validation report. Документ скорее работает как навигационная карта: вот что поменяли, вот как жить дальше, вот где доказательства, вот кто отвечает.
Отдельно полезно обновить операционную инструкцию сервиса. Не обязательно создавать новый файл, если в репозитории уже есть свой runbook или раздел в README для эксплуатации. Но после миграции там должны появиться как минимум четыре вещи: какие метрики смотреть, какой алерт считать критичным, какой флаг или настройка используется для быстрого отката и кто дежурит по этому участку. Это особенно важно для конфигурационных миграций: они любят ломаться не в момент релиза, а через несколько часов, когда включается реальное окружение и настоящая нагрузка.
Хороший миграционный пакет оставляет после себя очень спокойное ощущение. Через полгода новый человек открывает POST_MIGRATION_NOTES.md, видит, что в отчётах нормализовали время в UTC, чтение перевели на normalized_ts_utc, dual-write уже выключен, проверка описана в MIGRATION_VALIDATION_REPORT.md, откат лежит в ROLLBACK.md, а за участок отвечает конкретный owner. Если такого ощущения нет и документ напоминает археологическую находку с надписью «ну мы что-то тут мигрировали, вроде стало лучше», значит работа ещё не завершена.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ