Миграция не применяется
Runbook по типовым сбоям Atlas-миграций — на PR-гейте (atlas migrate lint), в build миграционного образа и в PreSync-Job (atlas migrate apply). Открывай по порядку: сначала симптом, потом причина, проверка,
фикс. Как писать миграции — в ../how-to/add-migration;
как откатывать — в ../how-to/rollback-migration.
Прежде чем что-то делать в prod-БД — зафиксируй текущее состояние Atlas:
atlas migrate status --env k8sЭто «источник правды»: что применено, что pending, есть ли failed-ревизия.
Журнал лежит в таблице atlas_schema_revisions той же БД.
Содержание
- Где смотреть логи
- 1.
atlas migrate lintупал в PR - 2.
checksum mismatch/atlas.sumразошёлся - 3. Ревизия
failed: миграция упала посередине - 4. PreSync-Job не подключается к БД
- 5.
relation/column does not exist - 6.
deadlock detected/ lock timeout - 7. Ожидание advisory lock
- 8.
canceling statement due to statement timeout - 9. На не-пустой БД нужен baseline
- 10. Permission denied при DDL
- Общие команды диагностики
- Быстрый порядок действий
- Связанные разделы
Где смотреть логи
Миграции применяет PreSync-Job (<svc>-migrate) до раскатки
Deployment. Argo блокирует sync, пока Job не завершится успешно.
# статус и логи последнего PreSync-Job
kubectl get jobs -n core -l app=<svc>-migrate
kubectl logs -n core job/<svc>-migrateВ логах виден вывод atlas migrate apply: какие версии применены, на
каком statement упало. PR-гейт (atlas migrate lint) смотри в выводе
GitHub Actions (workflow atlas-lint).
1. atlas migrate lint упал в PR
Симптом. CI-проверка atlas-lint красная, merge заблокирован.
Причина. Lint прогнал миграцию на одноразовой dev-базе и нашёл
проблему: destructive-изменение без защиты, ADD COLUMN NOT NULL без
default на существующей таблице, неверный порядок, нарушение
backward-compat, или atlas.sum не совпадает.
Проверка (локально, тот же прогон, что в CI).
atlas migrate lint --env ci --latest 1Фикс.
- Перепиши миграцию по expand-contract — см.
../how-to/add-migration. - Для осознанно-опасной операции (в maintenance-окне) пометь её директивой и задокументируй в PR; не глуши lint целиком.
- Если ругается на
atlas.sum— см. §2.
atlas migrate lint не моделирует триггеры/функции/mat-view глубоко —
их корректность проверяется ревью, см.
../how-to/add-migration.
2. checksum mismatch / atlas.sum разошёлся
Симптом. atlas migrate validate / lint / apply падает с
checksum mismatch или atlas.sum: ....
Причина. Файл миграции изменили после генерации, а atlas.sum не
пере-хешировали. Либо кто-то отредактировал уже применённую миграцию.
atlas.sum — это integrity-файл: хеш каждого .sql + суммарный хеш
каталога. Он не даёт незаметно поменять историю.
Проверка.
atlas migrate validate --env ciФикс.
-
Если миграция ещё не применена нигде (новый PR) — пере-хешируй:
atlas migrate hash --env ciи закоммить обновлённый
atlas.sumвместе с.sql. -
Если миграция уже применена (есть в
atlas_schema_revisionsна любом окружении) — не редактируй её. Откати правку файла к исходнику и напиши новую миграцию N+1. Иначе fresh-DB (после restore) получит другое состояние, чем prod.
3. Ревизия failed: миграция упала посередине
Симптом. PreSync-Job упал; atlas migrate status показывает версию в
состоянии failed.
Причина. Миграция N упала на одном из statement’ов (например, SET NOT NULL на данных с NULL). Часть DDL до него уже применилась. Atlas
записал ревизию N как частично применённую (applied < total, есть
error), дальше не идёт.
Проверка.
atlas migrate status --env k8sSELECT version, description, applied, total, error
FROM atlas_schema_revisions
ORDER BY version DESC LIMIT 5;
-- applied < total и error IS NOT NULL → это та самая ревизияПосмотри глазами саму миграцию N и текущую схему: какие шаги прошли, какой упал.
Фикс — два пути.
-
Fix-forward (предпочтительно). Если оставшиеся statement’ы можно до-применить — устрани причину (например, backfill NULL-строк отдельной миграцией перед
SET NOT NULL) и дай PreSync-Job повторить. Atlas продолжит с упавшего шага. Поэтому миграции пишем идемпотентно:IF NOT EXISTS/IF EXISTSна каждом DDL. -
Если до-применить нельзя. Вручную отмени уже применённые DDL этой ревизии под advisory-lock, приведи схему к состоянию
N-1, затем синхронизируй журнал:atlas migrate set <N-1> --env k8satlas migrate setпереписывает журнал ревизий под фактическую схему. Делать только после того, как схема физически приведена кN-1.
Запрещено: atlas migrate set без сверки схемы. Зафиксируешь
состояние, которого в БД нет — следующие миграции пойдут не в ту ветку
DDL. Полный сценарий отката — в
../how-to/rollback-migration.
4. PreSync-Job не подключается к БД
Симптом. Job падает сразу, до применения миграций:
connection refused, password authentication failed, database "..._migrate" does not exist, prepared statement ... already exists.
Причина.
- Atlas берёт session-уровневый advisory lock → подключаться надо
через сессионный PgBouncer-алиас
<db>_migrate(pool_mode=session), а не через транзакционный пул на :6433. Через transaction-pool Atlas ломается на advisory lock / prepared statements. - Секрет
<svc>-migratorне синкнулся (ExternalSecret) → пустой пароль. ATLAS_URLуказывает не на ту БД / безsearch_path/ безsslmode=require.
Проверка.
kubectl get externalsecret <svc>-migrator -n core # SecretSynced=True?
kubectl get secret <svc>-migrator -n core
# проверь ATLAS_URL в Job (без значения пароля)
kubectl get job <svc>-migrate -n core -o yaml | grep -A2 ATLAS_URLФикс.
- URL вида
postgres://$(MUSER):$(MPASS)@data-postgres-pgbouncer.data.svc:6433/<db>_migrate?search_path=<schema>&sslmode=require— обрати внимание на суффикс_migrate(session-pool алиас) иsearch_path. - Если алиаса
<db>_migrateнет — он добавляется в инфра-слое (PgBouncerpgbouncer.ini). Без него Atlas не возьмёт session lock. - Если ExternalSecret не синкнут — проверь запись
/products/core/<svc>/MIGRATOR_PASSWORDв Infisical и ClusterSecretStoreinfisical.
5. relation/column does not exist
Симптом. Миграции применились (нет failed/pending), но код падает при первом же SQL-запросе.
Причина.
- Деплой кода обогнал миграцию (PreSync не отработал / Argo-hook отключён). В норме PreSync-Job блокирует раскатку Deployment, так что это сигнал, что hook не сработал.
- Бампнули только образ
<svc>-migrations, без app-образа. PreSync-Job — это Argo-hook, а не отслеживаемый ресурс: смена одного hook-образа не даёт diff сравниваемых ресурсов → Argo остаётсяSynced→ синк не запускается → hook не срабатывает, миграция остаётся pending (atlas migrate status). В обычном потоке миграционный PR пересобирает и app-образ, и бамп его тега даёт diff Deployment → синк → PreSync. - Код ожидает колонку из следующей миграции (нарушен expand-contract: contract-фаза выкатилась раньше, чем код перестал читать поле).
- Сервис подключён к другой БД (другой namespace / instance).
Проверка.
\d <schema>.<table>
SELECT inet_server_addr(), current_database(), current_schema();atlas migrate status --env k8s # все ли версии примененыФикс.
- В prod — только fix-forward: новая миграция с недостающим DDL. Не правь применённую (см. §2).
- Если код опередил схему —
kubectl rollout undo, дождись PreSync, потом снова раскатывай код. - Если бампнули только migrations-образ — бампни и app-образ (или сделай force-sync с hooks), чтобы Argo запустил синк и PreSync применил миграцию.
- Соблюдай порядок expand-contract: contract-миграция (DROP) — только после того, как ни один работающий код её не читает.
6. deadlock detected / lock timeout
Симптом. Миграция висит десятки секунд, падает с deadlock detected
или canceling statement due to lock timeout.
Причина. Долгий ALTER TABLE пересекается с боевыми запросами к той
же таблице — Postgres ждёт ACCESS EXCLUSIVE lock и упирается в
lock_timeout либо ловит взаимоблокировку.
Проверка.
SELECT pid, state, query_start, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE state = 'active' OR wait_event IS NOT NULL
ORDER BY query_start;Фикс.
- Долгий
ALTER TABLE(особенноADD COLUMN ... NOT NULLна большой таблице) — разбей на expand-contract (см.../how-to/add-migration) или вынеси в maintenance-окно. CREATE INDEX CONCURRENTLYне держит запись, но идёт минутами и не может быть в транзакции — добавь директиву-- atlas:txmode noneв начало файла, чтобы Atlas не оборачивал миграцию вBEGIN/COMMIT.
7. Ожидание advisory lock
Симптом. PreSync-Job висит на старте atlas migrate apply и не
двигается.
Причина. Atlas берёт session advisory lock, чтобы две копии apply не шли параллельно. Другой Job/инстанс уже держит lock и применяет миграцию. Твой запуск корректно ждёт — это защита от двойного применения.
Проверка.
SELECT pid, state, wait_event, query
FROM pg_stat_activity
WHERE query ILIKE '%advisory%' OR wait_event = 'advisory';Фикс.
-
Ожидание < 1–2 минут — штатно, подожди.
-
10 минут — найди зависший PID (процесс убитого прошлого Job, чья сессия не закрылась):
SELECT pid, state, query_start, query FROM pg_stat_activity WHERE pid = <pid>; SELECT pg_terminate_backend(<pid>); -- освободит session lock -
Убедись, что подключение идёт через session-pool алиас
<db>_migrate(на transaction-pool advisory lock «течёт» между бэкендами и ведёт себя непредсказуемо) — см. §4.
8. canceling statement due to statement timeout
Симптом. ALTER TABLE ADD COLUMN ... NOT NULL или backfill-UPDATE
прерывается по таймауту.
Причина. На роли/БД установлен statement_timeout, а операция
переписывает всю таблицу.
Фикс.
- Разбей на expand-contract:
ADD COLUMNnullable → batched backfill отдельным Job’ом, не в миграции →SET NOT NULLотдельной миграцией. См.../how-to/add-migration. - Не обходи
statement_timeoutчерезSET statement_timeout = 0внутри миграции — это заморозит таблицу на неопределённое время под ACCESS EXCLUSIVE lock.
9. На не-пустой БД нужен baseline
Симптом. На существующей БД (схема уже создана руками / старым инструментом) PreSync-Job пытается применить 001..N и падает — объекты уже есть.
Причина. atlas_schema_revisions пуст, Atlas думает, что БД чистая, и
пробует накатить всю историю заново.
Фикс (одноразово). Сделай baseline до текущей версии — запиши 001..N как применённые, ничего не выполняя:
atlas migrate status --env k8s # узнать текущую версию N
atlas migrate apply --env k8s --baseline <N>После baseline обычный PreSync-Job — no-op на этой БД и применяет только
действительно новые миграции. На fresh БД (после restore) baseline
не делай — там вся история должна примениться с нуля. Подробно — в
ATLAS-BASELINE.md оверлея сервиса.
10. Permission denied при DDL
Симптом. permission denied for schema <name> / must be owner of table ... при apply.
Причина. Подключение идёт не под ролью <svc>_migrator (владелец
схемы, имеет DDL), а под app-ролью (только DML, CREATE отозван).
Проверка.
SELECT current_user; -- ожидаем <svc>_migrator
\dn+ <schema> -- owner схемыФикс. В ATLAS_URL PreSync-Job логин должен быть <svc>_migrator
(env MUSER), пароль — из секрета <svc>-migrator. App-роль для миграций
не используется намеренно (least privilege). Если роли/прав нет — это
инфра-слой (migrator_roles.yml): роль владеет схемой через
REASSIGN OWNED, app-роли REVOKE CREATE.
Общие команды диагностики
# статус миграций (применено / pending / failed)
atlas migrate status --env k8s
# integrity миграций и atlas.sum
atlas migrate validate --env ci
# прогнать lint как в CI
atlas migrate lint --env ci --latest 1
# применить pending (обычно делает PreSync-Job, не руками)
atlas migrate apply --env k8s
# логи PreSync-Job
kubectl logs -n core job/<svc>-migrate-- журнал ревизий Atlas
SELECT version, description, applied, total, error IS NOT NULL AS failed
FROM atlas_schema_revisions ORDER BY version DESC LIMIT 10;
-- таблицы в схеме сервиса
\dt <schema>.*
-- структура таблицы
\d <schema>.<table>
-- активность / lock'и
SELECT pid, state, wait_event_type, wait_event, query_start, query
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY query_start;Быстрый порядок действий
kubectl logs -n core job/<svc>-migrate— что сказал apply.atlas migrate status --env k8s— применено / pending / failed.- Если failed-ревизия — §3.
- Если не подключается — §4.
- Если lock/deadlock — §6 / §7.
- Никогда не редактируй применённую миграцию — пиши следующую.
- Перед
atlas migrate set— ручная сверка схемы.
Связанные разделы
../how-to/add-migration— Atlas-модель, формат, expand-contract, триггеры/mat-view.../how-to/rollback-migration— fix-forward, failed-ревизия, восстановление данных.../conventions/db-pgx— pgxpool, транзакции, где проходит граница app-роли и миграций.../conventions/configuration— как собирается DSN.