释放处理 | 11. Release Handling
11 版本处理
11.1 发布处理原则
Erlang编程语言的一个重要特性是能够在运行时更改模块代码,代码替换
,如Erlang参考手册中所述。
基于此功能,OTP应用程序SASL提供了一个框架,用于在运行时在整个发行版的不同版本之间进行升级和降级。这称为发布处理
。
该框架包括:
- Offline support -
systools
用于生成脚本和构建发行包
基于Erlang/OTP的最小系统(支持版本处理)由内核,STDLIB和SASL应用程序组成。
版本处理工作流程
步骤1
)按照中所述创建发行版Releases
。
步骤2
)将发布版传输并安装到目标环境。有关如何安装第一个目标系统的信息,请参阅系统原则。
步骤3
)对开发环境中的代码进行修改(例如,错误更正)。
步骤4
)在某个时候,是时候制作一个新版本的发行版了。相关.app
文件被更新并.rel
写入一个新文件。
步骤5
)对于每个修改的应用程序,创建一个application upgrade file
,.appup
。在此文件中,介绍了如何升级和/或降级应用程序的旧版本和新版本。
步骤6
)基于这些.appup
文件,创建一个release upgrade file
被调用的文件relup
。该文件描述了如何升级和/或降级整个版本的旧版本和新版本。
步骤7
)制作一个新的发行包并将其转移到目标系统。
步骤8
)使用释放处理程序解压新版本的软件包。
第9步
)安装新版本的发行版,同时使用发布处理程序。这是通过评估中的说明完成的relup
。可以添加,删除或重新加载模块,可以启动,停止或重新启动应用程序等等。在某些情况下,甚至有必要重新启动整个模拟器。
- 如果安装失败,系统可以重新启动。然后自动使用旧版本的版本。
释放处理方面
Appup Cookbook
包含.appup
通常易于在运行时处理的典型升级/降级的文件示例。但是,许多方面可能会使发布处理变得复杂,例如:
- 复杂或循环的依赖性可能会导致在升级或降级期间决定以何种顺序执行操作而不会冒运行时错误的困难甚至不可能。依赖关系可以是:
- Between nodes
- Between processes
- Between modules
- 在发布处理期间,不受影响的进程将继续正常执行。这可能会导致暂停或其他问题。例如,在使用特定模块的暂挂进程与加载该模块的新版本之间的时间窗口中创建的新进程可以执行旧代码。
因此,建议代码尽可能以小步骤进行更改,并始终保持向后兼容。
11.2 要求
要使发布处理正常工作,运行时系统必须知道它正在运行哪个发行版。它还必须能够在运行时更改在系统重启时(例如heart
故障发生后)使用的启动脚本和系统配置文件。因此,Erlang必须作为嵌入式系统启动; 有关如何执行此操作的信息,请参阅嵌入式系统。
要使系统重新启动正常工作,还需要系统启动心跳监视,请参阅erl(1)
ERTS中的heart(3)
手册页和内核中的手册页
其他需求:
- 发行包中包含的引导脚本必须从
.rel
发行包本身的相同文件生成。在执行升级或降级时,会从脚本中获取有关应用程序的信息。
11.3 分布式系统
如果系统由多个Erlang节点组成,则每个节点都可以使用它自己的版本。发布处理程序是本地注册的进程,必须在需要升级或降级的每个节点上调用。释放处理指令sync_nodes
可用于在多个节点上同步释放处理程序进程,请参阅appup(4)
SASL中的手册页。
11.4 版本处理说明
OTP支持创建文件时使用的一组版本处理指令
.appup
。释放处理程序了解这些低级
指令的子集。为了使用户更容易,还有一些高级
指令,这些指令被翻译成低级
指令systools:make_relup
。
本节介绍了一些最常用的指令。完整的说明列表包含appup(4)
在SASL 的手册页中。
首先,一些定义:
Residence模块
- 进程具有尾递归循环功能的模块。如果这些功能在多个模块中实现,则所有这些模块都是该过程的驻留模块。
对于使用OTP行为实现的过程,行为模块是该过程的驻留模块。回调模块是一个功能模块。
load_module
如果对功能模块进行了简单的扩展,则将新版本的模块加载到系统中就足够了,并删除旧版本。这被称为简单代码替换
,为此使用以下指令:
{load_module, Module}
更新
例如,如果进行更复杂的更改,则更改gen_server
简单代码替换的内部状态的格式是不够的。相反,有必要:
- 使用模块暂停进程(以避免在代码更换完成之前尝试处理任何请求)。
这称为同步代码替换
,为此使用以下指令:
{update, Module, {advanced, Extra}}
{update, Module, supervisor}
update{advanced,Extra}
如上所述,在改变行为的内部状态时使用参数。它会导致行为过程调用回调函数code_change
,将该术语Extra
和其他一些信息作为参数传递。请参阅手册页以了解相应的行为和Appup Cookbook
。
updatesupervisor
在更改主管的启动规格时使用参数。参见Appup Cookbook
。
当要更新模块时,释放处理程序通过遍历每个正在运行的应用程序的监督树并检查所有子规格来
查找哪些进程正在使用
该模块:
{Id, StartFunc, Restart, Shutdown, Type, Modules}
如果过程Modules
的子规范中列出了名称,则过程使用模块。
如果Modules=dynamic
事件管理器的情况是这样,则事件管理器进程通知释放处理程序关于当前安装的事件处理程序(gen_event
)的列表,并检查模块名称是否在该列表中。
释放处理程序暂停,请求更改代码,并分别通过调用函数sys:suspend/1,2
,和来恢复进程。sys:change_code/4,5sys: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
键定义的模块,然后启动应用程序。.appadd_module
以下是删除应用程序的说明:
{remove_application, Application}
删除应用程序意味着应用程序停止运行,使用许多delete_module
指令卸载模块,然后从应用程序控制器卸载应用程序规范。
以下是重新启动应用程序的说明:
{restart_application, Application}
重新启动应用程序意味着应用程序停止,然后再次启动,类似于使用指令remove_application
并add_application
依次执行。
apply (Low-Level)
要从释放处理程序调用任意函数,将使用以下指令:
{apply, {M, F, A}}
发布处理程序评估apply(M, F, A)
。
restart_new_emulator (Low-Level)
在更改为新的仿真器版本或者任何核心应用程序Kernel,STDLIB或SASL升级时使用此指令。如果由于其他原因需要重新启动系统,restart_emulator
则应该使用该指令。
该指令要求系统以心跳监测启动,请参阅erl(1)
ERTS中的heart(3)
手册页和Kernel中的手册页。
restart_new_emulator
指令必须始终是第一条指令。如果产生的涟漪systools:make_relup/3,4
,这是自动保证。
当释放处理程序遇到该指令时,它首先生成一个临时引导文件,该文件启动模拟器和核心应用程序的新版本以及所有其他应用程序的旧版本。然后通过调用关闭当前模拟器init:reboot()
,请参阅init(3)
Kernel中的手册页。所有进程都会正常终止,并且heart
程序会使用临时引导文件重启系统。重新启动后,其余的重新安装指令将被执行。这是作为临时启动脚本的一部分完成的。
警告
此机制会导致新版本的模拟器和核心应用程序在启动过程中与旧版本的其他应用程序一起运行。因此,要格外小心以避免不兼容。核心应用程序中不兼容的更改在某些情况下可能是必需的。如果可能,在实际更改之前,此类更改将在两个主要版本之前弃用。为确保应用程序不会因不兼容的更改而崩溃,请务必尽快删除对弃用函数的任何调用。
升级完成后会写入信息报告。要以编程方式查明升级是否完成,请调用release_handler:which_releases(current)
并检查它是否返回预期的(即新的)版本。
新仿真器运行时,新版本必须永久保存。否则,如果有新的系统重新启动,将使用旧版本。
在UNIX上,释放处理程序会通知heart
程序要使用哪个命令来重新引导系统。在这种情况下HEART_COMMAND
,通常由heart
程序使用的环境变量将被忽略。该命令默认为$ROOT/bin/start
。可以使用SASL配置参数设置另一个命令start_prg
,请参阅sasl(6)
手册页。
restart_emulator (Low-Level)
该指令与ERTS或任何核心应用程序的升级无关。任何应用程序都可以使用它来在所有升级指令执行后强制重新启动仿真器。
relup脚本只能有一条restart_emulator
指令,并且必须始终放在最后。如果产生的涟漪systools:make_relup/3,4
,这是自动保证。
当释放处理程序遇到该指令时,它会通过调用关闭该模拟器init:reboot()
,请参阅init(3)
Kernel中的手册页。所有进程都可以正常终止,然后系统可以通过heart
程序使用新版本重新启动。重启后不再执行升级指令。
11.5应用程序升级文件
要定义如何在应用程序的当前版本和以前版本之间升级/降级,应用程序升级文件
或者简单地.appup
创建一个文件。该文件将被调用Application.appup
,Application
应用程序名称在哪里:
{Vsn,
[{UpFromVsn1, InstructionsU1},
...,
{UpFromVsnK, InstructionsUK}],
[{DownToVsn1, InstructionsD1},
...,
{DownToVsnK, InstructionsDK}]}.
Vsn
,一个字符串,是该.app
文件中定义的应用程序的当前版本。
有关.appup
文件的语法和内容的信息,请参阅appup(4)
SASL中的手册页。
Appup Cookbook
包括.appup
典型升级/降级案例的文件示例。
例如:
考虑释放ch_rel-1
的Releases
。假设您想要向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}]}]
}.
11.6 版本升级文件
要定义如何在新版本和以前版本的发行版之间升级/降级,需要创建发行版升级文件
或简短relup
文件。
这个文件不需要手动创建,它可以通过生成systools:make_relup/3,4
。.rel
文件,.app
文件和.appup
文件的相关版本用作输入。它被扣除哪些应用程序将被添加和删除,以及哪些应用程序必须升级和/或降级。这些指令是从.appup
文件中提取出来的,并以正确的顺序转换为一个低级别指令列表。
如果relup
文件比较简单,可以手动创建。它只包含低级指令。
有关发行版升级文件的语法和内容的详细信息,请参阅relup(4)
SASL中的手册页。
例如,继续上一节:
您有一个新版本“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
11.7安装版本
当您创建新版本的发行版时,可以使用此新版本创建发行版软件包并将其传输到目标环境。
要在运行时安装新版本的版本,使用释放处理程序
。这是一个属于SASL应用程序的进程,它处理解包,安装和删除发行包。它通过release_handler
模块进行通信。有关详细信息,请参阅release_handler(3)
SASL中的手册页。
假设有一个具有安装根目录的操作目标系统$ROOT
,则包含新版本发行版的发行包将被复制到$ROOT/releases
。
首先,解压缩
发布包。然后从软件包中提取文件:
release_handler:unpack_release(ReleaseName) => {ok, Vsn}
ReleaseName
是除.tar.gz
扩展名之外的发行包的名称。
目录$ROOT/lib/releases/Vsn
中创建,在.rel
文件中,启动脚本start.boot
,系统配置文件sys.config
,并relup
放置。对于具有新版本号的应用程序,应用程序目录位于下方$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)
创建目标系统的第一个版本在系统原理描述"A"
的ch_rel
从Releases
。这一次sys.config
必须包含在发布包中。如果不需要配置,则该文件将包含空列表:
[].
步骤2)
启动系统作为一个简单的目标系统。实际上,它将作为一个嵌入式系统开始。但是,使用erl
正确的引导脚本和配置文件足以用于说明目的:
% cd $ROOT
% bin/erl -boot $ROOT/releases/A/start -config $ROOT/releases/A/sys
...
$ROOT
是目标系统的安装目录。
步骤3)
在另一个Erlang shell中,生成启动脚本并为新版本创建一个发行包"B"
。请记住包含(可能已更新)sys.config
和该relup
文件,请参阅Release Upgrade File
。
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/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
。该kernel
,stdlib
和sasl
目录不受影响,因为它们没有改变。
在$ROOT/releases
一个新的目录B
中创建,包含ch_rel-2.rel
,start.boot
,sys.config
,和relup
。
步骤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
11.8更新应用程序规格
安装新版本的发行版时,应用程序规范会自动更新所有加载的应用程序。
注意
有关新应用程序规范的信息是从发行包中包含的引导脚本获取的。因此,引导脚本是从.rel
用于构建发行包本身的相同文件生成的,这一点很重要。
具体来说,应用程序配置参数会根据(按增加的优先顺序)自动更新:
- 引导脚本中的数据,从新的应用程序资源文件中获取
App.app
这意味着在其他系统配置文件中设置的参数值和使用的值application:set_env/3
被忽略。
当已安装的版本永久化时,系统进程将init
被设置为指出新版本sys.config
。
安装完成后,应用程序控制器会比较所有正在运行的应用程序的旧配置参数和新配置参数,并调用回调函数:
Module:config_change(Changed, New, Removed)
Module
是由文件中的mod
键定义的应用程序回调模块.app
。
该函数是可选的,并且在实现应用程序回调模块时可以省略。