Миграция не применяется¶
Runbook по типовым сбоям make migrate-up / golang-migrate CLI. Открывай в
том же порядке, в каком идут секции: сначала пойми симптом, потом причину,
потом проверку, потом фикс. Правила написания миграций — в
../how-to/add-migration.md и
../conventions/db-pgx.md.
Прежде чем делать любые действия в prod-БД — зафиксируй текущее состояние:
Это единственный «источник правды» для golang-migrate. Без этой записи
ты не знаешь, что именно сломалось.
Содержание¶
- 1.
database "<name>" does not exist - 2.
no migration files found - 3.
dirty database version N - 4.
relation "<table>" does not exist/column ... does not exist - 5.
deadlock detected - 6.
could not obtain lock/ ожидание advisory lock - 7.
canceling statement due to statement timeout - 8.
SSL off/SSL required - 9. Миграция применилась, но код падает с
column not found - 10. Down-миграция ломается на FK
- Общие команды диагностики
- Быстрый порядок действий
- Связанные разделы
1. database "<name>" does not exist¶
Симптом. CLI падает на первом же шаге, до применения миграций.
Причина.
- Локально: docker-compose ещё не закончил bootstrap Postgres, healthcheck
не прошёл, а make migrate-up уже запустился.
- В CI/prod: переменные окружения указывают на БД, которую оператор не
создал (или создал с другим именем).
Проверка.
Альтернатива: psql "$DATABASE_URL" -c 'SELECT 1'.
Фикс.
- Локально: make up → дождись healthcheck (docker compose ps —
колонка Status должна быть healthy) → повтори make migrate-up.
- В CI/prod: сверь DB_NAME/POSTGRES_DB в env и то, что ожидает
сервис (см. internal/config/ в репозитории сервиса). Несоответствие —
частая причина.
2. no migration files found¶
Симптом. CLI находит «0 migrations», хотя файлы лежат на диске.
Причина.
- Текущий рабочий каталог не совпадает с ожидаемым -path.
- В prod-образе Dockerfile не скопировал migrations/ в контейнер.
Проверка.
Внутри контейнера:
Фикс.
- Локально: запускай make migrate-up из корня сервис-репо, либо
указывай абсолютный -path.
- В Dockerfile добавь строку:
и вызывай migrate с -path /app/migrations.
3. dirty database version N¶
Симптом. Любой запуск migrate падает с сообщением «Dirty database version N».
Причина. Прошлый прогон миграции N упал до COMMIT (или между
commit'ом DDL и update'ом schema_migrations). Запись в schema_migrations
остаётся с dirty=true, и CLI дальше не пускает.
Проверка.
Дальше нужно посмотреть глазами саму миграцию 17 и текущее состояние схемы: какие DDL-операции успели применится, какие нет.
Фикс.
- Открой
migrations/017_*.up.sql, пройди по шагам. - Для каждого шага проверь в БД, есть ли этот объект (
\d <table>,\di <index>,information_schema.columns). - Доведи схему до корректного состояния вручную: выполни недостающие
DDL-операции. Ничего «лишнего» сверху не добавляй — только то, что
должно было сделать
017_*.up.sql. - Сбрось dirty-флаг:
Это только помечает миграцию как применённую, DDL заново не выполняет.
5. migrate -database "$DATABASE_URL" -path ./migrations up — он должен
подхватить следующие миграции, если они есть.
Запрещено: делать force N без сверки схемы. Ты зафиксируешь в
schema_migrations состояние, которого в БД нет — следующие миграции
будут ссылаться на несуществующие объекты и падать.
4. relation "<table>" does not exist / column ... does not exist¶
Симптом. Миграция применилась (не dirty), но код падает при первом же SQL-запросе.
Причина.
- Код задеплоили раньше миграции (CD-pipeline не в том порядке).
- Код ожидает колонку, которая появится только в следующей миграции.
- DATABASE_URL у сервиса и у migrate-job'а указывают на разные БД.
Проверка.
Сравни с migrations/ в репо.
Фикс.
- В prod — только forward: напиши новую hotfix-миграцию с
недостающим DDL. Не правь уже применённую миграцию (см.
../how-to/add-migration.md §12).
- Если код опередил миграцию — откати код до предыдущего релиза, затем
применяй миграцию, затем накатывай код.
- Если URL указывает на чужую БД — правь конфиг, пересоздавай pod.
5. deadlock detected¶
Симптом. Миграция висит десятки секунд, потом падает с
deadlock detected или canceling statement due to lock timeout.
Причина. Долгий ALTER TABLE пересекается с боевыми запросами к
той же таблице. Postgres ждёт, пока таблица освободится, упирается в
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;
Фикс.
- Вставь SELECT pg_advisory_xact_lock(hashtext('<svc>_migrations')); в
начало up.sql — это не избавляет от lock'ов на бизнес-таблицу, но
гарантирует, что две реплики сервиса не пытаются применять миграцию
одновременно.
- Долгий ALTER TABLE (особенно ADD COLUMN ... NOT NULL на большой
таблице) — выноси в maintenance-окно или разбей на expand-contract
(см. ../how-to/add-migration.md §4).
- CREATE INDEX CONCURRENTLY — без BEGIN/COMMIT, не держит запись,
но занимает минуты — тоже в окно низкой нагрузки.
6. could not obtain lock / ожидание advisory lock¶
Симптом. Миграция висит на первом SELECT pg_advisory_xact_lock(...)
и не двигается.
Причина. Другая реплика сервиса (или параллельный make migrate-up)
уже взял advisory lock и применяет миграцию. Твой запуск корректно
ждёт — это защита от двойного применения.
Проверка.
SELECT pid, state, wait_event, query
FROM pg_stat_activity
WHERE query ILIKE '%pg_advisory_xact_lock%'
OR query ILIKE '%pg_advisory_lock%';
Фикс. - Если ожидание < 1–2 минут — подожди, всё штатно. - Если > 10 минут — найди зависший PID, проверь его текущий запрос:
Если это «висящая» миграция с прошлого упавшего прогона (процесс убит,
транзакция не закрыта) — БД очистит lock при TCP-таймауте, но можно
ускорить: SELECT pg_terminate_backend(<pid>);.
7. canceling statement due to statement timeout¶
Симптом. ALTER TABLE ADD COLUMN ... NOT NULL или backfill-UPDATE
прерывается по таймауту.
Причина. В БД установлен statement_timeout (обычно 30–60 секунд), а
операция переписывает всю таблицу.
Фикс.
- Разбей на expand-contract: сначала ADD COLUMN nullable, потом batched
backfill из прикладного кода (не из миграции), потом SET NOT NULL
отдельной миграцией — см.
../how-to/add-migration.md §4.
- Никогда не обходи statement_timeout командой
SET statement_timeout = 0 внутри миграции — это заморозит таблицу
на неопределённое время.
8. SSL off / SSL required¶
Симптом. Connection error до применения первой миграции:
no pg_hba.conf entry, SSL is required, SSL off.
Причина. sslmode в DSN не совпадает с настройкой сервера.
Фикс.
- Локально: sslmode=disable — Postgres из docker-compose без TLS.
- Prod: sslmode=verify-full с указанием CA-файла через sslrootcert=...
в DSN или через env-переменную. Подробнее о сборке DSN — в
../conventions/configuration.md §DSN.
9. Миграция применилась, но код падает с column not found¶
Симптом. schema_migrations показывает самую свежую версию, \d по
таблице тоже видит новые колонки — а сервис на старте падает.
Причина. - Сервис подключён к другой БД (другой namespace в k8s, другой instance). - Локально pgx-пул кэширует prepared statements с предыдущей версией схемы — рестарт сервиса исправляет. - Прошла только up-миграция на «одной» реплике БД; в другой реплике (read-replica) репликация отстаёт.
Проверка.
Сравни с тем, что видит сервис.
Фикс. Рестарт сервис-pod'а. Если помогло — было кеширование prepared statements. Если не помогло — разбирайся с URL и репликацией.
10. Down-миграция ломается на FK¶
Симптом. make migrate-down падает с
cannot drop table ... because other objects depend on it.
Причина. В down-скрипте DROP TABLE не учитывает, что за время
жизни schema появились зависимые таблицы (FK из других миграций).
Фикс.
- В dev/staging — DROP TABLE ... CASCADE допустим, но только в
down-файле и только если готов потерять зависимые данные.
- В prod — rollback через forward-миграцию. Down-миграции в prod
не применяем. Пиши новую up-миграцию, которая возвращает схему в
нужное состояние.
- Если down-операция необратима (данные утрачены) — оставь в down-файле
комментарий -- cannot be reversed: data loss on forward migration и
больше ничего.
Общие команды диагностики¶
# текущее состояние schema_migrations
migrate -database "$DATABASE_URL" -path ./migrations version
# насильно пометить миграцию как применённую (после ручной проверки схемы)
migrate -database "$DATABASE_URL" -path ./migrations force <N>
# применить все pending миграции
migrate -database "$DATABASE_URL" -path ./migrations up
# откатить одну миграцию (dev/staging)
migrate -database "$DATABASE_URL" -path ./migrations down 1
-- что применено
SELECT version, dirty FROM schema_migrations ORDER BY version DESC LIMIT 10;
-- какие таблицы в схеме сервиса
\dt <service>.*
-- структура конкретной таблицы
\d <service>.<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;
-- висящие транзакции дольше минуты
SELECT pid, state, xact_start, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND xact_start < NOW() - INTERVAL '1 minute';
Быстрый порядок действий¶
SELECT * FROM schema_migrations— что в БД помечено как применённое.ls migrations/— что должно быть применено.- Если расхождение в версиях — смотри пункт 1 или 3 выше.
- Если
dirty=true— пункт 3 этой страницы. - Если lock/deadlock — пункт 5 или 6.
- Никогда не редактируй уже применённую миграцию. Пиши следующую.
- Перед
force— ручная сверка схемы.
Связанные разделы¶
../how-to/add-migration.md— формат, advisory lock, expand-contract.../conventions/db-pgx.md— pgxpool, транзакции, миграции в runtime-сервисе.../conventions/configuration.md— как собирается DSN.