Сегодня мы поделимся решением, которое используем для установки релизов на нескольких системах наших заказчиков. Однако синхронизацию данных на стендах в рамках данного материала рассматривать не будем, поскольку это большой отдельный вопрос, хотя и частично связанный с deploy.
Постановка задачи
Прежде всего нам важно было сократить время, которое мы тратим на установку набора задач (так называемое «техокно») на боевую базу данных (далее Production).
Одной из причин возникновения ошибок при deploy-e часто служит «человеческий фактор». Самое банальное – пропустить один из сотни скриптов релиза или, наоборот, выполнить дважды. Даже если последствия не самые тяжелые, придется тратить время на разбор.
Итоговая цель – полное исключение из этого процесса сотрудников: обычно время таких работ выпадает на глубокую ночь, а спать-то хочется.
Дано
Для решения задачи у нас есть:
команда компетентных и мотивированных разработчиков БД Oracle;
желание не использовать коробочные решения, а еще разбавить поток бизнес-задач разработкой инструмента;
стенд разработки — DEV;
стенд бизнес-тестирования релиза — TEST_BUSINESS. По факту их 2 – один для четных релизов, второй – для нечетных;
стенд тестирования наката релиза – TEST_DEPLOY. Версии объектов и состав основных справочников совпадают с Production. Команда разработки использует его для отладки установки релиза перед передачей комплекта отделу сопровождения. Это позволяет нам не краснеть за качество установочного комплекта;
стенд, который приближен к бою по наполнению — preProduction. На нем удобно оценивать время работы скриптов, рассчитанных на работу с большим объемом данных, которых просто нет на других тестовых стендах. На данном стенде поддержка проводит свою репетицию установки релиза, дает добро или указывает на то, что наши руки растут не из того места;
основной рабочий стенд системы — Production;
команда сопровождения, которая выполняет установку релиза на Production (разработчикам запрещено что-либо делать на нем).
Решение задачи
Структура проекта
Сначала упрощаем сборку комплекта скриптов релиза. Для этого используем Git для хранения всех объектов БД, скриптов DML, DDL. Также он крайне удобен в процессе код-ревью задач. Иметь всю историю по изменению объекта никогда не бывает лишним.
Наша структура организации проекта в GIT выглядит так, как указано ниже. Мы следуем ей при сборке релизного комплекта.
Schema_01
Functions
Packages
Procedures
Triggers
…
Schema_02
Functions
Packages
Procedures
Triggers
…
SCRIPTS
task_0001
Instruction.txt (файл, содержит список новых/измененных объектов БД и скриптов в рамках задачи)
Schema_01
Script_1.sql
Script_2.sql
…
Schema_02
Script_1.sql
…
task_XXXX
Instruction.txt (файл, содержит список новых/измененных объектов БД и скриптов в рамках задачи)
Schema_01
Script_1.sql
…
Schema_02
Script_1.sql
…
Для скриптов (DML, DDL) заводится каталог для каждой задачи. Каталог, помимо самих скриптов, содержит управляющий файл Instruction.txt. В нем перечислены все объекты, затронутые в рамках задачи, и скрипты.
Пример содержимого файла Instruction.txt по задаче task_0001:
Schema_01\script_ 1.sql -- {50} создание структур на схеме
Schema_01\script_ 2.sql -- {55} заполнение ранее созданных структур
Schema_01\pk_manage_customer.pck
Schema_02\script_ 1.sql -- обновление данных
Этот управляющий файл используем в дальнейшем для сборки установочного пакета.
По расширению файлов в управляющем скрипте Instruction.txt при сборке однозначно определяем, что это за объект. В примере строка Schema_01\pk_manage_customer.pck – показывает, что это пакет, лежит в каталоге Sсhema_01/Packages. Аналогично для всех остальных типов объектов БД (prc, fnc, trg и т.д.).
В фигурных скобках указываем приоритет исполнения скрипта на БД, если это имеет значение. В рамках одной задачи за приоритет отвечает ее исполнитель. Обязанность по выстраиванию приоритетов в рамках всех скриптов релиза ложится на разработчика, который назначен ответственным за сборку релиза. Просматривать и вникать во все скрипты релиза необходимости нет: достаточно разобраться с теми, которые проявятся на этапе тестирования установки перед передачей группе поддержки.
Работа с GIT проходит стандартно: каждая задача разрабатывается в отдельной ветке, а после код-ревью вливается в ветку релиза. После выката релиза на Production ветка релиза вливается в master, а для следующего релиза выделяется новая ветка.
Сборка релизного комплекта
Для сборки релизного комплекта создали скрипт на python, который по файлу со списком задач релиза taskList.txt
task_0001
task_0002
…
task_xxxx
формирует zip-архив, содержащий все объекты/скрипты релиза, которые определяются по управляющим файлам Instruction.txt каждой задачи.
Таким образом, меняя содержимое Instruction.txt, можно быстро добавить/исключить скрипт по задаче, а через taskList.txt быстро добавить/исключить из релиза задачи полностью. Для нас это весьма актуально, так как состав задач может измениться в самый последний момент. Переформировать полностью комплект релиза в этом случае – минутное дело.
Соглашения по оформлению скриптов задачи
Скрипты по задаче оформляем по принципу: любое повторное выполнение скрипта или его отдельной части не должно деструктивно воздействовать на систему. Это очень важно! Такое маленькое соглашение дарит нам большие возможности.
Любую DML инструкцию можно составить так, чтобы она отработала только один раз, или чтобы повторное выполнение никак не испортило данные. В этом плане мы еще ни разу не сталкивались с неразрешимыми ситуациями.
Для DDL-инструкций существуют ошибки, которые возникают при повторном исполнении. Например, при добавлении поля таблицы, которое уже существует, получим:
ORA-01430: column being added already exists in table
Со временем мы собрали в справочник коды подобных ошибок, которые для наката скриптов считаем легитимными. При прогоне скриптов ошибки из справочника мы игнорируем. Для исполнения всех DDL команд используем процедуру-обертку примерно такого вида:
PROCEDURE P_EXECUTE_DDL(P_SQL_COMMAND VARCHAR2)
AS
BEGIN
EXECUTE IMMEDIATE P_SQL_COMMAND;
EXCEPTION
WHEN OTHERS THEN
IF P_IS_VALUE_IN_LIST(P_LIST_NAME => 'listValidError'
,P_ITEM_VALUE => TO_CHAR(SQLCODE)
) = 'N' THEN
RAISE;
END IF;
END P_EXECUTE_DDL;
Таким образом, «легитимные ошибки» не приводят к падению скрипта наката.
В итоге каждый скрипт по задаче выглядит примерно так:
BEGIN
--Блок 1
P_EXECUTE_DDL(q'{ALTER TABLE TABLE_1 ADD CODE VARCHAR(1 CHAR)}');
P_EXECUTE_DDL(q'{COMMENT ON COLUMN TABLE_1.CODE IS 'Поле 1'}');
INSERT INTO TABLE_1(CODE,…)
SELECT '1'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM INTO TABLE_1 T WHERE T.CODE = '1');
COMMIT;
END;
/
BEGIN
--Блок 2
........
END;
/
Все инструкции скрипта оформляем в анонимный блок, чтобы весь скрипт можно выполнить при необходимости в IDE, а также через EXECUTE IMMEDIATE.
Выбор места хранения установочного пакета
Подумали и решили установочный комплект сохранять в таблицу базы данных. И установку запускать тоже в базе данных через процедуру.
Законный вопрос: почему не воспользовались старым добрым sqlplus и запускающим скриптом, в котором был бы указан порядок скриптов релиза? Причин несколько:
сотрудники поддержки не горят желанием пользоваться sqlplus, и каждый работает с тем инструментом, к которому привык;
в случае, если на каком-либо этапе исполнение прервалось, следует в запускающем скрипте убирать из списка те, что уже отработали (для сокращения времени наката);
для логирования работы скриптов и ошибок при исполнении в таблице логов на БД понадобится дополнительный код в запускающем скрипте.
Итак, в БД создаем таблицу:
CREATE TABLE T_RELEASES
(
ID_RELEASE NUMBER(10) NOT NULL,
NUM_RELEASES VARCHAR2(20 CHAR), --'Номер релиза'
OWNER_NAME VARCHAR2(20 CHAR), --'Название схемы для исполнения'
SCRIPT_NAME VARCHAR2(100 CHAR), --'Название файла скрипта'
SCRIPT_TEXT CLOB, --'Содержимое скрипта'
PRIORITY NUMBER(5) DEFAULT 50, --'Приоритет выполения в релизе'
IS_INSTALLED VARCHAR2(1 CHAR) DEFAULT 'N', --'Признак исполнения'
IS_IGNORE_ERR VARCHAR2(1 CHAR) DEFAULT 'N', --'Признак игнорирования ошибки'
DT_LOAD DATE DEFAULT SYSDATE, --'Дата загрузки'
DT_INSTALL DATE, --'Дата выполнения скрипта'
MSG_ERROR VARCHAR2(1000 CHAR), --'Сообщение ошибки выполнения'
constraint UQ_RELEASES_3UQ unique (NUM_RELEASES, SCRIPT_NAME, OWNER_NAME),
constraint PK_RELEASES primary key (ID_RELEASE)
)
;
Для загрузки релизного комплекта в таблицу T_RELEASES используем самописную java-утилиту. Утилита загружает zip-архив, полученный на этапе сборки:
Для каждого скрипта из архива при загрузке создается отдельная запись в таблице.
Содержимое скрипта помещается в поле SCRIPT_TEXT.
После загрузки архива в БД получаем:
Запуск установочных скриптов
Для выполнения содержимого загруженных в CLOB скриптов написали процедуру. Код упрощён для понимания сути идеи:
PROCEDURE P_START_INSTALL_RELEASE(P_RELEASE_NAME VARCHAR2)
AS
V_CNT_ERR NUMBER;
V_CNT_WORK_JOB NUMBER;
V_CNT_INVALID NUMBER;
V_LOCK_HANDLE VARCHAR2(128 CHAR);
V_LOCK_STATUS NUMBER := 0;
BEGIN
/*
Тут возможен запуск скриптов PRE - обработки
......
*/
-- БЛОК ОСНОВНЫХ СКРИПТОВ РЕЛИЗА
FOR CUR IN (SELECT T.NUM_RELEASES
,T.OWNER_NAME
,T.SCRIPT_NAME
,T.SCRIPT_TEXT
,T.PRIORITY
,T.IS_IGNORE_ERR
FROM T_RELEASES T
WHERE T.IS_INSTALLED = 'N'
AND T.NUM_RELEASES = P_RELEASE_NAME
ORDER BY T.PRIORITY
) LOOP
BEGIN
--ВЫПОЛНЕНИЕ СКРИПТА
IF CUR.OWNER_NAME = 'Sсhema_1' THEN
Sсhema_1.P_EXECUTE_SCRIPT( P_SCRIPT => CUR.SCRIPT_TEXT
,P_IS_IGNORE_ERROR => CUR.IS_IGNORE_ERR
);
ELSIF CUR.OWNER_NAME = 'Sсhema_2' THEN
Sсhema_2.P_EXECUTE_SCRIPT( P_SCRIPT => CUR.SCRIPT_TEXT
,P_IS_IGNORE_ERROR => CUR.IS_IGNORE_ERR
);
..............................
END IF;
--ОБНОВЛЯЕМ СТАТУС СКРИПТА
UPDATE T_RELEASES T
SET T.IS_INSTALLED = 'Y'
WHERE T.SCRIPT_NAME = CUR.SCRIPT_NAME;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
--ОБНОВЛЯЕМ СТАТУС СКРИПТА
UPDATE T_RELEASES T
SET T.DT_INSTALL = SYSDATE
,T.IS_INSTALLED = CASE
WHEN CUR.IS_IGNORE_ERR = 'N' THEN 'N'
ELSE 'Y'
END
,T.MSG_ERROR = SUBSTR(SQLCODE ' ' SQLERRM,1,900)
WHERE T.ID_RELEASE = CUR.ID_RELEASE;
--Если при ошибке текущего скрипта не включено игнорирование - то прерывание выполнения установки
IF CUR.IS_IGNORE_ERR = 'N' THEN
RAISE_APPLICATION_ERROR(-20080, 'КРИТИЧНАЯ ОШИБКА ПРИ ВЫПОЛНЕНИИ СКРИПТА: ' || CUR.SCRIPT_NAME);
END IF;
END;
END LOOP;
/*
Тут возможен запуск скриптов POST - обработки, например, перекомпиляция объектов
......
*/
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SUBSTR(SQLERRM, 1, 256));
END P_START_INSTALL_RELEASE;
P_EXECUTE_SCRIPT – процедура в каждой схеме, которая исполняет содержимое файлов, загруженных в таблицу. Это могут быть, как версионируемые объекты (*.pkg, *.prc, .fnc, …), так и скрипты с изменениями DML и DDL (.sql).
Скрипты, состоящие из нескольких анонимных блоков, разбиваются на части, содержащие только один анонимный блок. Разделителем блоков является единичный слеш (/) в строке. Далее каждый блок исполняется последовательно через EXECUTE IMMEDIATE.
На каждом этапе предусмотрено логирование исполнения скриптов и всех ошибок в таблицу логов. Так мы фиксируем, что, когда и кем было установлено. Во время наката всегда можно посмотреть, какой скрипт исполняется в текущий момент.
Для исполнения процедур динамического SQL, содержащего DDL-инструкции, рекомендуем ознакомится со статьей https://www.sql.ru/faq/faq_topic.aspx?fid=164.
Порядок установки релиза
Чем проще, тем лучше! В нашем случае влияние человеческого фактора свели к минимуму. Инструкция по установке релиза для сопровождения теперь состоит из трех пунктов:
Загрузить в базу данных релизный комплект (zip-архив);
Для запуска установки релиза выполнить хранимую процедуру:
BEGIN
P_START_INSTALL_RELEASE(P_RELEASE_NAME => 'Release_1');
END;
Посмотреть лог установки. Если возникла ошибка, то сообщить дежурному разработчику.
Чтобы свести к минимуму вероятность возникновения ошибки, установка релиза тестируется на двух контурах, которые по структуре объектов идентичны с Production. На первом контуре разработчик отлаживает установочный комплект и передает его сопровождению. На втором (preProduction) сотрудник отдела сопровождения делает контрольную установку.
Но какая-то доля вероятности ошибки всегда будет оставаться – за этим следит дежурный разработчик. В случае возникновения ошибки он проводит ее оценку. Далее мы правим скрипт или проставляем признак игнорирования ошибки, а после запускаем процедуру установки заново. Стоит отметить, что такой сценарий развития событий давно уже не возникал.
Результат решения
После внедрения нашего подхода количество ошибок и вопросов по установке от исполнителей сильно сократилось. Исключены случаи, когда во время установки может быть пропущен скрипт или какая-то его часть; время исполнения и результат фиксируются. В конечном итоге мы добились главного – десятикратно уменьшили общее время, затрачиваемое на установку релиза.
Еще один положительный момент – единый для всех разработчиков стиль оформления скриптов, а не только кода объектов, для которых зафиксированы некие правила.
Заключение
Естественно, для каждой команды разработки со своими правилами, наборами стендов, организацией установки и т.д. данное решение в чем-то может быть неудобным или избыточным.
Это исключительно наш результат, к которому мы пришли не сразу, а через несколько итераций, учитывая не только удобство разработки, но и обратную связь от непосредственных исполнителей deploy-я.
Фрагменты кода приведены в упрощенном виде для иллюстрации метода.
Комментарии (7)
chukov
11.05.2022 15:41+3Добрый день. Спасибо за статью.
В сторону Liquibase не смотрели?
igoranfinogentov Автор
12.05.2022 15:19-1Наша итоговая цель, над которой работаем – один микросервис для обслуживания системы. Помимо прочего он включает накат скриптов на БД и деплой приложения. Причем в некоторых случаях это должно происходить синхронно.
Liquibase предназначен только для наката скриптов на БД. После первичного ознакомления не пришли к решению, как его можно совместить с другими сервисными операциями.
Поэтому решили, что в нашем случае универсальнее будет сделать свой инструмент. Тут плюс еще в том, что мы можем в любой момент собственный инструмент изменить под процесс, а не наоборот, подстраиваться под возможности стороннего продукта.
aleksey-stukalov
13.05.2022 20:18+4А можно как-то подробнее? Задача выглядит как супер-стандартная и решаемая Liquibase-ом. Очень хочется чтобы была раскрыта тема сравнения с Flyway и Liquibase.
P.S. Пункт "желание не использовать коробочные решения, а еще разбавить поток бизнес-задач разработкой инструмента" из дано вглядит крайне дико и наталкивает на мысли, что на анализ существующих решений не было потрачено необходимого времени.
Avissian
12.05.2022 11:06Не раскрыта тема, можно ли из репозитория поднять новую базу, хотя бы, без данных. Как DDL из скриптов по типу alter table add column попадают в "основные" DDL с create table
igoranfinogentov Автор
12.05.2022 15:20В репозитории мы храним скрипты по созданию объектов, но данная статья немного о другом. Тут описан процесс деплоя релизов, а не переноса и разворачивания стенда с нуля.
oragraf
Чего только люди не наизобретают - лишь бы не использовать flydb/liquibase etc.
igoranfinogentov Автор
Ответ ниже.