查看源码 Appup 食谱

本节包含 .appup 文件的示例,用于在运行时完成的典型升级/降级案例。

更改功能模块

当功能模块发生更改时,例如,如果添加了新函数或更正了错误,则简单的代码替换就足够了,例如

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

更改驻留模块

在根据 OTP 设计原则实现的系统中,除了系统进程和特殊进程之外,所有进程都驻留在 supervisorgen_servergen_statemgen_eventgen_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}]}]
}.

如果 m1ch3 属于同一应用程序,则 .appup 文件可以如下所示

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

在降级时,m1 也依赖于 ch3systools 知道升级和降级之间的区别,并生成正确的 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}.

在这种情况下,除了第一个参数之外,所有参数都将被忽略,并且该函数只是再次返回内部状态。如果仅扩展了代码,这已经足够了。如果改为更改内部状态(类似于 更改内部状态 中的示例),则在此函数中完成此操作并返回 {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_applicationremove_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 文件。