Skip to Content
How-toДобавить миграцию

Как добавить миграцию

Мы используем Atlas  (versioned mode). Миграции — обычный SQL в migrations/; целостность отслеживается atlas.sum; применяются они отдельным шагом до раската (Argo CD PreSync hook), а не на старте приложения. Принципы (expand-contract, online DDL, least-privilege) — в ../conventions/db-pgx.

Раньше был golang-migrate + *.up.sql/*.down.sql + advisory-lock в каждой миграции + MIGRATE-on-startup. Это устарело — см. миграцию на Atlas внизу.

Содержание

Модель в двух словах

  • Миграция = один forward-SQL-файл migrations/NNNN_name.sql. Down-файлов нет (в prod катимся только вперёд — fix-forward).
  • migrations/atlas.sum — файл целостности (хэши). Генерится Atlas, руками не правится, коммитится вместе с миграцией.
  • Версии применённых миграций Atlas хранит в таблице atlas_schema_revisions (своя, в схеме сервиса). Никакого schema_migrations руками и advisory-lock в каждом файле — Atlas сам берёт session-lock на весь прогон.
  • Применяет миграции PreSync-hook Job (Atlas-образ сервиса) до того, как Argo катит Deployment. Упала миграция → деплой не прошёл, работающий сервис не падает. DDL выполняет выделенная роль <svc>_migrator; рантайм-роль сервиса — только DML.

1. Определить тип изменения

  • Non-breaking: новая таблица, nullable-колонка, индекс. Старый код не ломается; миграция совместима с текущим prod-кодом.
  • Breaking: rename/drop колонки, change type, ADD NOT NULL на большой таблице. Требует expand-contract (несколько миграций + промежуточные деплои кода).

Не уверен — считай breaking.

2. Создать миграцию

Из корня репозитория сервиса:

# создаёт пустой версионированный файл migrations/<ts>_add_review_status.sql # и обновляет atlas.sum atlas migrate new add_review_status --env local

Впиши forward-SQL в созданный файл:

-- migrations/20240612090000_add_review_status.sql ALTER TABLE reviews ADD COLUMN status TEXT;

Пересчитай целостность (после любой ручной правки файлов миграций):

atlas migrate hash --env local

Имя описывает что делает: хорошо — add_review_status, index_reviews_place_id; плохо — fix_bug_PROJ-1234, tmp_revert.

Транзакция: Postgres DDL транзакционен, и Atlas по умолчанию оборачивает каждую миграцию в транзакцию. BEGIN/COMMIT внутри файла не пиши — исключение только для CONCURRENTLY (см. ниже).

3. CONCURRENTLY и не-транзакционные миграции

CREATE INDEX CONCURRENTLY не работает внутри транзакции. Помечай такой файл директивой Atlas — он выполнит его вне транзакции:

-- atlas:txmode none CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_reviews_place_id ON reviews (place_id) WHERE deleted_at IS NULL;

Одна не-транзакционная операция = один файл (не смешивай с обычным DDL).

4. Expand-contract

Rolling deploy = старый и новый код работают одновременно, поэтому breaking- изменения режутся на обратно-совместимые шаги. Пример nickname → username:

  1. expand: ALTER TABLE users ADD COLUMN username TEXT; (nullable) → deploy.
  2. код dual-write: пишет оба поля, читает nickname → deploy.
  3. backfill: заполнить username батчами (см. Backfill safety) — отдельным job, не миграцией.
  4. код: читать username (fallback nickname), писать оба → deploy.
  5. contract-1: ALTER TABLE users ALTER COLUMN username SET NOT NULL; (после проверки COUNT(*) WHERE username IS NULL = 0).
  6. код: только username → deploy.
  7. contract-2: ALTER TABLE users DROP COLUMN nickname;.

4 миграции + 3 деплоя, но zero-downtime. Одна миграция с RENAME COLUMN гарантированно ломает rolling-deploy. atlas migrate lint валит PR на таких несовместимостях — это и есть страховка дисциплины.

5. Триггеры, функции, materialized view

Пишутся как обычный SQL в миграции — Atlas применяет любой SQL, ограничений нет:

CREATE FUNCTION reviews.touch() RETURNS trigger AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER reviews_touch BEFORE UPDATE ON reviews FOR EACH ROW EXECUTE FUNCTION reviews.touch(); CREATE MATERIALIZED VIEW reviews.place_rating AS SELECT place_id, avg(rating) AS rating FROM reviews GROUP BY place_id;

⚠️ Важно про ревью: atlas migrate lint (community) не моделирует триггеры/функции/materialized view в дифф-анализе — он проверит statement-level (деструктив/локи по таблицам), но семантику этих объектов глубоко не разберёт. Поэтому миграции, трогающие их, требуют усиленного ручного ревью:

  • REFRESH MATERIALIZED VIEW берёт AccessExclusiveLock → используй REFRESH MATERIALIZED VIEW CONCURRENTLY (нужен UNIQUE-индекс на mat-view) и выноси тяжёлый refresh из миграции в job/cron.
  • Триггер на hot-таблице добавляет латентность на каждый write — оцени.
  • Изменение/удаление триггера/функции, от которой зависит работающий код, — тоже expand-contract (сначала перестать зависеть, потом дропнуть).

В PR явно отметь, что миграция трогает триггер/функцию/mat-view, чтобы ревьюер посмотрел эти пункты.

6. Lint и локальный прогон

# safety-анализ (деструктив, локи, обратная несовместимость) — то же гоняет CI atlas migrate lint --env ci --latest 1 # применить локально (поднимет твою dev-БД до текущей схемы) atlas migrate apply --env local --url "postgres://localhost:5432/<svc>?sslmode=disable" # что применено / что pending atlas migrate status --env local --url "..."

atlas migrate lint запускается на каждом PR, трогающем migrations/ (workflow atlas-migrate-lint), и роняет PR на опасных изменениях + комментирует результат. Целостность atlas.sum проверяет atlas migrate validate.

7. Как это применяется в кластере

Ничего вручную в проде не запускаем. Поток:

  1. Мержишь PR → CI собирает на один SHA оба образа: app …/<svc>:<sha> и миграций …/<svc>-migrations:<sha> (Atlas CLI + каталог migrations/).
  2. Проставляешь оба tag’а в gitops-overlay сервиса. Бампни и app-образ: PreSync-Job — это Argo-hook, не отслеживаемый ресурс, и смена только migrations-образа не даёт diff → синк не запустится и миграция не применится. Бамп app-тега даёт diff Deployment, который триггерит синк (в обычном потоке миграция и так едет вместе с кодом, так что app-образ меняется сам).
  3. Argo на синке запускает PreSync-hook Job atlas migrate apply --env k8s (под ролью <svc>_migrator, через session-pool алиас <db>_migrate) до Deployment. Успех → катится новый код; провал → деплой блокируется, сервис жив.

На свежей БД (после restore) тот же Job накатывает всё с нуля — схема самовосстанавливается. На уже заполненной БД делается разовый --baseline (см. runbook сервиса).

8. Чего не делать

  • Long lock на hot-таблице: ADD COLUMN ... NOT NULL DEFAULT на большой таблице — разбей (nullable → backfill → SET DEFAULT → SET NOT NULL).
  • CREATE INDEX без CONCURRENTLY на большой таблице — блокирует writes. Используй -- atlas:txmode none + CONCURRENTLY (см. §3).
  • Большой UPDATE одной транзакцией — backfill только батчами и не в миграции (см. Backfill safety).
  • Cross-service/ cross-database в миграции — миграция трогает только свою БД; изменения в чужом сервисе — через событие (outbox + Kafka).

9. Никогда не правь применённую миграцию

Файл миграции, попавший в main, зафиксирован — Atlas хранит его хэш в atlas.sum и сверяет с применённым. Нашёл ошибку → новая миграция поверх (atlas migrate new fix_…), а не правка старого файла. Изменение применённого файла сломает atlas migrate validate (integrity) на всех окружениях.

Backfill safety

Backfill большой таблицы атомарным UPDATE без LIMIT возьмёт row-lock на все строки, выжрет WAL и зависнет. Атомарный backfill запрещён для таблиц > 100k строк. DDL и backfill — в разных шагах: миграция добавляет nullable-колонку / индекс CONCURRENTLY; сам backfill — отдельный Go-job/SQL-скрипт, батчами.

WITH batch AS ( SELECT id FROM reviews WHERE author_slug IS NULL ORDER BY id LIMIT 5000 FOR UPDATE SKIP LOCKED ) UPDATE reviews r SET author_slug = lower(r.author_name) FROM batch WHERE r.id = batch.id;
  • Batch 1000–10000; пауза между батчами (replica catch-up).
  • FOR UPDATE SKIP LOCKED — параллельный запуск не пересечётся.
  • Идемпотентность: WHERE new_col IS NULL отсекает обработанные.
  • Метрика backfill_rows_done_total{migration} + alert rate(...[5m]) == 0 FOR 30m при rows_total > 0.

Checkpointing для больших миграций

Большая = > 10M строк ИЛИ > 1 часа. Однопроходно нельзя (kill pod → с нуля; растущий WAL; replication lag; блок autovacuum). Паттерн:

  • Checkpoint-таблица migration_checkpoints(migration_id PK, last_processed_id, …) в схеме сервиса.
  • Go-Job (не Deployment), restartPolicy: OnFailure, activeDeadlineSeconds.
  • processBatch делает UPDATE строк и UPDATE checkpoint в одной транзакции → после restart возобновляется с последнего батча.
  • Первая ошибка в батче → return err (не continue — молча потеряешь строки).
  • Метрики migration_rows_done_total / _total + alert на «не растёт 15 мин».

Rollback testing на staging

Любая миграция проверяется на staging перед prod. Без этого PR не мержится.

  1. Снимок схемы/данных до (pg_dump --schema-only, --data-only ключевых таблиц).
  2. Применить ровно одну: atlas migrate apply --env <staging> --latest 1.
  3. Smoke сервиса (/healthz + read-heavy + write-heavy endpoint).
  4. Проверить совместимость код↔схема: для expand — сервис v(N-1) работает на схеме v(N); для contract — v(N+1) на v(N).
  5. Откат: в проде только fix-forward; atlas migrate down — лишь на staging/dev (см. rollback-migration).

В описании PR обязательна секция:

## Rollback plan - Strategy: fix-forward / atlas migrate down (dev only) / snapshot restore - Tested on staging: yes / date - Data loss risk: none / bounded (X rows) / catastrophic - Touches trigger/function/materialized view: no / yes (что именно)

Без неё ревьюер не аппрувит.

Чеклист

  • Один forward-файл migrations/<ts>_name.sql; atlas migrate hash прогнан, atlas.sum обновлён.
  • CREATE INDEX CONCURRENTLY-- atlas:txmode none, без BEGIN/COMMIT.
  • Breaking разбит на expand-contract.
  • Backfill — отдельным job, батчами (не в миграции).
  • Триггер/функция/mat-view — отмечено в PR, усиленное ревью (локи/refresh).
  • atlas migrate lint --env ci зелёный локально; CI-lint пройдёт.
  • Применённую миграцию не правил — фикс новой миграцией.
  • Rollback plan в описании PR.

Миграция на Atlas: что изменилось

Было (golang-migrate)Стало (Atlas)
NNN_name.up.sql + .down.sqlодин forward NNNN_name.sql, down нет (fix-forward)
advisory-lock в каждом файлеAtlas сам берёт session-lock на прогон
schema_migrations (своя)atlas_schema_revisions (Atlas)
make migrate-up/downatlas migrate apply/status, lint в CI
MIGRATE=true на старте приложенияPreSync-hook Job до раската (decoupled)
рантайм-роль с DDL<svc>_migrator (DDL) + рантайм DML-only
нет авто-проверки безопасностиatlas migrate lint роняет опасный PR
Last updated on