Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

EF Core миграции в проде: как Database.Migrate() на старте может превратить релиз в тревожную кнопку

Дата публикации: 24-07-2026 03:06:35

Сегодня про EF Core migrations и один очень удобный вызов, который локально экономит время, а в production может связать старт приложения, DDL-блокировки и rollback в одну неприятную историю. Читать далее

Основное содержимое страницы с новостью.

Привет, Хабр! Меня зовут Павел, я ведущий разработчик. Сегодня без Kafka, но снова про место, где прод обычно становится честнее документации: миграции базы данных.

Есть очень удобный код:

var app = builder.Build();

using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync();

await app.RunAsync();

Локально выглядит прекрасно. Приложение стартует, EF Core сам применяет миграции, разработчик доволен, база вроде тоже не возражает.

А потом это попадает в прод. В Kubernetes поднимаются три pod’а, каждый просыпается с мыслью “я сейчас наведу порядок в схеме”, база берет DDL-блокировку, readiness не проходит, релиз нервно смотрит в мониторинг. Где-то рядом человек открывает вкладку с вакансиями, просто чтобы успокоиться.

EF Core migrations в проде

Сам метод не злой. Он полезен в локальной разработке, тестовом окружении, маленьких внутренних сервисах и одноразовых утилитах.

Проблема начинается, когда runtime-migration становится частью обычного production startup.

Первый риск — несколько инстансов. Если сервис масштабируется, миграцию могут попытаться применить сразу несколько процессов. Начиная с EF Core 9 появился механизм блокировки миграций, который защищает от части проблем конкурентного применения. Но он не делает сам подход бесплатным: остальные pod’ы все равно ждут, старт приложения связан с DDL, а релиз зависит от состояния БД.

Второй риск — права. Чтобы приложение применяло миграции, его пользователь в БД должен уметь менять схему. В проде это часто лишнее доверие к коду, который должен обслуживать запросы, а не заниматься ремонтом асфальта под колесами.

Третий риск — непосмотренный SQL. В миграции на C# все может выглядеть невинно:

migrationBuilder.AddColumn<string>(
    name: "Comment",
    table: "Orders",
    nullable: false,
    defaultValue: "");

А в базе это уже конкретная DDL-операция с блокировками, перепроверкой данных и неприятным вопросом: “а сколько строк в Orders?”

Несколько инстансов стартуют миграцию

Несколько инстансов стартуют миграцию

У Microsoft в документации по EF Core прямо перечислены ограничения применения миграций приложением в production: конкурентный запуск, лишние права, отсутствие возможности заранее проверить SQL и риск проблем при откате.

Рекомендуемые варианты спокойнее:

  • сгенерировать SQL-скрипт и применить его контролируемо;

  • использовать migration bundle;

  • запускать миграцию отдельным шагом pipeline или отдельным Kubernetes Job;

  • держать только один исполнитель миграции;

  • не давать обычному приложению DDL-права в production.

То есть приложение должно стартовать как приложение. А не как маленький DBA с доступом к болгарке.

DDL живет по своим правилам

Самая неприятная часть миграций в проде: приложение думает объектами, а база думает таблицами, индексами и блокировками.

Например, в PostgreSQL обычный CREATE INDEX блокирует записи в таблицу. Для больших таблиц часто нужен:

migrationBuilder.Sql(
    """
    CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_orders_created_at
    ON orders(created_at);
    """,
    suppressTransaction: true);

CONCURRENTLY позволяет строить индекс без блокировки обычных INSERT, UPDATE, DELETE, но у него есть цена: операция идет дольше и не может выполняться внутри transaction block. Поэтому в EF Core приходится явно подавлять транзакцию для такого SQL.

То же самое с ограничениями и колонками. Добавить колонку, заполнить ее, поставить NOT NULL, удалить старую колонку — это не всегда одна миграция. В проде это часто несколько релизов.

DDL-операции и production-риски

DDL-операции и production-риски

Expand/contract вместо “сломать и починить”

Самый скучный и поэтому полезный подход — делать схему совместимой с несколькими версиями приложения.

Допустим, надо переименовать status в state.

Плохой вариант:

  1. Переименовать колонку.

  2. Выложить код.

  3. Надеяться, что старые pod’ы уже умерли.

Хороший вариант:

Шаг

Что делаем

Expand

Добавляем новую nullable-колонку state

Dual write

Новый код пишет и в status, и в state

Backfill

Пачками переносим старые данные

Switch read

Код начинает читать state

Contract

После всех проверок удаляем status

Это медленнее, чем “давайте одним ALTER”. Зато при rollback старый код еще понимает схему, а новый код не падает на старых данных.

Expand/contract rollout

Как я бы делал в нормальном релизе

Для production-проекта я бы разделил код и схему.

  1. На build-этапе собрать migration bundle или SQL-script.

  2. На review посмотреть SQL глазами, а не только миграцию на C#.

  3. На staging прогнать на данных, похожих по объему на production.

  4. Перед выкаткой выполнить migration job ровно один раз.

  5. Только потом выкатывать приложение.

  6. Для больших таблиц делать backfill пачками.

  7. Destructive changes переносить в отдельный релиз.

И еще один важный пункт: rollback-план должен быть до миграции, а не после фразы “а почему у нас колонка исчезла?”

Мини-чек-лист перед миграцией

Перед релизом я бы спросил:

  • SQL миграции сгенерирован и просмотрен?

  • Есть ли операции, которые берут долгую блокировку?

  • Проверяли на таблице похожего размера?

  • Нужен ли CREATE INDEX CONCURRENTLY?

  • Можно ли применить миграцию повторно без взрыва?

  • Приложение совместимо со старой и новой схемой?

  • Откат приложения переживет уже примененную миграцию?

  • Есть ли backfill-план?

  • Есть ли метрики длительности миграции и ошибок?

  • У обычного app-user нет лишних DDL-прав?

Главная мысль:

Миграция базы — это часть релиза, а не побочный эффект старта приложения.

Database.Migrate() удобен, пока не становится единственным человеком в комнате с правом менять схему. В production лучше, когда миграции выполняются явно, проверяемо и отдельно от старта сервиса.

В Telegram-канале «Продовый оффсет» отдельно выложу короткий чек-лист для релиза, схему expand/contract и памятку по PostgreSQL DDL: где нужен CONCURRENTLY, где suppressTransaction, а где лучше просто не делать вид, что таблица на 200 млн строк это маленькая DTO.

На что опирался

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

18.18%PostgreSQL индексы в проде (Почему запрос быстрый на dev, медленный на prod, и как индексы могут лечить или добивать систему)2

81.82%Очереди background jobs в .NET (Hangfire/Quartz/Worker Service: retry, дедупликация, блокировки и что делать, когда job «почти точно выполнилась»)9

36.36%API versioning и обратная совместимость (Как менять контракт, не ломая клиентов, мобильные приложения и веру бизнеса в стабильность)4

Проголосовали 11 пользователей. Воздержались 3 пользователя.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Анатомия SQLite-провайдера: уходим от EF Core — типизированное хранилище для десктопа, мобайла и Blazor WASM010.2129-06-2026
2Бенчмаркая foreach и энумераторы: аллокации, которые прятались 10 лет09.4511-07-2026
3[Перевод] Понятие о конечных автоматах: руководство разработчика по предсказуемой логике приложений09.6829-05-2026
4In-memory база врёт: 5 расхождений с продовой БД0707-07-2026
5Метод, которого не существовало: как я собрал локальный RAG для CAD API07.2410-07-2026
6redb.Route — уходим от MassTransit, идём к Apache Camel: Kafka, Scatter‑Gather и транзакции06.8430-06-2026
7Бенчмаркая регресс LINQ: обещали −19%, на четырёх машинах намерил +31%014.7218-07-2026
8Ты не найдёшь эту ошибку. Потому что её нет в твоём коде. Как Self-describing API спасает от чужих рефакторингов5807-07-2026
9Ленивый LINQ: разбираем yield и ленивые вычисления по кирпичикам07.4421-07-2026
10Книга: «Основы DevOps и Software Delivery. Практика развертывания и сопровождения ПО в продакшене»06.507-07-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 8.29. Источник: habr.com.