Comments 13
На первый взгляд выглядит так, что оба фреймворка решают похожие по смыслу задачи, но немного в разных областях (мы больше интегрируемся с базой данных и Liquibase, в то время как Spring Batch ближе к коду приложения). Поэтому оба имеют право на жизнь и процветание :)
В разных языках программирования есть библиотеки/пакеты/модули, выполняющие одни и те же функции.
Что происходит если один из процессов завершился с ошибкой, с откатом состояния к прошлому варианту, а остальные успешно?
А как вы думаете, что случится при миграции а то и откате (после ошибки) одной большой, нет "гиганской" транзакцией, т.е. если без штук типа ихней "многопоточки"…
Даже умолчим про autovacuum, immutable states, пересбор индексов/статистик и т.п. прелести и сопутствующие эффекты на такой длинной транзакции и огромной базе...
Так мигрировать на горячую не есть камильфо...
Хотя если нужно таки всё неразрывно под DDL, то обычно морозят states (PITR, снапшоты и т.п.), многопоточно сливают изменения в новые таблички, а потом апдейтят/сливают bulk-ом в одной транзакции, и если все без остановки (aka hot), то сверху накатывают изменения из states(point_after — point_before), WAL по PITR и т.п… А так вообще очень сильно зависит от ситуации/требований (например, я утрирую, всё много сильно сложнее, если миграция базы длится неделю и размер diff-а для последующего обновления настолько огромен, что время накатывания оного снова приближается к той же неделе).
Ну и бэкапы никто еще не отменял...
Дальше все зависит от выполняемой задачи, на сколько нам критично мигрировать все данные без ошибки. Часто можно пропустить диапазон с ошибкой и дождаться завершения миграции, затем точечно разобраться с ошибками и после их устранения перезапустить «многопоточку». При перезапусках «многопоточки» работу будут взяты только еще необработанные диапазоны. Например, такой сценарий возможен при миграции полей «заранее», когда приложение их еще не видит у себя.
Есть также вариант с созданием резервной копии таблицы до миграции, если нам нужна возможность откатиться к состоянию до «многопоточки». Дальше делаем или rename или восстанавливаем из нее данные.
В крайних случаях можно переключиться на стендбай с запаздыванием, где «многопоточки» еще не было или восстановиться из бэкапа и донакатить WALы.
Тут еще могут быть и другие варианты проведения миграции и восстановления, зависит от сценариев. На проде у нас критичных случаев, когда бы понадобились варианты 2 или 3 не было.
Немного оффтоп: а зачем так мигрировать вообще? В смысле, вы про "прозрачную миграцию" что-нибудь слышали?
Т.е. собственно процесс "миграции" происходит постепенно (on-the-fly, application-assisted), используя новые классы данных и небольшой background сценарий вызовов сверху:
типа while not ready: get(oldset) + set/alter-update(newset) + del(oldset)
.
Т.е. чтение реализуется в виде:
dataset GetSomeEntry(...) {
// read dataset (from new-table)
...
// if does not exists - try to read from old-table
GetSomeEntry_MIG(...)
...
// read child items:
if (dataset.version < version.current) {
//... use old api ...
} else {
//... use new api ...
}
...
}
Если язык реализации приложения — скриптовый, то оно инжектами/обёртками на стадии загрузки приложения легко реализуется...
Запись только новым API, в новом формате (в новые таблицы/структуры).
А остальное (типа выборки и т.д.) оборачивается view (если необходимо) и/или сливом bulk-ом в materialized/temporary-table в новом формате.
А так всё последовательно в два шага, например:
правкой всех forign constraint/trigger, переименованием таблицы
sometable
вsometable_mig
, созданием новойsometable_new
, c начальным identity равнымmax(sometable_mig.id)
и вьюхойsometable
:
create view sometable as -- simulate new table using old table: select ... from sometable_mig union all select * from sometable_new
Ну или наоборот табличкой
sometable
и viewsometable_migview
, зависит от условий, например сколько в кодовой базе обращений кsometable
не "обернутых" переменными, и/или соотношение insert/delete к select/join (хотя постгрес умеет updatable view и подобные конструкции позволяющие прозрачную миграцию)...
- по полному завершению процесса "миграции" (хоть через неделю — месяц), вторым шагом, удалением view
sometable
, пустой таблицыsometable_mig
, и конечным переименованием таблицыsometable_new
вsometable
.
И накатывания "чистой" кодовой базы, уже без инъекций типа
if (dataset.version < version.current) {...}
Transparent migration позволяет выкатить обновление приложения без миграции, полностью перелопатить просто гигантские базы, практически совсем без отрыва (т.е. hot), без длинных транзакций, с одним WAL (без плясок с бубном вокруг репликации, не трогая ее вовсе) и на 90% используя новый (готовый) API приложения (ну и обернутый практически теми же тестами, что и основной функционал).
Да запросто… было так… лет десять назад.
Кстати, насчет "съедет план", у union как правило он много стабильнее (даже через вью).
Я даже ранее какой-нибудь "поехавший" на OR-IN план union-ами лечил.
Т.е. когда вот это вот:
select * from MT t ...
where t.somefield in (select x.field1 from XT x where $criteria1)
or t.otherfield in (select x.field2 from XT x where $criteria2)
-- ready, 5172 ms
заменялось на:
select * from MT t ...
where t.somefield in (select x.field1 from XT x where $criteria1)
union all
select * from MT t ...
where t.otherfield in (select x.field2 from XT x where $criteria2)
-- ready, 31 ms
Оно еще как-то понятно.
Но когда такое же случалось, просто на большой выборке с OR по нескольким полям (1 к 1, без full-scan, сложных join и т.д.)…
Какого спрашивается.
Спасибо за статью. Читал её ещё в 2018-м. С тех пор пор прошло много времени. У меня есть альтернативная реализация "многопоточки". Там немного bash
для распараллеливания и одна хранимая процедура на PL/pgSQL
, чтобы скрыть все сложности внутри.
#PostgreSQL. Ускоряем деплой в семь раз с помощью «многопоточки»