11  版本管理

11 版本管理

Erlang 编程语言的一个重要特性是在运行时更改模块代码,即 **代码替换**,如 Erlang 参考手册所述。

基于此特性,OTP 应用程序 SASL 提供了一个框架,用于在运行时在整个版本的不同版本之间进行升级和降级。这称为 **版本管理**。

该框架包含

  • 离线支持 - systools 用于生成脚本和构建版本包
  • 在线支持 - release_handler 用于解包和安装版本包

基于 Erlang/OTP 的最小系统,支持版本管理,因此包括 Kernel、STDLIB 和 SASL 应用程序。

**步骤 1**)按照 版本 中的描述创建版本。

**步骤 2**)将版本传输到目标环境并安装。有关如何安装第一个目标系统的详细信息,请参阅 系统原则

**步骤 3**)在开发环境中修改代码,例如纠正错误。

**步骤 4**)在某个时间点,是时候制作一个新版本的版本了。相关 .app 文件将被更新,并将写入一个新的 .rel 文件。

**步骤 5**)对于每个修改的应用程序,都会创建一个 应用程序升级文件.appup。在这个文件中,描述了如何在这两个应用程序的旧版本和新版本之间进行升级和/或降级。

**步骤 6**)基于 .appup 文件,创建一个名为 relup版本升级文件。该文件描述了如何在这整个版本的旧版本和新版本之间进行升级和/或降级。

**步骤 7**)制作一个新的版本包并将其传输到目标系统。

**步骤 8**)使用版本处理程序解包新的版本包。

**步骤 9**)安装新版本的版本,也使用版本处理程序。这是通过评估 relup 中的说明来完成的。可以添加、删除或重新加载模块,可以启动、停止或重启应用程序,等等。在某些情况下,甚至需要重启整个模拟器。

  • 如果安装失败,系统可以重新启动。然后自动使用旧版本的版本。
  • 如果安装成功,新版本将成为默认版本,如果系统重新启动,将使用该版本。

Appup 食谱 包含 .appup 文件的示例,用于通常易于在运行时处理的典型升级/降级情况。但是,许多方面都会使版本管理变得复杂,例如

  • 复杂的或循环的依赖关系可能使决定必须按什么顺序执行操作变得困难甚至不可能,而不会冒在升级或降级期间发生运行时错误的风险。依赖关系可以是

    • 节点之间
    • 进程之间
    • 模块之间
  • 在版本管理期间,未受影响的进程将继续正常执行。这会导致超时或其他问题。例如,在使用特定模块暂停进程和加载该模块的新版本之间的时间窗口内创建的新进程可能会执行旧代码。

因此,建议代码更改尽可能小,并且始终保持向后兼容。

为了使版本管理正常工作,运行时系统必须了解它正在运行哪个版本。它还必须能够在运行时更改在系统重新启动时(例如,在出现故障后由 heart 重新启动)要使用的启动脚本和系统配置文件。因此,Erlang 必须作为嵌入式系统启动;有关如何执行此操作的信息,请参阅嵌入式系统。

为了使系统重新启动正常工作,还需要系统启动时具有心跳监测功能,请参阅 ERTS 中的 erl(1) 手册页和 Kernel 中的 heart(3) 手册页

其他要求

  • 版本包中包含的启动脚本必须从与版本包本身相同的 .rel 文件生成。

    执行升级或降级时,应用程序信息将从脚本中获取。

  • 系统必须使用一个系统配置文件进行配置,称为 sys.config

    如果找到,该文件将在创建版本包时自动包含在内。

  • 除了第一个版本之外,所有版本的版本都必须包含 relup 文件。

    如果找到,该文件将在创建版本包时自动包含在内。

如果系统由多个 Erlang 节点组成,则每个节点可以使用其自己的版本。版本处理程序是一个本地注册的进程,必须在需要升级或降级的每个节点上调用。版本管理指令 sync_nodes 可用于同步多个节点上的版本处理程序进程,请参阅 SASL 中的 appup(4) 手册页。

OTP 支持一组 **版本管理说明**,这些说明在创建 .appup 文件时使用。版本处理程序理解这些说明的子集,即 **低级** 说明。为了方便用户,还有一些 **高级** 说明,这些说明由 systools:make_relup 转换为低级说明。

本节将描述一些最常用的指令。完整的指令列表包含在 SASL 中的 appup(4) 手册页中。

首先,一些定义

  • **驻留模块** - 进程在其尾递归循环函数所在的模块。如果这些函数在多个模块中实现,则所有这些模块都是该进程的驻留模块。
  • **功能模块** - 不是任何进程的驻留模块的模块。

对于使用 OTP 行为实现的进程,行为模块是该进程的驻留模块。回调模块是一个功能模块。

如果对功能模块进行了简单的扩展,则只需将模块的新版本加载到系统中,并删除旧版本。这称为 **简单代码替换**,为此,使用以下指令

{load_module, Module}

如果进行了更复杂的更改,例如对 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_event) 通知发布处理程序,并且会检查模块名称是否在此列表中。

发布处理程序会分别通过调用函数 sys:suspend/1,2sys:change_code/4,5sys:resume/1,2 来挂起、请求代码更改和恢复进程。

如果引入了新模块,则使用以下指令

{add_module, Module}

该指令加载模块,在嵌入模式下运行 Erlang 时是必需的。在交互式(默认)模式下运行 Erlang 时,它不是严格要求的,因为代码服务器随后会自动搜索和加载未加载的模块。

add_module 的反面是 delete_module,它会卸载模块。

{delete_module, Module}

当评估指令时,任何应用程序中以 Module 作为驻留模块的进程都会被杀死。因此,用户必须确保在删除模块之前终止所有此类进程,以避免出现监管者重新启动失败的情况。

以下是用於添加应用程序的指令

{add_application, Application}

添加应用程序意味着使用多个 add_module 指令加载 .app 文件中 modules 键定义的模块,然后启动应用程序。

以下是用于删除应用程序的指令

{remove_application, Application}

删除应用程序意味着停止应用程序,使用多个 delete_module 指令卸载模块,然后从应用程序控制器中卸载应用程序规范。

以下是用于重新启动应用程序的指令

{restart_application, Application}

重新启动应用程序意味着停止应用程序,然后重新启动它,类似于依次使用 remove_applicationadd_application 指令。

要从发布处理程序调用任意函数,请使用以下指令

{apply, {M, F, A}}

发布处理程序会评估 apply(M, F, A)

当更改为新的仿真器版本或升级任何核心应用程序 Kernel、STDLIB 或 SASL 时,使用此指令。如果需要出于其他原因重新启动系统,则应改用 restart_emulator 指令。

此指令要求系统在启动时使用心跳监控,请参阅 ERTS 中的 erl(1) 手册页和 Kernel 中的 heart(3) 手册页。

restart_new_emulator 指令必须始终是 relup 中的第一个指令。如果 relup 是由 systools:make_relup/3,4 生成的,则会自动确保这一点。

当发布处理程序遇到此指令时,它会首先生成一个临时引导文件,该文件启动仿真器和核心应用程序的新版本以及所有其他应用程序的旧版本。然后,它通过调用 init:reboot() 来关闭当前仿真器,请参阅 Kernel 中的 init(3) 手册页。所有进程都会正常终止,系统会使用临时引导文件由 heart 程序重新启动。重新启动后,将执行 relup 指令的其余部分。这作为临时引导脚本的一部分完成。

警告

此机制会导致仿真器和核心应用程序的新版本在启动时使用其他应用程序的旧版本运行。因此,请格外注意避免不兼容。在某些情况下,核心应用程序中的不兼容更改可能是必要的。如果可能,这种更改将在实际更改之前通过两个主要版本进行弃用。为确保应用程序不会因不兼容更改而崩溃,请尽快删除对已弃用函数的任何调用。

升级完成后会写入一个信息报告。要以编程方式确定升级是否已完成,请调用 release_handler:which_releases(current) 并检查它是否返回预期(即新)版本。

新仿真器运行时,必须使新版本永久生效。否则,如果发生新的系统重新启动,将使用旧版本。

在 UNIX 上,发布处理程序会告诉 heart 程序使用哪个命令来重新启动系统。通常由 heart 程序使用的环境变量 HEART_COMMAND 在这种情况下会被忽略。该命令默认为 $ROOT/bin/start。可以使用 SASL 配置参数 start_prg 设置另一个命令,请参阅 sasl(6) 手册页。

此指令与 ERTS 或任何核心应用程序的升级无关。任何应用程序都可以使用它来强制在执行所有升级指令后重新启动仿真器。

relup 脚本只能包含一个 restart_emulator 指令,并且该指令必须始终放置在最后。如果 relup 是由 systools:make_relup/3,4 生成的,则会自动确保这一点。

当发布处理程序遇到此指令时,它会通过调用 init:reboot() 来关闭仿真器,请参阅 Kernel 中的 init(3) 手册页。所有进程都会正常终止,然后系统可以使用新版本由 heart 程序重新启动。重新启动后,不会再执行任何升级指令。

要定义如何在当前版本和应用程序的先前版本之间进行升级/降级,请创建一个应用程序升级文件,简称 .appup 文件。该文件名为 Application.appup,其中 Application 是应用程序名称。

{Vsn,
 [{UpFromVsn1, InstructionsU1},
  ...,
  {UpFromVsnK, InstructionsUK}],
 [{DownToVsn1, InstructionsD1},
  ...,
  {DownToVsnK, InstructionsDK}]}.
  • Vsn,一个字符串,是应用程序的当前版本,如 .app 文件中所定义。
  • 每个 UpFromVsn 都是要从其升级的应用程序的先前版本。
  • 每个 DownToVsn 都是要降级到的应用程序的先前版本。
  • 每个 Instructions 都是发布处理指令列表。

有关 .appup 文件的语法和内容的信息,请参阅 SASL 中的 appup(4) 手册页。

Appup 食谱 包括典型升级/降级情况的 .appup 文件示例。

示例:考虑来自 发布 的发布 ch_rel-1。假设您要向服务器 ch3 添加一个函数 available/0,该函数返回可用通道的数量(在试用示例时,更改原始目录的副本,以便第一个版本仍然可用)。

-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 回调模块的新(旧)版本。在 ebin 目录中创建应用程序升级文件 ch_app.appup

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

要定义如何在发布的新版本和先前版本之间进行升级/降级,请创建一个发布升级文件,简称 relup 文件。

无需手动创建此文件,它可以由 systools:make_relup/3,4 生成。使用 .rel 文件、.app 文件和 .appup 文件的相关版本作为输入。会推断出要添加和删除的应用程序,以及必须升级和/或降级的应用程序。这些指令是从 .appup 文件中获取的,并转换为以正确顺序排列的单个低级指令列表。

如果 relup 文件相对简单,则可以手动创建。它只包含低级指令。

有关发布升级文件的语法和内容的详细信息,请参阅 SASL 中的 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

创建发布的新版本后,可以使用该新版本创建发布包并将其传输到目标环境。

要安装发布的运行时新版本,请使用发布处理程序。这是一个属于 SASL 应用程序的进程,它负责处理发布包的解包、安装和删除。它通过 release_handler 模块进行通信。有关详细信息,请参阅 SASL 中的 release_handler(3) 手册页。

假设有一个带有安装根目录 $ROOT 的正在运行的目标系统,则将包含发布新版本的发布包复制到 $ROOT/releases

首先,解包发布包。然后,从包中提取文件。

release_handler:unpack_release(ReleaseName) => {ok, Vsn}
  • ReleaseName 是发布包的名称,不包括 .tar.gz 扩展名。
  • Vsn 是解压缩的发布的版本,如其 .rel 文件中所定义。

将创建一个目录 $ROOT/lib/releases/Vsn,其中包含 .rel 文件、引导脚本 start.boot、系统配置文件 sys.configrelup。对于具有新版本号的应用程序,应用程序目录将放置在 $ROOT/lib 下。不会影响未更改的应用程序。

可以安装解压缩的发布。发布处理程序会依次评估 relup 中的指令。

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

如果安装过程中出现错误,系统将使用旧版本的发布进行重启。如果安装成功,系统随后将使用新版本的发布,但如果出现任何问题导致系统重启,它将再次使用旧版本。

要使新安装的版本成为默认版本,必须将其设置为永久版本,这意味着旧版本将变为版本。

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, []}

已安装但未设置为永久版本的发布可以删除。然后,有关该发布的信息将从 $ROOT/releases/RELEASES 中删除,并且与该发布相关的代码,即新的应用程序目录和 $ROOT/releases/Vsn 目录将被删除。

release_handler:remove_release(Vsn) => ok

步骤 1) 创建目标系统,如 ch_rel 的第一个版本 "A" 的系统原理中所述 发布。这次,sys.config 必须包含在发布包中。如果不需要配置,则该文件应包含空列表。

[].

步骤 2) 启动系统作为简单的目标系统。实际上,它应该作为嵌入式系统启动。但是,使用具有正确引导脚本和配置文件的 erl 足以用于说明目的。

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

$ROOT 是目标系统的安装目录。

步骤 3) 在另一个 Erlang shell 中,生成启动脚本并为新版本 "B" 创建发布包。请记住要包含(可能更新的)sys.configrelup 文件,请参见 发布升级文件

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

新的发布包现在还包含 ch_app 的版本“2”和 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/B/ch_rel-2.rel
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 相邻。 kernelstdlibsasl 目录不受影响,因为它们没有改变。

$ROOT/releases 下,将创建一个新的目录 B,其中包含 ch_rel-2.relstart.bootsys.configrelup

步骤 6) 检查函数 ch3:available/0 是否可用。

2> ch3:available().
** exception error: undefined function ch3:available/0

步骤 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

安装新版本的发布时,将自动更新所有已加载应用程序的应用程序规范。

注意

有关新应用程序规范的信息将从发布包中包含的引导脚本中获取。因此,重要的是,引导脚本必须使用与用于构建发布包本身的相同 .rel 文件生成。

具体来说,应用程序配置参数将根据以下优先级顺序自动更新(从低到高):

  • 从新应用程序资源文件 App.app 中获取的引导脚本中的数据。
  • 新的 sys.config
  • 命令行参数 -App Par Val

这意味着在其他系统配置文件中设置的参数值和使用 application:set_env/3 设置的值将被忽略。

当安装的发布设置为永久版本时,系统进程 init 将设置为指向新的 sys.config

安装后,应用程序控制器将比较所有正在运行应用程序的旧配置参数和新配置参数,并调用回调函数

Module:config_change(Changed, New, Removed)
  • Module 是应用程序回调模块,由 .app 文件中的 mod 键定义。
  • ChangedNew{Par,Val} 的列表,分别代表所有已更改和已添加的配置参数。
  • Removed 是所有已删除参数 Par 的列表。

该函数是可选的,在实现应用程序回调模块时可以省略。