Перейти к содержанию

Миграция не применяется

Runbook по типовым сбоям make migrate-up / golang-migrate CLI. Открывай в том же порядке, в каком идут секции: сначала пойми симптом, потом причину, потом проверку, потом фикс. Правила написания миграций — в ../how-to/add-migration.md и ../conventions/db-pgx.md.

Прежде чем делать любые действия в prod-БД — зафиксируй текущее состояние:

SELECT * FROM schema_migrations;

Это единственный «источник правды» для golang-migrate. Без этой записи ты не знаешь, что именно сломалось.

Содержание

1. database "<name>" does not exist

Симптом. CLI падает на первом же шаге, до применения миграций.

Причина. - Локально: docker-compose ещё не закончил bootstrap Postgres, healthcheck не прошёл, а make migrate-up уже запустился. - В CI/prod: переменные окружения указывают на БД, которую оператор не создал (или создал с другим именем).

Проверка.

make psql
# если падает — БД действительно нет или до неё нет сети

Альтернатива: 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/ в контейнер.

Проверка.

ls -la migrations/
migrate -database "$DATABASE_URL" -path ./migrations version

Внутри контейнера:

docker compose exec <service> ls -la /app/migrations

Фикс. - Локально: запускай make migrate-up из корня сервис-репо, либо указывай абсолютный -path. - В Dockerfile добавь строку:

COPY migrations /app/migrations

и вызывай 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 дальше не пускает.

Проверка.

SELECT * FROM schema_migrations;
-- version | dirty
-- 17      | t

Дальше нужно посмотреть глазами саму миграцию 17 и текущее состояние схемы: какие DDL-операции успели применится, какие нет.

Фикс.

  1. Открой migrations/017_*.up.sql, пройди по шагам.
  2. Для каждого шага проверь в БД, есть ли этот объект (\d <table>, \di <index>, information_schema.columns).
  3. Доведи схему до корректного состояния вручную: выполни недостающие DDL-операции. Ничего «лишнего» сверху не добавляй — только то, что должно было сделать 017_*.up.sql.
  4. Сбрось dirty-флаг:
migrate -database "$DATABASE_URL" -path ./migrations force 17

Это только помечает миграцию как применённую, 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'а указывают на разные БД.

Проверка.

\d <schema>.<table>
SELECT version, dirty FROM schema_migrations ORDER BY version DESC LIMIT 5;

Сравни с 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, проверь его текущий запрос:

SELECT pid, state, query_start, query
FROM pg_stat_activity
WHERE pid = <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) репликация отстаёт.

Проверка.

SELECT inet_server_addr(), current_database(), current_schema();

Сравни с тем, что видит сервис.

Фикс. Рестарт сервис-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';

Быстрый порядок действий

  1. SELECT * FROM schema_migrations — что в БД помечено как применённое.
  2. ls migrations/ — что должно быть применено.
  3. Если расхождение в версиях — смотри пункт 1 или 3 выше.
  4. Если dirty=true — пункт 3 этой страницы.
  5. Если lock/deadlock — пункт 5 или 6.
  6. Никогда не редактируй уже применённую миграцию. Пиши следующую.
  7. Перед force — ручная сверка схемы.

Связанные разделы