查看源码 Appup 食谱
本节包含 .appup
文件的示例,用于在运行时完成的典型升级/降级案例。
更改功能模块
当功能模块发生更改时,例如,如果添加了新函数或更正了错误,则简单的代码替换就足够了,例如
{"2",
[{"1", [{load_module, m}]}],
[{"1", [{load_module, m}]}]
}.
更改驻留模块
在根据 OTP 设计原则实现的系统中,除了系统进程和特殊进程之外,所有进程都驻留在 supervisor
、gen_server
、gen_statem
、gen_event
或 gen_fsm
这些行为之一中。它们属于 STDLIB 应用程序,升级/降级通常需要运行时系统重启。
因此,除了 特殊进程 的情况外,OTP 不支持更改驻留模块。
更改回调模块
回调模块是一个功能模块,对于代码扩展,简单的代码替换就足够了。
示例
当向 ch3
添加一个函数时,如 发布处理 中的示例所述,ch_app.appup
如下所示
{"2",
[{"1", [{load_module, ch3}]}],
[{"1", [{load_module, ch3}]}]
}.
OTP 还支持更改行为进程的内部状态;请参阅 更改内部状态。
更改内部状态
在这种情况下,简单的代码替换是不够的。进程必须在使用回调模块的新版本之前,使用回调函数 code_change/3
显式地转换其状态。因此,使用同步代码替换。
示例
考虑来自 gen_server 行为 的 ch3
模块。内部状态是表示可用通道的术语 Chs
。假设您要添加一个计数器 N
,用于跟踪到目前为止的 alloc
请求数量。这意味着格式必须更改为 {Chs,N}
。
.appup
文件可以如下所示
{"2",
[{"1", [{update, ch3, {advanced, []}}]}],
[{"1", [{update, ch3, {advanced, []}}]}]
}.
update
指令的第三个元素是一个元组 {advanced,Extra}
,它表示受影响的进程在加载模块的新版本之前要进行状态转换。这是通过进程调用回调函数 code_change/3
完成的(请参阅 STDLIB 中的 gen_server
)。术语 Extra
,在本例中为 []
,按原样传递给函数
-module(ch3).
...
-export([code_change/3]).
...
code_change({down, _Vsn}, {Chs, N}, _Extra) ->
{ok, Chs};
code_change(_Vsn, Chs, _Extra) ->
{ok, {Chs, 0}}.
第一个参数是 {down,Vsn}
(如果存在降级),或者是 Vsn
(如果存在升级)。术语 Vsn
从模块的“原始”版本中获取,即您要从中升级或降级的版本。
版本由模块属性 vsn
定义(如果有)。ch3
中没有此类属性,因此在这种情况下,版本是 beam 文件的校验和(一个巨大的整数),这是一个不重要的值,将被忽略。
ch3
的其他回调函数也必须进行修改,并且可能必须添加新的接口函数,但此处未显示。
模块依赖
假设通过添加接口函数来扩展模块,如 发布处理 中的示例所示,其中向 ch3
添加了一个函数 available/0
。
如果在模块 m1
中添加对此函数的调用,如果在发布升级期间先加载了新版本的 m1
并在加载新版本的 ch3
之前调用 ch3:available/0
,则可能会发生运行时错误。
因此,在升级时必须先加载 ch3
,然后再加载 m1
,在降级时则相反。m1
被称为依赖于 ch3
。在发布处理指令中,这由 DepMods
元素表示
{load_module, Module, DepMods}
{update, Module, {advanced, Extra}, DepMods}
DepMods
是模块列表,Module
依赖于这些模块。
示例
当从“1”升级到“2”或从“2”降级到“1”时,应用程序 myapp
中的模块 m1
依赖于 ch3
myapp.appup:
{"2",
[{"1", [{load_module, m1, [ch3]}]}],
[{"1", [{load_module, m1, [ch3]}]}]
}.
ch_app.appup:
{"2",
[{"1", [{load_module, ch3}]}],
[{"1", [{load_module, ch3}]}]
}.
如果 m1
和 ch3
属于同一应用程序,则 .appup
文件可以如下所示
{"2",
[{"1",
[{load_module, ch3},
{load_module, m1, [ch3]}]}],
[{"1",
[{load_module, ch3},
{load_module, m1, [ch3]}]}]
}.
在降级时,m1
也依赖于 ch3
。systools
知道升级和降级之间的区别,并生成正确的 relup
,其中在升级时先加载 ch3
,然后再加载 m1
,但在降级时先加载 m1
,然后再加载 ch3
。
更改特殊进程的代码
在这种情况下,简单的代码替换是不够的。当加载特殊进程的驻留模块的新版本时,进程必须对其循环函数进行完全限定的调用,以切换到新代码。因此,必须使用同步代码替换。
注意
用户定义的驻留模块的名称必须列在特殊进程的子规范的
Modules
部分中。否则,发布处理程序无法找到该进程。
示例
考虑 sys 和 proc_lib 中的示例 ch4
。当由 supervisor 启动时,子规范可以如下所示
{ch4, {ch4, start_link, []},
permanent, brutal_kill, worker, [ch4]}
如果 ch4
是应用程序 sp_app
的一部分,并且在将此应用程序从版本“1”升级到“2”时要加载该模块的新版本,则 sp_app.appup
可以如下所示
{"2",
[{"1", [{update, ch4, {advanced, []}}]}],
[{"1", [{update, ch4, {advanced, []}}]}]
}.
update
指令必须包含元组 {advanced,Extra}
。该指令使特殊进程调用回调函数 system_code_change/4
,这是用户必须实现的函数。术语 Extra
,在本例中为 []
,按原样传递给 system_code_change/4
-module(ch4).
...
-export([system_code_change/4]).
...
system_code_change(Chs, _Module, _OldVsn, _Extra) ->
{ok, Chs}.
- 第一个参数是内部状态
State
,从函数sys:handle_system_msg(Request, From, Parent, Module, Deb, State)
传递,并且在接收到系统消息时由特殊进程调用。在ch4
中,内部状态是可用通道的集合Chs
。 - 第二个参数是模块的名称 (
ch4
)。 - 第三个参数是
Vsn
或{down,Vsn}
,如gen_server:code_change/3
在 更改内部状态 中所述。
在这种情况下,除了第一个参数之外,所有参数都将被忽略,并且该函数只是再次返回内部状态。如果仅扩展了代码,这已经足够了。如果改为更改内部状态(类似于 更改内部状态 中的示例),则在此函数中完成此操作并返回 {ok,Chs2}
。
更改 supervisor
supervisor 行为支持更改内部状态,即更改重启策略和最大重启频率属性,以及更改现有的子规范。
可以添加或删除子进程,但这不会自动处理。必须在 .appup
文件中给出指令。
更改属性
由于 supervisor 要更改其内部状态,因此需要同步代码替换。但是,必须使用特殊的 update
指令。
首先,必须加载回调模块的新版本,无论是在升级还是降级的情况下。然后可以检查 init/1
的新返回值,并相应地更改内部状态。
以下 upgrade
指令用于 supervisor
{update, Module, supervisor}
示例
要将 ch_sup
(来自 Supervisor 行为)的重启策略从 one_for_one
更改为 one_for_all
,请更改 ch_sup.erl
中的回调函数 init/1
-module(ch_sup).
...
init(_Args) ->
{ok, {#{strategy => one_for_all, ...}, ...}}.
文件 ch_app.appup
{"2",
[{"1", [{update, ch_sup, supervisor}]}],
[{"1", [{update, ch_sup, supervisor}]}]
}.
更改子规范
当更改现有子规范时,指令以及 .appup
文件与之前描述的更改属性相同
{"2",
[{"1", [{update, ch_sup, supervisor}]}],
[{"1", [{update, ch_sup, supervisor}]}]
}.
这些更改不会影响现有的子进程。例如,更改启动函数仅指定以后在需要时如何重启子进程。
不能更改子规范的 id。
更改子规范的 Modules
字段可能会影响发布处理过程本身,因为此字段用于在执行同步代码替换时标识哪些进程受到影响。
添加和删除子进程
如前所述,更改子规范不会影响现有的子进程。会自动添加新的子规范,但不会删除。不会自动启动或终止子进程,必须使用 apply
指令来完成。
示例
假设在将 ch_app
从“1”升级到“2”时,要向 ch_sup
添加新的子进程 m1
。这意味着在从“2”降级到“1”时要删除 m1
{"2",
[{"1",
[{update, ch_sup, supervisor},
{apply, {supervisor, restart_child, [ch_sup, m1]}}
]}],
[{"1",
[{apply, {supervisor, terminate_child, [ch_sup, m1]}},
{apply, {supervisor, delete_child, [ch_sup, m1]}},
{update, ch_sup, supervisor}
]}]
}.
指令的顺序很重要。
supervisor 必须注册为 ch_sup
,脚本才能工作。如果 supervisor 未注册,则无法直接从脚本访问。相反,必须编写一个帮助函数,该函数查找 supervisor 的 pid 并调用 supervisor:restart_child
等。然后,该函数要使用 apply
指令从脚本中调用。
如果在 ch_app
的版本“2”中引入了模块 m1
,则还必须在升级时加载该模块,并在降级时删除该模块
{"2",
[{"1",
[{add_module, m1},
{update, ch_sup, supervisor},
{apply, {supervisor, restart_child, [ch_sup, m1]}}
]}],
[{"1",
[{apply, {supervisor, terminate_child, [ch_sup, m1]}},
{apply, {supervisor, delete_child, [ch_sup, m1]}},
{update, ch_sup, supervisor},
{delete_module, m1}
]}]
}.
如前所述,指令的顺序非常重要。在升级时,必须先加载 m1
,并更改 supervisor 的子规范,然后才能启动新的子进程。在降级时,必须先终止子进程,然后才能更改子规范并删除模块。
添加或删除模块
_示例
_ 将一个新的功能模块 m
添加到 ch_app
{"2",
[{"1", [{add_module, m}]}],
[{"1", [{delete_module, m}]}]
启动或终止进程
在按照 OTP 设计原则构建的系统中,任何进程都将是属于 supervisor 的子进程,请参阅更改 Supervisor 中的添加和删除子进程。
添加或删除应用程序
添加或删除应用程序时,不需要 .appup
文件。生成 relup
时,会比较 .rel
文件,并自动添加 add_application
和 remove_application
指令。
重启应用程序
当更改过于复杂,无法在不重启进程的情况下进行时(例如,如果 supervisor 层次结构已重组),重启应用程序非常有用。
示例
当向 ch_sup
添加子进程 m1
时,如更改 Supervisor 中的添加和删除子进程所示,更新 supervisor 的替代方法是重启整个应用程序。
{"2",
[{"1", [{restart_application, ch_app}]}],
[{"1", [{restart_application, ch_app}]}]
}.
更改应用程序规范
安装发布版本时,会在评估 relup
脚本之前自动更新应用程序规范。因此,.appup
文件中不需要任何指令。
{"2",
[{"1", []}],
[{"1", []}]
}.
更改应用程序配置
通过更新 .app
文件中的 env
键来更改应用程序配置,是更改应用程序规范的一个实例,请参阅上一节。
或者,可以在 sys.config
中添加或更新应用程序配置参数。
更改包含的应用程序
用于添加、删除和重启应用程序的发布处理指令仅适用于主要应用程序。对于包含的应用程序,没有相应的指令。但是,由于包含的应用程序实际上是一个具有最顶层 supervisor 的监督树,作为包含应用程序中 supervisor 的子进程启动,因此可以手动创建 .relup
文件。
示例
假设有一个包含应用程序 prim_app
的发布版本,该应用程序在其监督树中有一个 supervisor prim_sup
。
在新版本的发布版本中,应用程序 ch_app
将包含在 prim_app
中。也就是说,其最顶层 supervisor ch_sup
将作为 prim_sup
的子进程启动。
工作流程如下
步骤 1) 编辑 prim_sup
的代码
init(...) ->
{ok, {...supervisor flags...,
[...,
{ch_sup, {ch_sup,start_link,[]},
permanent,infinity,supervisor,[ch_sup]},
...]}}.
步骤 2) 编辑 prim_app
的 .app
文件
{application, prim_app,
[...,
{vsn, "2"},
...,
{included_applications, [ch_app]},
...
]}.
步骤 3) 创建一个新的 .rel
文件,包括 ch_app
{release,
...,
[...,
{prim_app, "2"},
{ch_app, "1"}]}.
可以通过两种方式启动包含的应用程序。这在接下来的两节中描述。
应用程序重启
步骤 4a) 启动包含的应用程序的一种方法是重启整个 prim_app
应用程序。通常,将使用 prim_app
的 .appup
文件中的 restart_application
指令。
但是,如果这样做并生成 .relup
文件,不仅会包含重启(即删除和添加)prim_app
的指令,还会包含启动 ch_app
(以及在降级情况下停止)的指令。这是因为 ch_app
包含在新的 .rel
文件中,但未包含在旧文件中。
相反,可以手动创建一个正确的 relup
文件,可以从头开始,也可以通过编辑生成的版本。用于启动/停止 ch_app
的指令将被加载/卸载应用程序的指令替换。
{"B",
[{"A",
[],
[{load_object_code,{ch_app,"1",[ch_sup,ch3]}},
{load_object_code,{prim_app,"2",[prim_app,prim_sup]}},
point_of_no_return,
{apply,{application,stop,[prim_app]}},
{remove,{prim_app,brutal_purge,brutal_purge}},
{remove,{prim_sup,brutal_purge,brutal_purge}},
{purge,[prim_app,prim_sup]},
{load,{prim_app,brutal_purge,brutal_purge}},
{load,{prim_sup,brutal_purge,brutal_purge}},
{load,{ch_sup,brutal_purge,brutal_purge}},
{load,{ch3,brutal_purge,brutal_purge}},
{apply,{application,load,[ch_app]}},
{apply,{application,start,[prim_app,permanent]}}]}],
[{"A",
[],
[{load_object_code,{prim_app,"1",[prim_app,prim_sup]}},
point_of_no_return,
{apply,{application,stop,[prim_app]}},
{apply,{application,unload,[ch_app]}},
{remove,{ch_sup,brutal_purge,brutal_purge}},
{remove,{ch3,brutal_purge,brutal_purge}},
{purge,[ch_sup,ch3]},
{remove,{prim_app,brutal_purge,brutal_purge}},
{remove,{prim_sup,brutal_purge,brutal_purge}},
{purge,[prim_app,prim_sup]},
{load,{prim_app,brutal_purge,brutal_purge}},
{load,{prim_sup,brutal_purge,brutal_purge}},
{apply,{application,start,[prim_app,permanent]}}]}]
}.
Supervisor 更改
步骤 4b) 启动包含的应用程序(或在降级情况下停止它)的另一种方法是将向 prim_sup
添加和删除子进程的指令与加载/卸载所有 ch_app
代码及其应用程序规范的指令结合起来。
同样,.relup
文件是手动创建的,可以从头开始,也可以通过编辑生成的版本。首先加载 ch_app
的所有代码,并加载应用程序规范,然后再更新 prim_sup
。降级时,应先更新 prim_sup
,然后再卸载 ch_app
的代码及其应用程序规范。
{"B",
[{"A",
[],
[{load_object_code,{ch_app,"1",[ch_sup,ch3]}},
{load_object_code,{prim_app,"2",[prim_sup]}},
point_of_no_return,
{load,{ch_sup,brutal_purge,brutal_purge}},
{load,{ch3,brutal_purge,brutal_purge}},
{apply,{application,load,[ch_app]}},
{suspend,[prim_sup]},
{load,{prim_sup,brutal_purge,brutal_purge}},
{code_change,up,[{prim_sup,[]}]},
{resume,[prim_sup]},
{apply,{supervisor,restart_child,[prim_sup,ch_sup]}}]}],
[{"A",
[],
[{load_object_code,{prim_app,"1",[prim_sup]}},
point_of_no_return,
{apply,{supervisor,terminate_child,[prim_sup,ch_sup]}},
{apply,{supervisor,delete_child,[prim_sup,ch_sup]}},
{suspend,[prim_sup]},
{load,{prim_sup,brutal_purge,brutal_purge}},
{code_change,down,[{prim_sup,[]}]},
{resume,[prim_sup]},
{remove,{ch_sup,brutal_purge,brutal_purge}},
{remove,{ch3,brutal_purge,brutal_purge}},
{purge,[ch_sup,ch3]},
{apply,{application,unload,[ch_app]}}]}]
}.
更改非 Erlang 代码
更改用 Erlang 以外的其他编程语言编写的程序的代码(例如,端口程序)取决于应用程序,并且 OTP 不提供特殊支持。
示例
当更改端口程序的代码时,假设控制端口的 Erlang 进程是 gen_server
portc
,并且该端口是在回调函数 init/1
中打开的。
init(...) ->
...,
PortPrg = filename:join(code:priv_dir(App), "portc"),
Port = open_port({spawn,PortPrg}, [...]),
...,
{ok, #state{port=Port, ...}}.
如果要更新端口程序,可以使用 code_change/3
函数扩展 gen_server
的代码,该函数关闭旧端口并打开新端口。(如有必要,gen_server
可以首先请求必须从端口程序保存的数据,并将此数据传递给新端口)
code_change(_OldVsn, State, port) ->
State#state.port ! close,
receive
{Port,close} ->
true
end,
PortPrg = filename:join(code:priv_dir(App), "portc"),
Port = open_port({spawn,PortPrg}, [...]),
{ok, #state{port=Port, ...}}.
更新 .app
文件中的应用程序版本号,并编写 .appup
文件
["2",
[{"1", [{update, portc, {advanced,port}}]}],
[{"1", [{update, portc, {advanced,port}}]}]
].
确保 C 程序所在的 priv
目录包含在新的发布包中
1> systools:make_tar("my_release", [{dirs,[priv]}]).
...
运行时系统重启和升级
两条升级指令会重启运行时系统
restart_new_emulator
当 ERTS、Kernel、STDLIB 或 SASL 升级时使用。当
systools:make_relup/3,4
生成relup
文件时,会自动添加它。它在所有其他升级指令之前执行。有关此指令的更多信息,请参阅发布处理指令中的 restart_new_emulator (Low-Level)。restart_emulator
当所有其他升级指令执行后需要重启运行时系统时使用。有关此指令的更多信息,请参阅发布处理指令中的 restart_emulator (Low-Level)。
如果需要重启运行时系统,并且不需要任何升级指令,也就是说,如果重启本身足以使升级后的应用程序开始运行新版本,则可以手动创建一个简单的 .relup
文件
{"B",
[{"A",
[],
[restart_emulator]}],
[{"A",
[],
[restart_emulator]}]
}.
在这种情况下,可以使用发布处理程序框架,自动打包和解包发布包、自动更新路径等,而无需指定 .appup
文件。