查看源代码 发布处理

发布处理原则

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 Cookbook 包含 .appup 文件的示例,这些示例适用于通常在运行时易于处理的典型升级/降级情况。但是,许多方面会使发布处理变得复杂,例如

  • 复杂或循环依赖关系可能使难以甚至不可能决定以何种顺序执行操作,而不会在升级或降级期间冒运行时错误的风险。依赖关系可以是

    • 节点之间
    • 进程之间
    • 模块之间
  • 在发布处理期间,不受影响的进程继续正常执行。这可能导致超时或其他问题。例如,在暂停使用某个模块的进程和加载该模块的新版本之间的时间窗口中创建的新进程可以执行旧代码。

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

要求

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

为了使系统重新启动正常工作,还需要使用心跳监控启动系统;请参阅 ERTS 中的erl和 Kernel 中的模块heart

其他要求

  • 发布包中包含的引导脚本必须从与发布包本身相同的 .rel 文件生成。

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

  • 系统必须仅使用一个名为 sys.config 的系统配置文件进行配置。

    如果找到此文件,则在创建发布包时会自动包含该文件。

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

    如果找到此文件,则在创建发布包时会自动包含该文件。

分布式系统

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

发布处理指令

OTP 支持一组在创建 .appup 文件时使用的发布处理指令。发布处理程序理解其中的一个子集,即低级指令。为了方便用户,还有许多高级指令,这些指令由 systools:make_relup 转换为低级指令。

本节介绍了一些最常用的指令。完整的指令列表包含在 SASL 中的appup中。

首先,一些定义

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

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

load_module

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

{load_module, Module}

update

如果进行了更复杂的更改,例如,更改了gen_server的内部状态格式,则简单代码替换是不够的。相反,有必要

  • 暂停使用该模块的进程(以避免它们在代码替换完成之前尝试处理任何请求)。
  • 要求它们转换内部状态格式并切换到新版本的模块。
  • 删除旧版本。
  • 恢复进程。

这称为同步代码替换,为此使用以下指令

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

当更改行为的内部状态时,如上所述,使用带有参数 {advanced,Extra}update。它会导致行为进程调用回调函数 code_change/3,传递术语 Extra 和一些其他信息作为参数。请参阅各自行为的手册页和Appup Cookbook

当更改主管的启动规范时,使用带有参数 supervisorupdate。请参阅Appup Cookbook

当要更新模块时,发布处理程序会通过遍历每个正在运行的应用程序的监督树并检查所有子规范来查找哪些进程正在使用该模块

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

如果进程的子规范中的 Modules 中列出了该名称,则该进程使用该模块。

如果 Modules=dynamic,对于事件管理器就是这种情况,则事件管理器进程会将当前安装的事件处理程序列表(gen_event)通知给发布处理程序,并且会改为检查该模块名称是否在此列表中。

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

add_module 和 delete_module

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

{add_module, Module}

此指令加载模块 Module。当在嵌入式模式下运行 Erlang 时,必须使用此指令。在交互模式下运行 Erlang 时,它不是严格必需的,因为代码服务器会自动搜索并加载未加载的模块。

add_module 的反义词是 delete_module,它会卸载模块

{delete_module, Module}

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

应用程序指令

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

{add_application, Application}

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

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

{remove_application, Application}

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

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

{restart_application, Application}

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

apply(低级)

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

{apply, {M, F, A}}

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

restart_new_emulator(低级)

当更换为新版本的运行时系统,或者核心应用程序 Kernel、STDLIB 或 SASL 中的任何一个升级时,使用此指令。如果出于其他原因需要系统重启,则应使用 restart_emulator 指令。

此指令要求系统在启动时启用心跳监控;请参阅 ERTS 中的 erl 和 Kernel 中的模块 heart

restart_new_emulator 指令必须始终是 relup 中的第一个指令。如果 relup 由 systools:make_relup/3,4 生成,则会自动满足此条件。

当发布处理器遇到此指令时,它首先生成一个临时引导文件,该文件启动新版本的运行时系统和核心应用程序,以及所有其他应用程序的旧版本。然后,它通过调用 init:reboot/0 来关闭当前运行时系统的实例。所有进程都会被优雅地终止,系统将由 heart 程序使用临时引导文件重新启动。重启后,执行 relup 的其余指令。这是作为临时引导脚本的一部分完成的。

警告

此机制导致新版本的运行时系统和核心应用程序在启动期间使用其他应用程序的旧版本运行。因此,请格外小心以避免不兼容。在某些情况下,核心应用程序中可能需要不兼容的更改。如果可能,此类更改会在实际更改之前经过两个主要版本的弃用。为了确保应用程序不会因不兼容的更改而崩溃,请始终尽快删除对已弃用函数的任何调用。

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

当新的运行时系统运行时,必须使新的发布版本成为永久版本。否则,如果系统再次重启,将使用旧版本。

在 UNIX 上,发布处理器会告知 heart 程序使用哪个命令来重启系统。在这种情况下,通常由 heart 程序使用的环境变量 HEART_COMMAND 将被忽略。该命令默认为 $ROOT/bin/start。可以使用 SASL 配置参数 start_prg 设置其他命令。有关更多信息,请参阅 SASL

restart_emulator(底层)

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

relup 脚本只能包含一个 restart_emulator 指令,并且它必须始终放在末尾。如果 relup 由 systools:make_relup/3,4 生成,则会自动满足此条件。

当发布处理器遇到此指令时,它通过调用 init:reboot/0 来关闭运行时系统。所有进程都会被优雅地终止,然后系统可以通过 heart 程序使用新的发布版本重新启动。重启后,不再执行其他升级指令。

应用程序升级文件

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

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

UpFromVsnDownToVsn 也可以指定为正则表达式。有关 .appup 文件的语法和内容的更多信息,请参阅 SASL 中的 appup

Appup Cookbook 包含了典型升级/降级案例的 .appup 文件示例。

示例:考虑 Releases 中的发布版本 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

示例,继续上一节:您有一个新版本的 ch_app "2" 和一个 .appup 文件。还需要新版本的 .rel 文件。这次该文件名为 ch_rel-2.rel,发布版本字符串从 "A" 更改为 "B"

{release,
 {"ch_rel", "B"},
 {erts, "14.2.5"},
 [{kernel, "9.2.4"},
  {stdlib, "5.2.3"},
  {sasl, "4.2.1"},
  {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 模块与此进程通信。

假设有一个具有安装根目录 $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)Releases 中所述的 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-9.2.4/ebin/kernel.app
lib/kernel-9.2.4/ebin/application.beam
...
lib/stdlib-5.2.3/ebin/stdlib.app
lib/stdlib-5.2.3/ebin/argparse.beam
...
lib/sasl-4.2.1/ebin/sasl.app
lib/sasl-4.2.1/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 中,代码尚未更新的进程,例如 supervisor,仍然在评估来自 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 的列表。

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