Erlang编程语言的一个很重要的特点是能在运行时变更模块代码,即Erlang参考手册中所描述的代码替换。
基于这个功能,OTP应用SASL提供了一个用于在运行时对整个发布的不同版本之间进行升级和降级的框架。这就是我们称之为发布处理的东西。
该框架由用于生成脚本和构建发布包的离线支持工具( systools )和用于解压和安装发布包的在线支持工具( release_handler )组成。
因此,我们要注意基于 Erlang/OTP,并启用发布处理的系统,最少由Kernel、STDLIB和SASL组成。
下一章 Appup Cookbook 包含了 .appup 文件的一些例子,是一些通常在运行时较容易处理的一些典型的升级、降级案例。但是,还有方方面面的因素会让发布处理变得很复杂。说一些例子:
因此推荐以尽可能小的步骤变更代码,并且始终保持向后兼容。
要让发布处理能正常运作,运行时系统需要知道目前正在运行哪个发布。它必须能(在运行时)更改系统如果重启后要使用的启动脚本的系统配置文件,例如在系统崩溃之后通过 heart 。因此,Erlang必须作为一个嵌入式系统启动,更多信息请参考嵌入式系统。
为了让系统重启能正确运行,系统启动还必须启用心跳监控,请参见 erl(1) 和 heart(3) 。
其他要求:
如果系统包含若干个Erlang节点,每个节点可能在使用它自己版本的发布。发布版本是一个在本地注册的进程,并且当需要进行升级或者降级的时候,必须在每个节点上调用。还有一个发布处理指令可以用于在一系列节点上同步发布处理器进程: sync_nodes 。参见 appup(4) 。
OTP支持一套发布处理指令(用在创建 .appup 文件的时候)。发布处理器可以理解该指令的一个子集——低级指令。为了让用户用得简单,还有一系列高级指令,可以通过 systools:make_relup 翻译为低级指令。
这里将解释一些最常用的指令。指令的完整列表可以在 appup(4) 中找到。
首先是一些定义:
注意对于使用了OTP行为实现的进程,行为模块是该进程的驻留模块。回调模块是功能模块。
如果对某个功能模块进行了简单的扩展,那么只要简单地将模块的新版本载入系统并删除就版本即可。这称之为简单代码替换,可以使用以下指令:
{load_module, Module}
如果进行了更加复杂的更改,例如对某个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,2 、 sys:change_code/4,5 和 sys:resume/1,2 来完成的。
如果引入了一个新的模块,可以使用以下代码:
{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 。
该指令用于当变更到一个新的模拟器版本上,或者由于某种其他原因需要进行系统重启。要求该系统必须启用了心跳监控,参见 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 目录都会被删除。
接着前一节的例子:
[].
% cd $ROOT
% bin/erl -boot $ROOT/releases/A/start -config $ROOT/releases/A/sys
...
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> 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 。
2> ch3:available().
** exception error: undefined function 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 文件是同一个。
特别是,应用配置参数会根据以下数据(优先级从低到高)自动更新:
这还表示在其他系统配置文件中设置的参数值,包括使用 application:set_env/3 设置的值,都是不考虑的。
当某个已安装的发布被设为持久的,系统进程 init 会设置指向新的 sys.config 。
在安装完成之后,应用控制器会为所有运行的应用比较新旧配置参数并调用以下回调函数:
Module:config_change(Changed, New, Removed)
Module 是由 .app 文件中 mod 所定义的应用回调模块。 Changed 和 New 是 {Par,Val} 的列表,分别对应所有更改的和添加的配置参数。 Removed 是所有被删除的参数 Par 的列表。
该函数是可选的,在实现应用回调模块时可以忽略。