9 OTP设计原则——第四部分

9.8 应用

这一章节结合 Kernel 的man page中的 app(4)application(3) 部分阅读。

9.8.1 应用概念

当你写完代码实现某个特定功能之后,你希望把它变成一个 应用 —— 可以被启动停止的组件单元,同时也能被其他系统复用。

为了做到这一点,创建一个 应用回调模块 ,还需要描述这个应用如何启动和停止的文件。

因此,需要一个 应用描述说明 ,并放到 应用源文件 中。除了这些,这个文件还表明应用由哪些模块组成以及回调模块的名称。

如果你用 systools —— Erlang/OTP的代码打包工具(查看 Releases ),为每个应用准备的代码会放到特殊的目录,遵从预定义的 *目录结构*。

9.8.2 应用回调模块

如何启动和终止应用,即监控树,用下面的两个回调函数实现:

start(StartType, StartArgs) -> {ok, Pid} | {ok, Pid, State}
stop(State)
  • start 在启动应用时调用,通过启动顶层监控器创建监控树。应该返回顶层监控器的pid和一个选项,State,默认是[]。它会传给stop程序。
  • StartType 通常来说是原子 normal。其他值用于接管或者失败,具体阅读 分布式应用
  • StartArgs 在 应用源文件 中的 mod 键中给出。
  • stop/1 在应用停止 之后 被调用,做必要的清理工作。实际的应用停止,实际上是监控树的停止,会自动处理,具体描述在 启动和停止应用 部分。

打包一个用监视器行为模式实现的监控树的应用回调模块代码如下:

-module(ch_app).
-behaviour(application).
-export([start/2, stop/1]).

start(_Type, _Args) ->
	ch_sup:start_link().
stop(_State) ->
	ok.

库应用不能被启动或停止,因此不需要应用回调模块。

9.8.3 应用源文件

为了定义完整应用,要把 应用描述信息 放到 应用源文件 中,或者说放到一个.app文件:

{application, Application, [Opt1, ... , OptN]}.
  • Application,是应用的名字原子。这个源文件名必须是Application.app。
  • 每个 Opt 是一个元组 {Key, Value},定义了应用的属性。所有键都是可选的,没有明确说明的情况会使用默认值。

库文件libapp的.app文件看起来像这样:

{application, libapp, []}.

ch_app监控树的最小.app文件ch_app.app看起来是这样:

{application, ch_app,[{mod, {ch_app,[]}}]}.

关键字mod指定了回调模块和参数,即sc_app和[]。表示启动进程会调用下面的代码:

ch_app:start(normal, []).

停止时下面的代码调用:

ch_app:stop([]).

当使用Erlang/OTP打包工具systools时,description、vsn、modules、registered和applications简直也要指定:

{application, ch_app,
 [{description, "Channel allocator"},
  {vsn, "1"},
  {modules, [ch_app, ch_sup, ch3]},
  {registered, [ch3]},
  {applications, [kernel, stdlib, sasl]},
  {mod, {ch_app,[]}}
 ]}.
  • description - 是简短的字符串描述文字,默认是空
  • vsn - 是字符串形式的版本号,默认是空
  • modules - 应用引入的所有模块。systools生成boot脚本和tar文件时要用到这个列表。一个模块只能在一个应用中定义,默认是空。
  • registered - 应用中的所有进程注册名。systools用这个列表检测应用间命名冲突。
  • applications - 这个应用的依赖应用,即必须已经启动的应用。systools用这个列表生成boot脚本。默认是空列表。注意所有应用都至少依赖Kernel和STDLIB应用。

*注意:查看Kernel的man page中的 app 部分了解更详细的配置语法和内容。 *

9.8.4 目录结构

用systools打包代码时,每个应用的代码放到不同的目录,lib/Application-Vsn,Vsn是版本号。

即使不用systools,也很有必要了解目录结构,因为Erlang/OTP的代码打包就是按照OTP规范来设计的目录结构。如果一个应用有多个版本,代码服务器(Kernel man page code(3) )自动选取版本号高的目录中的代码。

目录结构也可以用于开发环境,省略版本号。

应用目录有以下子目录:

  • src - 包含Erlang源代码
  • ebin - 包含Erlang生成文件,即beam文件。.app文件也在这里
  • priv - 应用所需的特殊代码。C可执行文件放在这里。code:priv_dir/1 函数用于访问这个目录
  • include - 用于文件包含
9.8.5 应用控制器

Erlang运行时系统启动时,一些进程作为Erlang应用的一部分也会启动起来。其中一个就是 应用控制器 进程,注册为 application_controller

应用控制器协调所有对进程的操作。可以用 application 模块中的函数来交互,查看Kernel的manpage 中 application(3) 。例如应用的加载、卸载、启动、停止。

9.8.6 应用的加载和卸载

应用启动之前需要先加载。应用控制器从.app文件中读取并存储信息:

1> application:load(ch_app).
ok
2> application:loaded_applications().
[{kernel,"ERTS CXC 138 10","2.8.1.3"},
 {stdlib,"ERTS CXC 138 10","1.11.4.3"},
 {ch_app,"Channel allocator","1"}]

如果应用停止之后不再启动,那么可以把它卸载。有关信息会从应用控制器内部数据中删除:

3> application:unload(ch_app).
ok
4> application:loaded_applications().
[{kernel,"ERTS CXC 138 10","2.8.1.3"},
 {stdlib,"ERTS CXC 138 10","1.11.4.3"}]

注意:加载/卸载应用并不会加载/卸载应用使用的代码。代码加载按照一般的方式完成。

9.8.7 启动和停止应用

应用通过以下调用启动:

5> application:start(ch_app).
ok
6> application:which_applications().
[{kernel,"ERTS CXC 138 10","2.8.1.3"},
 {stdlib,"ERTS CXC 138 10","1.11.4.3"},
 {ch_app,"Channel allocator","1"}]

如果应用没有加载,应用控制器会调用 application:load/1 加载。它检查 applications 键值,确保依赖的应用已经启动。

应用控制器随后为这个应用创建一个 应用管理(application master) 。application master是应用的所有程序的组leader。它通过调用应用回调模块的 start/2 启动应用,并传入 .app 文件的 mod 键值。

如果要停止应用,但是不卸载,调用如下:

7> application:stop(ch_app).
ok

application master通过通知顶层监视器停止应用。顶层监视器通知所有子进程关闭。整个监控树按照启动的相反顺序终止。application master调用应用回调模块的 *stop/1*。

9.8.8 配置应用

应用通过配置参数配置。配置参数是{Par, Val}的元组列表,通过.app文件中的env简直说明:

{application, ch_app,
 [{description, "Channel allocator"},
 {vsn, "1"},
 {modules, [ch_app, ch_sup, ch3]},
 {registered, [ch3]},
 {applications, [kernel, stdlib, sasl]},
 {mod, {ch_app,[]}},
 {env, [{file, "/usr/local/log"}]}
 ]}.

Par是一个原子。Val可以是任何Erlang项。应用可以调用 application:get_env(App, Par) 等一系列函数获取配置参数值,具体查看Kernel的 application(3) 内容。

例子

% erl
Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]
Eshell V5.2.3.6 (abort with ^G)
1> application:start(ch_app).
ok
2> application:get_env(ch_app, file).
{ok,"/usr/local/log"}

.app文件中的值可以被 系统配置文件 覆盖。这个文件包含相关应用的配置参数:

[{Application1, [{Par11,Val11},...]},
 ...,
 {ApplicationN, [{ParN1,ValN1},...]}].

系统配置项通过调用Name.config,Name是Erlang启动时指定的参数 -config Name。具体细节查看 Kernel 页面的 config(4) 内容。

例子

假设test.config有下面的内容:

[{ch_app, [{file, "testlog"}]}].

file 的值会覆盖 .app 文件中的键值:

% erl -config test
Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]
Eshell V5.2.3.6 (abort with ^G)
1> application:start(ch_app).
ok
2> application:get_env(ch_app, file).
{ok,"testlog"}

如果用了 发行控制 ,只有一个系统配置文件,这个文件调用 sys.config 使用。

.app文件中的值和系统配置中的值可以通过命令行覆盖:

% erl -ApplName Par1 Val1 ... ParN ValN

例子

% erl -ch_app file '"testlog"'
Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]
Eshell V5.2.3.6 (abort with ^G)
1> application:start(ch_app).
ok
2> application:get_env(ch_app, file).
{ok,"testlog"}
9.8.9 应用启动类型

启动类型 在启动应用时定义:

application:start(Application, Type)

application:start(Application) 和调用 application:start(Application, temporary) 一样。Type也可以是 permanenttransient

  • 一个permanent应用终止时,其他应用和运行时系统都会终止
  • 如果transient应用以normal方式终止,会被reported,其它应用不会终止。如果以其它原因退出,同样运行时系统和其他应用会退出。
  • 如果一个temporary应用终止,其他应用和运行时系统不受影响。

任何应用都可以通过调用 application:stop/1 停止,其他应用不受影响。

transient模式实际中很少使用,因为监控树终止时,原因是shutdown而不是normal。

9.9 应用引用

9.9.1 介绍

应用可以引用其他应用。被引用的应用有自己的应用目录和.app文件,但是它作为另一个应用监控树的一部分启动。

应用只能被一个应用引用。

被引用的应用可以引用其他应用。

没有被其他应用引用的应用被称为 主应用

应用控制器加载主应用时自动加载被引用的应用,但是不会启动它们。相反,被引用应用的顶层监控器会被包含应用中的一个监视器启动。

这意味着运行时,一个被包含的应用是主应用的一部分,被包含应用中的进程会认为自己属于主应用。

9.9.2 引用应用的定义

应用通过.app文件中的included_applications键定义:

{application, prim_app,
 [{description, "Tree application"},
  {vsn, "1"},
  {modules, [prim_app_cb, prim_app_sup, prim_app_server]},
  {registered, [prim_app_server]},
  {included_applications, [incl_app]},
  {applications, [kernel, stdlib, sasl]},
  {mod, {prim_app_cb,[]}},
  {env, [{file, "/usr/local/log"}]}
 ]}.
9.9.3 启动时的进程同步

被引用的应用监控树作为应用应用监控树的一部分启动。如果需要同步引用和被引用的应用,可以通过定义 启动阶段 实现。

启动阶段划分通过.app文件中的 start_phases 键值定义,是一组{Phase, PhaseArgs}元组列表定义,Phase是原子,PhaseArgs是Erlang项。

包含应用的应用mod键的值必须被设置为 {application_starter, [Module, StartArgs]},Module是应用回调模块名,StartArgs作为Module:start/2的参数:

{application, prim_app,
 [{description, "Tree application"},
  {vsn, "1"},
  {modules, [prim_app_cb, prim_app_sup, prim_app_server]},
  {registered, [prim_app_server]},
  {included_applications, [incl_app]},
  {start_phases, [{init,[]}, {go,[]}]},
  {applications, [kernel, stdlib, sasl]},
  {mod, {application_starter,[prim_app_cb,[]]}},
  {env, [{file, "/usr/local/log"}]}
 ]}.

{application, incl_app,
 [{description, "Included application"},
  {vsn, "1"},
  {modules, [incl_app_cb, incl_app_sup, incl_app_server]},
  {registered, []},
  {start_phases, [{go,[]}]},
  {applications, [kernel, stdlib, sasl]},
  {mod, {incl_app_cb,[]}}
 ]}.

启动有引用应用的主应用时,主应用正常启动:

  • 应用控制器为这个应用建一个application master
  • 应用控制器调用 Module:start(normal, StartArgs)启动顶层监视器

然后,对于主应用和每个被引用的应用,从上到下、从左到右调用Module:start_phase(Phase, Type, PhaseArgs)。如果应用没有定义阶段,它不会被调用。

被引用的应用.app文件有以下要求:

  • {mod, {Module, StartArgs}} 选项。指定应用回调模块Module,StartArgs忽略,因为Module:start仅会调用主应用。
  • 如果引用的应用也应用其他应用,{mod, {application_starter, [Module, StartArgs]}}也要有。
  • {start_phases, [{Phase, PhaseArgs}]},指定阶段的集合必须作为主应用阶段定义的子集。

当启动上面定义的prim_app,应用控制器在application:start(prim_app)返回之前调用下面的回调函数:

application:start(prim_app)
=> prim_app_cb:start(normal, [])
=> prim_app_cb:start_phase(init, normal, [])
=> prim_app_cb:start_phase(go, normal, [])
=> incl_app_cb:start_phase(go, normal, [])
ok

分布式应用

9.10.1 介绍

在多个Erlang节点的分布式系统,以分布式的方式控制应用很有必要。如果应用运行的某个节点down机,这个应用会被另一个节点重启。

这样的应用被称为 分布式应用 。注意应用的控制是分布式的。这种场景中的所有应用都可以分布式处理,丽日,使用其他节点上的服务。

因为分布式应用可以在节点间转移,需要必要的寻址机制确保其他节点可以被应用寻址。Kernel中的global和pg2模块用于这个目的。

9.10.2 分布式应用定义

分布式应用由应用控制器和分布式应用管理进程——dist_ac。两个进程都是Kernel应用的一部分。分布式应用由Kernel应用的配置项定义,使用下面的配置参数:

distributed = [{Application, [TimeOut,], NodeDesc}]

  • 定义Application在哪个节点运行
  • >NodeDesc = [Node | {Node, …, Node}] 是优先级排列的节点名,元组中的节点顺序没定义。
  • Timeout = integer() 表示在其他节点重启应用要等待的毫秒,默认是0。

要让分布式的应用控制工作正常,节点必须协商在哪里启动应用。使用下面的Kernel配置项定义:

  • sync_nodes_mandatory = [Node] - 定义必须启动的节点
  • sync_nodes_optional = [Node] - 定义可以启动的节点
  • sync_nodes_timeout = integer() | infinity - 定义等待其他节点启动的时间

启动时,等待通过sync_nodes_mandatory和sync_nodes_optional配置项指定的节点启动。最多等待时间由sync_nodes_timeout指定。

例子

myapp应用运行在节点 cp1@cave。如果节点挂掉,会被cp2@cave或cp3@cave重启。cp1.conf配置如下:

[{kernel,
 [{distributed, [{myapp, 5000, [cp1@cave, {cp2@cave, cp3@cave}]}]},
  {sync_nodes_mandatory, [cp2@cave, cp3@cave]},
  {sync_nodes_timeout, 5000}
 ]
}].

cp2@cave和cp3@cave配置项类似,除了sync_nodes_mandatory。

注意:相关节点的distributed值和sync_nodes_timeout值必须相同,否则可能发生预期之外的行为。

9.10.3 分布式应用的启动和停止

所有相关节点启动之后,分布式系统在所有节点调用application:start(Application)开始运行。

一个boot脚本可以用于应用的自动启动。

应用在distributed配置项指定的第一个可以使用的节点启动。应用master会被创建,并调用下面函数:

Module:start(normal, StartArgs)

例子

继续之前提到的例子:

> erl -sname cp1 -config cp1
> erl -sname cp2 -config cp2
> erl -sname cp3 -config cp3

应用通过application:stop(Application)停止。

9.10.4 容错

节点挂掉时,其他节点会试图重启它。通过distributed键值指定的第一个节点。通过调用:

Module:start(normal, StartArgs)

一个例外是如果应用有start_phases键,应用通过下面的方式调用:

Module:start({failover, Node}, StartArgs)

Node是终止的节点名。

例子:

9.10.5 接管

优先级更高的节点启动时接管应用启动所在的节点。

Module:start({takeover, Node}, StartArgs)