Сегодня мы поделимся решением, которое используем для установки релизов на нескольких системах наших заказчиков. Однако синхронизацию данных на стендах в рамках данного материала рассматривать не будем, поскольку это большой отдельный вопрос, хотя и частично связанный с deploy.

Постановка задачи

Прежде всего нам важно было сократить время, которое мы тратим на установку набора задач (так называемое «техокно») на боевую базу данных (далее Production).

Одной из причин возникновения ошибок при deploy-e часто служит «человеческий фактор». Самое банальное – пропустить один из сотни скриптов релиза или, наоборот, выполнить дважды. Даже если последствия не самые тяжелые, придется тратить время на разбор.

Да что ж такое! Ты снова ошибся, Карл!
Да что ж такое! Ты снова ошибся, Карл!

Итоговая цель – полное исключение из этого процесса сотрудников: обычно время таких работ выпадает на глубокую ночь, а спать-то хочется.

Дано

Для решения задачи у нас есть:

  1. команда компетентных и мотивированных разработчиков БД Oracle;

  2. желание не использовать коробочные решения, а еще разбавить поток бизнес-задач разработкой инструмента;

  3. стенд разработки — DEV;

  4. стенд бизнес-тестирования релиза — TEST_BUSINESS. По факту их 2 – один для четных релизов, второй – для нечетных;

  5. стенд тестирования наката релиза – TEST_DEPLOY. Версии объектов и состав основных справочников совпадают с Production. Команда разработки использует его для отладки установки релиза перед передачей комплекта отделу сопровождения. Это позволяет нам не краснеть за качество установочного комплекта;

  6. стенд, который приближен к бою по наполнению — preProduction. На нем удобно оценивать время работы скриптов, рассчитанных на работу с большим объемом данных, которых просто нет на других тестовых стендах. На данном стенде поддержка проводит свою репетицию установки релиза, дает добро или указывает на то, что наши руки растут не из того места;

  7. основной рабочий стенд системы — Production;

  8. команда сопровождения, которая выполняет установку релиза на 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 и запускающим скриптом, в котором был бы указан порядок скриптов релиза? Причин несколько:

  1. сотрудники поддержки не горят желанием пользоваться sqlplus, и каждый работает с тем инструментом, к которому привык;

  2. в случае, если на каком-либо этапе исполнение прервалось, следует в запускающем скрипте убирать из списка те, что уже отработали (для сокращения времени наката);

  3. для логирования работы скриптов и ошибок при исполнении в таблице логов на БД понадобится дополнительный код в запускающем скрипте.

Итак, в БД создаем таблицу:

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)


  1. oragraf
    11.05.2022 11:59
    +6

    Чего только люди не наизобретают - лишь бы не использовать flydb/liquibase etc.


    1. igoranfinogentov Автор
      12.05.2022 15:21
      -1

      Ответ ниже.


  1. chukov
    11.05.2022 15:41
    +3

    Добрый день. Спасибо за статью.

    В сторону Liquibase не смотрели?


    1. igoranfinogentov Автор
      12.05.2022 15:19
      -1

      Наша итоговая цель, над которой работаем – один микросервис для обслуживания системы. Помимо прочего он включает накат скриптов на БД и деплой приложения. Причем в некоторых случаях это должно происходить синхронно.

      Liquibase предназначен только для наката скриптов на БД. После первичного ознакомления не пришли к решению, как его можно совместить с другими сервисными операциями.

      Поэтому решили, что в нашем случае универсальнее будет сделать свой инструмент. Тут плюс еще в том, что мы можем в любой момент собственный инструмент изменить под процесс, а не наоборот, подстраиваться под возможности стороннего продукта.


      1. aleksey-stukalov
        13.05.2022 20:18
        +4

        А можно как-то подробнее? Задача выглядит как супер-стандартная и решаемая Liquibase-ом. Очень хочется чтобы была раскрыта тема сравнения с Flyway и Liquibase.

        P.S. Пункт "желание не использовать коробочные решения, а еще разбавить поток бизнес-задач разработкой инструмента" из дано вглядит крайне дико и наталкивает на мысли, что на анализ существующих решений не было потрачено необходимого времени.


  1. Avissian
    12.05.2022 11:06

    Не раскрыта тема, можно ли из репозитория поднять новую базу, хотя бы, без данных. Как DDL из скриптов по типу alter table add column попадают в "основные" DDL с create table


    1. igoranfinogentov Автор
      12.05.2022 15:20

      В репозитории мы храним скрипты по созданию объектов, но данная статья немного о другом. Тут описан процесс деплоя релизов, а не переноса и разворачивания стенда с нуля.