发布处理

发布处理原理

Erlang编程语言的一个很重要的特点是能在运行时变更模块代码,即Erlang参考手册中所描述的代码替换

基于这个功能,OTP应用SASL提供了一个用于在运行时对整个发布的不同版本之间进行升级和降级的框架。这就是我们称之为发布处理的东西。

该框架由用于生成脚本和构建发布包的离线支持工具( systools )和用于解压和安装发布包的在线支持工具( release_handler )组成。

因此,我们要注意基于 Erlang/OTP,并启用发布处理的系统,最少由Kernel、STDLIB和SASL组成。

  1. 一个发布是由前一章 发布 中所描述的方式创建的。然后该发布被传输并安装到目标环境中。关于如何安装首个目标系统的信息,请参考系统原理(System Principles)。
  2. 在开发环境中对代码进行修改,比如错误修正。
  3. 在某一刻,可以发布一个新的版本了。会更新相关的 .app 文件并写入一个新的 .rel 文件。
  4. 对于每个被修改过的应用,会创建一个 应用升级文件.appup 文件。在该文件中描述了如何在应用的新旧版本之间升级或降级。
  5. 根据 .appup 文件,会创建一个 发布升级文件 。这个文件描述了如何在整个发布的新旧版本之间升级或者降级。
  6. 生成新的发布包并传输到目标系统。
  7. 使用发布处理器解开新的发布包。
  8. 还是使用了发布处理器,安装了新版本的发布。这是通过运行 relup 中的指令来完成的。可能会添加、删除或者重新载入模块,启动、停止或重启应用等等。某些情况甚至需要重启整个虚拟器。 如果安装失败了,系统可能被重启,然后自动使用旧的发布版。
  9. 如果安装成功了,新版本会成为默认的版本,现在如果系统重启了,那么它会被自动使用。

下一章 Appup Cookbook 包含了 .appup 文件的一些例子,是一些通常在运行时较容易处理的一些典型的升级、降级案例。但是,还有方方面面的因素会让发布处理变得很复杂。说一些例子:

  • 复杂的或者环形的依赖会导致很难甚至不可能确定升级或者降级的次序而不出现运行时错误。依赖可能是:
    • 节点之间,
    • 进程之间和
    • 模块之间。
  • 在发布处理过程中,未受影响的进程将继续正常的执行。这可能会导致超时或者其他问题。例如,在挂起使用某个模块的进程并载入该模块的新版本的时间窗口内,新的进程创建了,可能还在继续执行旧的代码。

因此推荐以尽可能小的步骤变更代码,并且始终保持向后兼容。

要求

要让发布处理能正常运作,运行时系统需要知道目前正在运行哪个发布。它必须能(在运行时)更改系统如果重启后要使用的启动脚本的系统配置文件,例如在系统崩溃之后通过 heart 。因此,Erlang必须作为一个嵌入式系统启动,更多信息请参考嵌入式系统

为了让系统重启能正确运行,系统启动还必须启用心跳监控,请参见 erl(1)heart(3)

其他要求:

  • 包含在发布包中的启动脚本必须是从发布包自身的 .rel 生成得来的。 在升级或降级的执行过程中,关于应用的信息都是从该脚本获得的。
  • 系统必须使用且仅用一个系统配置文件,名叫 sys.config 。 如果存在,那么创建发布包的时候会自动包含该文件。
  • 一个发布的所有版本,除了第一个,必须包含一个 relup 文件。 如果存在,那么创建发布包的时候会自动包含该文件。

分布式系统

如果系统包含若干个Erlang节点,每个节点可能在使用它自己版本的发布。发布版本是一个在本地注册的进程,并且当需要进行升级或者降级的时候,必须在每个节点上调用。还有一个发布处理指令可以用于在一系列节点上同步发布处理器进程: sync_nodes 。参见 appup(4)

发布处理指令

OTP支持一套发布处理指令(用在创建 .appup 文件的时候)。发布处理器可以理解该指令的一个子集——低级指令。为了让用户用得简单,还有一系列高级指令,可以通过 systools:make_relup 翻译为低级指令。

这里将解释一些最常用的指令。指令的完整列表可以在 appup(4) 中找到。

首先是一些定义:

驻留模块
该模块中有一个(或多个)属于某个进程的尾递归循环函数。如果尾递归循环函数是在多个模块中实现的,那么所有这些模块都是该进程的驻留模块。
功能模块
对任何进程都不是驻留模块的模块。

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

load_module

如果对某个功能模块进行了简单的扩展,那么只要简单地将模块的新版本载入系统并删除就版本即可。这称之为简单代码替换,可以使用以下指令:

{load_module, Module}

update

如果进行了更加复杂的更改,例如对某个gen_server的内部状态的格式进行了更改,那么简单代码替换就不够了。这时需要挂起使用该模块的进程(防止其在代码替换完成之前再处理任何请求),并让他们转换内部状态的格式,并切换到模块的新版本上,删除旧版本,最后恢复进程。这称之为同步代码替换,它要用到以下指令:

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

当进行如上所述的变更行为的内部的状态时,要用到带有参数 {advanced, Extra}update 。它会让行为进程调用回掉函数 code_change ,并将值 Extra 和其他一些信息作为参数传递给它。参见相应行为的手册和 Appup Cookbook

当更改督程的启动规格的时候,要用到带有 supervisor 参数的 update 。参见 Appup Cookbook

发布处理器通过遍历每个运行的应用的监督树并检查所有的子进程规格来查找使用某个模块的进程:

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

如果某个模块的名字列在某个进程的子进程规格的 Modules 中,那么该进程在使用该模块。

如果 Modules=dynamic ,即事件管理器的情况,那么事件管理器进程会告知发布处理器目前安装了的事件处理器(gen_fsm),然后检查是否模块名在这个列表中。

发布处理器挂起进程、要求代码变更,并恢复进程是通过相应地调用 sys:suspend/1,2sys:change_code/4,5sys:resume/1,2 来完成的。

add_module 和 delete_module

如果引入了一个新的模块,可以使用以下代码:

{add_module, Module}

该指令加载模块并在当Erlang运行于嵌入模式的时候是绝对必须的。当Erlang运行于交互模式(默认)时则并非严格要求,因为代码服务器会自动搜索并加载未加载的模块。

add_module 模块的对面就是 delete_module ,他用于卸载一个模块:

{delete_module, Module}

注意在任何应用中的任何进程,当 Module 是驻留进程时,在运行指令的时候都会被杀死。因此用户必须确保在删除模块的时候所有这种进程必须终止,来避免出现督程失败重启的情况。

应用指令

添加一个应用的指令为:

{add_application, Application}

添加一个应用表示由在 .app 文件中的 modules 键定义的模块已经使用了一系列 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)

应用升级文件

要定义如何在应用的当前和上一版本之间进行升级和降级,我们要创建一个应用升级文件,简称 .appup 文件。该文件必须被命名为 Application.appup ,其中 Application 是应用的名称:

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

Vsn 是一个字符串表示应用当前的版本,和定义在 .app 文件中的一样。每个 UpFromVsn 是要从应用的哪个版本升级上来,每个 DownToVsn 是应用要降级至的版本。每个 Instructions 是一个发布处理指令的列表。

appup 文件的语法和内容在 appup(4) 中有详细的描述。

Appup Cookbook 中,给出了典型升级/降级案例的 .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_pp.appup

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

发布升级文件

要定义如何在某个发布的新旧版本之间进行升级和降级,我们要创建一个发布升级文件,或简称 relup 文件。

该文件无须手工创建,可以使用 systools:make_relup/3,4 进行生成。使用相关版本的 .rel.app.appup 文件作为输入。除去了哪些应用应该被添加或者删除和哪些应用应该被升级或降级。它的指令都是从 .appup 文件中获得并按照正确的顺序转换成单独的一个低级指令的列表。

relup 文件相对简单,可以被手工创建。但记住它只能包含低级指令。

发布升级文件的语法和内容在 relup(4) 中有详细的阐述。

例如,继续前一节的例子。我们有一个 ch_app 的新版本“2”和一个 .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 暴露了借口,在 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.config 以及 relup 文件。对于有新版本号的应用,应用目录会被放在 $ROOT/lib 下。未更改的应用不受影响。

解开的发布可以被安装。然后发布处理器会一条条执行在 relup 中指令:

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

如果在安装过程中出现了错误,那么系统会使用老版本的发布重新启动。如果安装成功了,系统以后就会使用新版本的发布,但是一旦出了状况系统重启了,那么还是会使用前一个版本进行启动。为了能成为默认版本,新安装的发布必须被设为持久(permanent)的,也就是说前一个版本变成旧的了:

release_handler:make_permanent(Vsn) => ok

系统将哪个版本是旧的哪个是持久的信息保存在文件 $ROOT/releases/RELEASES$ROOT/releases/start_erl.data

要从 Vsn 降级至 FromVsn ,必须再次调用 install_release

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

一个安装了,但是并非持久的发布可以被移除。然后关于该发布的信息会从 $ROOT/releases/RELEASES 中被删除,同时该发布指定的代码,也就是新的应用的目录和 $ROOT/releases/Vsn 目录都会被删除。

接着前一节的例子:

  1. 按照系统原理中所描述的创建一个目标系统,包含来自 发布 一章的 ch_rel 的第一个版本“A”。这次必须在发布包中包含 sys.config 。如果不需要任何配置,该文件应该包含空列表:
[].
  1. 作为一个简单的目标系统启动。注意实际中,它要作为一个嵌入式系统启动。不过,用正确的启动脚本以及 .config 文件启动 erl 对于描述我们的目的已经足够了:
% cd $ROOT
% bin/erl -boot $ROOT/releases/A/start -config $ROOT/releases/A/sys
...
  1. 在另一个Erlang shell中,为新版本“B”生成启动脚本并创建发布包。记住要包含(一个可能更新过的) sys.config 文件和 relup 文件,参见之前的 发布升级文件
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/ch_rel-2.rel
  1. 将发布包 ch_rel-2.tar.gz 复制到 $ROOT/releases 目录中。
  2. 在运行中的目标系统上,解开发布包:
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

  1. 检查函数 ch3:available/0 是否可用:
2> ch3:available().
** exception error: undefined function ch3:available/0
  1. 安装新的发布。执行 $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

更新应用规格

当安装了某个发布的新版本,应用规格会为所有应用进行自动更新。

Note

关于新的应用规格的信息是从包含在发布包中的启动脚本中获得的。因此要确保启动脚本是由和用于构建发布包自身的 .rel 文件是同一个。

特别是,应用配置参数会根据以下数据(优先级从低到高)自动更新:

  1. 在启动脚本中的数据,即从新的应用资源文件 App.app 中获得的
  2. 新的 sys.config
  3. 命令行参数 -App Par Val

这还表示在其他系统配置文件中设置的参数值,包括使用 application:set_env/3 设置的值,都是不考虑的。

当某个已安装的发布被设为持久的,系统进程 init 会设置指向新的 sys.config

在安装完成之后,应用控制器会为所有运行的应用比较新旧配置参数并调用以下回调函数:

Module:config_change(Changed, New, Removed)

Module 是由 .app 文件中 mod 所定义的应用回调模块。 ChangedNew{Par,Val} 的列表,分别对应所有更改的和添加的配置参数。 Removed 是所有被删除的参数 Par 的列表。

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