OTP Release Handling

Материал из Erlang по-русски.

Оригинал этой статьи находится по адресу Release Handling

Автор: Mirrorer
Дата: 12.11.2006
Версия: 1.0



Содержание

Принципы управления релизами

Важной особенностью языка программирования Erlang является возможность менять модули кода во время работы системы, или замена кода, как описано в Erlang Reference Manual.

Основанное на этой особенности приложение SASL, входящее в состав OTP, предоставляет фреймворк для обновления или возврата к более старой версии целого релиза во время выполнения. Это называется управление релизами.

Фреймворк состоит из оффлайновой части (systools) для генерации скриптов и построения пакетов релизов, и онлайновой части (release_handler) для распаковки и установки пакетов релизов.

Необходимо заметить, что минимальная система, основанная на Erlang/OTP, обеспечивающая возможность управления релизами, состоит, таким образом, из приложений Kernel, STDLIB и SASL.

Следующая глава, Сборник рецептов для Appup, содержит примеры .appup файлов для типичных случаев обновления или отката до предыдущих версий, которые легко выполнить во время работы системы. Однако есть много аспектов, который могут усложнить управление релизами. Вот несколько примеров:

  • Сложные или циклические зависимости могут усложнить или даже сделать невозможным принятие решения относительно того, в каком порядке должны выполняться действия при обновлении или откате до старой версии. Зависимости могут быть:
    • между узлами,
    • между процессами,
    • между модулями.
  • В процессе управления релизами нетронутые процессы продолжают нормальное выполнение. Это может привести к тайм-аутам или другим проблемам. Например, новые процессы, созданные во временном промежутке между остановкой процессов, использующих определенный модуль, и загрузкой новой версии модуля, могут выполнить старый код.

Рекомендуется изменять код как можно меньшими шагами, всегда оставляя обратную совместимость.

Требования

Для того, чтобы управление релизами работало правильно, система должна знать о том, какой релиз работает в текущий момент. У системы также должна быть возможность (во время работы) изменить загрузочный скрипт и файл конфигурации системы, которые будут использоваться в случае перезагрузки системы, например функцией heart после перезагрузки. Таким образом, Erlang должен быть запущен в качестве встроенной системы. Для получения информации о том, как это сделать, смотрите Embedded System(Встроенная система).

Чтобы перезагрузка системы работала правильно, также необходимо, чтобы система запускалась с мониторингом сердцебиения (heartbeat), см. erl(1) and heart(3).

Другие требования:

Если этот файл будет найден, он будет автоматически включен в создаваемый пакет релиза.

  • Все версии релиза, за исключением первой, должны содержать файл relup.

Если этот файл будет найден, он будет автоматически включен в создаваемый пакет релиза.

Распределенные системы

Если система состоит из нескольких Erlang-узлов, каждый узел может использовать свою собственную версию релиза. Менеджер релизов это локально зарегистрированный процесс, и он должен вызываться на каждом узле, где требуется обновление или откат до предыдущей версии. Существует также функция управления релизами, которая может использоваться для синхронизации процессов менеджеров управления релизами на нескольких узлах - sync_nodes. См. appup(4).

Функции управления релизами

OTP поддерживает множество функций по управлению релизами (release handling instructions), которые используются при создании .appup файлов. Менеджер процессов понимает подмножество этих функций, низкоуровневые функции (low-level). Для удобства пользователя существует также набор высокоуровневых функций(high-level), которые транслируются в низкоуровневые функции при помощи systools:make_relup.

Ниже описано несколько наиболее используемых инструкций. Полный список функций можно найти в appup(4).

Сначала несколько определений:

Резидентный модуль

Модуль, в котором процесс содержит функцию (и) с хвостовой рекурсией. Если функция с хвостовой рекурсией определена в нескольких моделях, все эти модули будут резидентными по отношению к процессу.

Функциональный модуль

Модуль, который не является резидентным ни для какого процесса.

Необходимо заметить, что для процесса, реализующего поведение OTP, модуль поведения будет резидентным. Модуль обратного вызова будет функциональным модулем.

load_module

Если в функциональном модуле было сделано небольшое изменение, эффективно будет просто загрузить новую версию модуля в систему и удалить старую версию. Это называется простой заменой кода (simple code replacement) и для этого используется следующая инструкция:

{load_module, Module}

update

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

{update, Module, {advanced, Extra}}
{update, Module, supervisor}

update с аргументом {advanced, Extra} используется для изменения внутреннего состояния, как было описано выше. Это приведет к тому, что процесс поведения вызовет функцию обратного вызова code_change, передавая терм Extra и некоторую другую информацию в качестве аргументов. Для получения дополнительной информации обратитесь к страницам мануала для соответствующих поведений, а также к Сборник рецептов для Appup

update с аргументом supervisor используется для изменения спецификации запуска супервизора. См. Сборник рецептов для Appup.

Менеджер релизов находит процесс, использующий модуль, который необходимо обновить путем обхода дерева контроля для каждого запущенного приложения, и проверяя все спецификации потомков:

{Id, StartFunc, Restart, Shutdown, Type, Modules}

Процесс использует модуль, если имя модуля есть в списке Modules в спецификации потомков процесса.

Если Modules=dynamic, что будет верно в случае менеджеров событий, процесс менеджера событий информирует менеджер релизов о списке установленных на текущий момент обработчиков событий (gen_fsm), а также проверяет, используется ли в этом списке имя модуля.

Менеджер релизов приостанавливает работу, делает запрос на изменение кода и восстанавливает выполнение процессов путем вызова функций sys:suspend/1,2, sys:change_code/4,5 и sys:resume/1,2 соответственно.

add_module and delete_module

Если появляется новый модуль, используется следующая функция:

{add_module, Module}

Эта функция загружает модуль и является необходимой при запуске Erlang во встроенном режиме. Она не является обязательной при запуске Erlang в интерактивном (обычном) режиме, поскольку сервер приложения автоматически ищет и загружает незагруженные модули.

Противоположностью add_module является delete_module, которая выгружает модуль из системы:

{delete_module, Module}

Необходимо заметить, что любой процесс, в любом приложении, с резидентным модулем Module, умирает при выполнении указанной функции. Следовательно, пользователь должен убедиться, что все такие процессы прерваны перед удалением модуля, во избежание возможной ситуации с невозможностью перезапуска супервизоров.

Функции приложения

Функция для добавления приложения:

{add_application, Application}

Добавление приложений подразумевает, что модули, описанные ключами modules в файле .app загружены при помощи функций add_module, после чего запускается приложение.

Функция удаления приложения:

{remove_application, Application}

Удаление приложения подразумевает, что приложение останавливается, модули выгружаются с помощью функции delete_module, и после этого спецификация приложения выгружается из контроллера приложения.

Функция удаления приложения:

{restart_application, Application}

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

remove_application и add_application.

apply (низкоуровеневая функция)

Для вызова необходимой функции менеджера релизов используется следующая функция:

{apply, {M, F, A}}

Менеджер релизов выполнит apply(M, F, A).

restart_new_emulator (низкоуровеневая функция)

Эта функция используется при смене версии эмулятора или при необходимости перезагрузки системы по какой-либо причине. Требуется, чтобы система была запущена с постоянным контролем состояния. См. erl(1) и heart(3).

Когда менеджер событий начинает выполнять эту функцию, он завершает работу текущего эмулятора вызовом init:reboot(), см. init(3). Все процессы завершают свою работу, и система может быть перегружена главной программой, используя новую версию релиза. Новая версия должна быть перманентной при запуске и работе нового эмулятора. Иначе в случае перезагрузки системы используется старая версия релиза.

На UNIX системах менеджер релизов сообщает главной программе, какая команда используется для перезагрузки системы. Необходимо заметить, что переменная окружения HEART_COMMAND, обычно используемая главной программой, в этом случае игнорируется. Вместо нее используется $ROOT/bin/start. Другая команда может быть установлена конфигурационным параметром SASL start_prg, см. sasl(6).

Файл обновления приложения

Для указания, как обновлять или откатывать до предыдущей версии текущее приложение, используется файл обновления приложения (application upgrade file), короче говоря .appup файл. Этот файл должен называться Application.appup, где Application – имя приложения:

{Vsn,
 [{UpFromVsn1, InstructionsU1},
  ...,
  {UpFromVsnK, InstructionsUK}],
 [{DownToVsn1, InstructionsD1},
  ...,
  {DownToVsnK, InstructionsDK}]}.

Vsn – строка, обозначающая текущую версию приложения, как описано в файле .app. Каждая UpFromVsn это предыдущая версия приложения, до которого будет производиться обновление, и каждая Donovan это предыдущая версия приложения, до которой будет производиться откат. Каждая Instructions это список функций по управлению релизом.

Синтаксис и содержимое файла appup детально описано в appup(4).

В главе Сборник рецептов для Appup даны примеры .appup файлов для типичных случаев обновления и отката до старых версий

Пример: Предположим у нас есть релиз ch_rel-1 из главы Релизы. Предположим также, что бы хотим добавить функцию available/0 серверу ch3, которая будет возвращать количество возможных каналов.

Подсказка: При работе с примером, делайте изменения в копии оригинальной директории, таким образом, будет доступна исходная версия. 


-module(ch3).
-behaviour(gen_server).

-export([start_link/0]).
-export([alloc/0, free/1]).
-export([available/0]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() ->
    gen_server:start_link({local, ch3}, ch3, [], []).

alloc() ->
    gen_server:call(ch3, alloc).

free(Ch) ->
    gen_server:cast(ch3, {free, Ch}).

available() ->
   gen_server:call(ch3, available).

init(_Args) ->
   {ok, channels()}.

handle_call(alloc, _From, Chs) ->
   {Ch, Chs2} = alloc(Chs),
   {reply, Ch, Chs2};
handle_call(available, _From, Chs) ->
   N = available(Chs),
   {reply, N, Chs}.

handle_cast({free, Ch}, Chs) ->
   Chs2 = free(Ch, Chs),
   {noreply, Chs2}.

Должна быть создана новая версия файла ch_app.app, где версия будет обновлена:

{application, ch_app,
 [{description, "Channel allocator"},
  {vsn, "2"},
  {modules, [ch_app, ch_sup, ch3]},
  {registered, [ch3]},
  {applications, [kernel, stdlib, sasl]},
  {mod, {ch_app,[]}}
 ]}.

Для обновления ch_app с "1" до "2" (или для отката версии с "2" до "1"), нам просто нужно загрузить новую (старую) версию модуля обратного вызова ch3. Мы создаем файл обновления приложения ch_app.appup в директории ebin:

{"2",
 [{"1", [{load_module, ch3}]}],
 [{"1", [{load_module, ch3}]}]
}.

Файл обновления релиза

Для указания, как обновлять или откатывать до предыдущей версии релиз, используется файл обновления релиза(release upgrade file), короче говоря, relup файл.

Этот файл не нужно cоздавать вручную, он может быть сгенерирован при помощи systools:make_relup/3,4. В качестве входных данных используются нужные версии .rel файла, .app файлы и .appup файлы.

Если relup относительно простой, его можно создать вручную. Запомните, что он должен содержать только низкоуровневые функции.

Синтаксис и содержимое файла обновления релиза детально описаны в relup(4).

Продолжим пример из предыдущего пункта. У нас есть версия "2" ch_app и .appup файл. Нам нужна также новая версия .rel файла. В этот раз файл называется ch_rel-2.rel и строка версии релиза изменена с "A" на "B":

{release,
 {"ch_rel", "B"},
 {erts, "5.3"},
 [{kernel, "2.9"},
  {stdlib, "1.12"},
  {sasl, "1.10"},
  {ch_app, "2"}]
}.

Теперь может быть сгенерирован новый файл relup:

1> systools:make_relup("ch_rel-2", ["ch_rel-1"], ["ch_rel-1"]).
ok


Этот вызов сгенерирует relup файл с указаниями, как обновлять от версии "A" ("ch_rel-1") до версии "B" ("ch_rel-2") и как откатиться от версии "B" до версии"A".

Необходимо заметить, что и старые и новые версии файлов .app и .rel должны находится той же директории, что и код, так же, как и .appup и (новые) .beam файлы. Возможно, изменить директорию кода, используя опцию path:

1> systools:make_relup("ch_rel-2", ["ch_rel-1"], ["ch_rel-1"],
   [{path,["../ch_rel-1",
    "../ch_rel-1/lib/ch_app-1/ebin"]}]).
ok

Установка релиза

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

Чтобы установить новую версию релиза во время работы системы, используется менеджер релизов(release handler). Это процесс, принадлежащий приложению SASL, который обрабатывает распаковку, установку, и удаление пакетов релизов. Он используется через интерфейс release_handler, который детально описан в release_handler(3).

Предположим, что целевая система запущена и работает, с установленной корневой директорией $ROOT, пакет релиза с новой версией релиза должен быть скопирован в $ROOT/releases.

Первым делом необходимо распаковать(unpack) пакет релиза, после чего файлы извлекаются из пакета:

release_handler:unpack_release(ReleaseName) => {ok, Vsn}

ReleaseName это имя пакета релиза, без расширения .tar.gz. Vsn это версия распакованного релиза, определенная в .rel файле.

Будет создана директория $ROOT/lib/releases/Vsn, где будут расположены .rel файл, загрузочный скрипт start.boot, конфигурационный файл системы sys.config и relup. Для приложений с новыми версиями, директории приложений будут расположены в $ROOT/lib. Приложения, которые не будут изменяться, не трогаются.

Распакованный релиз может быть установлен(installed). Менеджер релизов по очереди выполняет инструкции из файла relup:

release_handler:install_release(Vsn) => {ok, FromVsn, []}

Если в процессе установки происходит ошибка, система перегружается с использованием старой версии релиза. Если установка прошла успешно, система начинает использовать новую версию релиза, но если что-то случится и система перезагрузится, она снова запустится со старой версией релиза. Чтобы установить версию по умолчанию, свежеустановленная версия релиза должна быть сделана перманентной(permanent), что означает, что предыдущая версия становится старой(old):

release_handler:make_permanent(Vsn) => ok

Система сохраняет информацию о том, какие версии являются старыми, а какие перманентными в файлах $ROOT/releases/RELEASES и $ROOT/releases/start_erl.data.

Для отката с версии Vsn до FromVsn , должна быть снова вызвана install_release:

release_handler:install_release(FromVsn) => {ok, Vsn, []}

Установленный, но не перманентный релиз может быть удален(removed). Информация о релизе затем удаляется из $ROOT/releases/RELEASES и относящийся к релизу код, такой как новые директории приложения, и директория $ROOT/releases/Vsn, удаляется.

release_handler:remove_release(Vsn) => ok

Продолжим пример из следующих разделов:

1) Создаем целевую систему, как описано в System Principles с первой версией "A" релиза ch_rel из главы Релизы. В это время в пакет релиза должен быть включен sys.config. Если конфигурация не нужна, файл должен содержать пустой список:

[].

2) Запускам систему в качестве простой целевой системы. Необходимо отметить, что в реальной жизни она должна быть запущена как встроенная система. Тем не менее, при использовании erl с правильным загрузочным скриптом и .config файлом достаточно для учебных целей:

% cd $ROOT
% bin/erl -boot $ROOT/releases/A/start -config $ROOT/releases/A/sys
...

$ROOT это установочная директория целевой системы.

3) В другой Erlang оболочке сгенерируем новые скрипты запуска и создадим новый пакет релиза с версией "B". Не забудьте включить (возможно, обновленный) sys.config и relup файл, см. Файл обновления релиза выше.

1> systools:make_script("ch_rel-2").
ok
2> systools:make_tar("ch_rel-2").
ok

Новый пакет релиза теперь содержит версию "2" приложения ch_app, также файл relup:

% tar tf ch_rel-2.tar 
lib/kernel-2.9/ebin/kernel.app
lib/kernel-2.9/ebin/application.beam
...
lib/stdlib-1.12/ebin/stdlib.app
lib/stdlib-1.12/ebin/beam_lib.beam
...      
lib/sasl-1.10/ebin/sasl.app
lib/sasl-1.10/ebin/sasl.beam
...
lib/ch_app-2/ebin/ch_app.app
lib/ch_app-2/ebin/ch_app.beam
lib/ch_app-2/ebin/ch_sup.beam
lib/ch_app-2/ebin/ch3.beam
releases/B/start.boot
releases/B/relup
releases/B/sys.config
releases/ch_rel-2.rel

4) Скопируйте пакет релиза ch_rel-2.tar.gz в директорию $ROOT/releases.

5) В работающей целевой системе, распакуйте пакет релиза:

1> release_handler:unpack_release("ch_rel-2").
{ok,"B"}

Новая версия приложения ch_app-2 устанавливается в директорию $ROOT/lib рядом с ch_app-1. kernel, stdlib и sasl директории не изменяются.

В директории $ROOT/releases, создается новая директория B, в которой находятся ch_rel-2.rel, start.boot, sys.config и relup.

6) Проверяем наличие функции ch3:available/0:

2> ch3:available().
** exited: {undef,[{ch3,available,[]},
                   {erl_eval,do_apply,5},
                   {shell,eval_loop,2}]} **

7) Устанавливаем новый релиз. Инструкции из $ROOT/releases/B/relup выполняются по очереди, в результате чего загружается новая версия ch3. Теперь нам доступна функция ch3:available/0:

3> release_handler:install_release("B").
{ok,"A",[]}
4> ch3:available().
3
5> code:which(ch3).
".../lib/ch_app-2/ebin/ch3.beam"
6> code:which(ch_sup).
".../lib/ch_app-1/ebin/ch_sup.beam"
   

Заметьте, что те процессы из ch_app, чей код не обновлялся, например супервизор, до сих пор выполняют код из ch_app-1.

8) Если теперь перезагрузить целевую систему, она опять будет использовать версию "A". Версию "B" нужно сделать перманентной, для того, чтобы использовать ее после перезагрузки системы.

7> release_handler:make_permanent("B").
ok

Обновление спецификаций приложения

При установке новой версии релиза, автоматически обновляются спецификации приложений для всех загруженных приложений. Image:note.gifИнформация о новых спецификациях приложений берется из загрузочного скрипта, включенного в пакет релиза. Следовательно, важно, чтобы загрузочный скрипт был сгенерирован из того же .rel файла, который был использован для создания пакета релиза.

В частности, параметры конфигурации приложения обновляются следующим образом (в порядке возрастания приоритета):

Это означает, что значения параметров, установленные в системных конфигурационных фалах, также как и значения установленные с использованием функции application:set_env/3, опускаются.

Когда установленный релиз становится перманентным, системный процесс init настраивается на использование нового sys.config.

После установки, контроллер приложения сравнит новые и старые конфигурационные параметры для всех запущенных приложений и вызовет функцию обратного вызова:

Module:config_change(Changed, New, Removed)

Module это модуль обратного вызова приложения, определенный в ключе mod .app файла. Changed и New это списки {Par,Val} для всех измененных и добавленных конфигурационных параметров соответственно. Removed это список всех параметров Par, которые были удалены.

Эта функция необязательна, и может быть опущена при реализации модуля обратного вызова приложения.