一:OTP应用
浅显地说,OTP应用无非就是一组相互关联的代码。我们将其中一部分称为库应用:这些应用纯粹是供其他应用调用的一系列模块的集合。(Erlang/OTP的stdlib就是库应用的一个实例。)还有一些应用则更为常见,它们具有自己的生存周期,启动之后会运行上一段时间,最后终止。我们将这类应用称为主动应用。每个主动应用都配有一个负责对应用进程进行管理的根监督者。
1.OTP应用的组织形式
创建OTP应用时的主要工作集中于标准目录结构的建立和应用元数据的编写。元数据的作用在于让系统获悉应该如何启动和停止应用,还可用于指定应用的依赖项,也就是在应用启用前必须预先安装或启动哪些其他应用。如下图所示,Erlang/OTP应用的目录布局很简单。许多熟悉Erlang但却不了解OTP的人在开发应用时采用的也是这个目录结构,只是没有用上元数据。
注:OTP应用的目录布局。目录名可以包含版本号。标准子目录包括doc、ebin、include、priv和src,其中只有ebin是必需的 。
其中<application-name>显然应该换成你自己的应用名,此处是tcp_rpc.[-<version>]是可选项:开发时用不到它,但在交付时通常会采用tcp_xpc-1.0.2这样的目录名,这样做可以简化今后的代码升级工作。下表给出了详细的解释:
应用目录下的子目录
目录 | 描述 |
---|---|
doc | 用于存放文档。如果文档是用EDoc生成的,请将overview.cdoc文件放在此处,其余的文件将会自动生成 |
ebin | 用于存放编译后的代码(.beam文件)。含有应用元数据的,app文件也,应存放在此处 |
include | 用于存放公共头文件。所有作为公共API的一部分的.hl文件都应该放在这个目录中。仅用于你自己的代码之中且不打算公开的私有.hl文件则应该与其余的源码文件一起放在src目录下 |
priv | 用于存放各种需要随应用一起发布的其他内容。包括但不限于模板文件、共享对象文件和DLL等。定位应用priv目录的方法很简单:调用code:priv_dir(<application-name>),便会以字符串形式得到 pniv目录的完整路径 |
src | 用于存放应用的源码。不仅包括Erlang的.crl文件和内部.hrl文件,也包括ASN.l、YECC、MB等其他源文件(如果不打算随应用一起发布源码,可以省去该目录或将目录留空。) |
2.为应用添加元数据
应用的目录结构已经建立完毕,可以添加OTP所需的元数据了。这些元数据以普通Erlang:项式描述,位于ebin目录下的一个名为<application--name>.app的文本文件中。下面代码展示了
ebin/tcp_rpc.app文件,也就是tcp_rpc应用的元数据。
%% ebin/tcp_rpc..app
%% -*- mode:Erlang; fill-column: 75; comment-column: 50; -*-
{application, tcp_rpc,
[{description, "RPC server for Erlang and OTP in action"},
{vsn,"0.1.0"},
{modules, [tr_app,
tr_sup,
tr_server]},
{registered, [tr_sup]),
{applications, [kernel, stdlib]},
{mod, {tr_app, []})
]}.
这个.app文件的作用在于告诉OTP如何启动应用,以及该应用应该如何与系统中的其他应用相融合。再重复一遍,我们的首要目的并不在于打包发布,而在于组装更大的可启动、停止、监督和升级的功能单元。
.app文件的格式很简明。除去注释,只剩下一个由句号结尾的Erlang项式:三元组
{application,...,...}。其中第二个元素是应用名称所对应的原子,此处即是tcp_rpc。第三个元素是一个参数列表,其中每个参数都是{Key,Value}对的形式,有些是必需的,有些是可选的。此处罗列的是大部分应用都会用到的最为重要的那些参数。下表按上述代码中出现的顺序分别描述了这些参数:
.app文件中的主要参数
参数 | 描述 |
---|---|
description | 针对应用的简短描述。通常只有一两句话,但你想写多长都可以 |
vsn | 应用的版本。版本可以用任意字符串表示,但我们建议你坚持采用标准谁的<主版本号>.<次版本号>.<修订版本号>格式:虽然Erlang/OTP本身并不关心你使用什么字符串来表示应用的版本,但某些程序会出于特定目的自行解析应用的版本字符串,胡乱选取版本字符串很可能会让这些程序功能错乱 |
modules | 应用中的模块列表。这个清单的维护工作甚是乏味,不过有现成的工具可以助你一臂之力。模块在列表中的顺序无关紧要,但按字典序对模块进行排序会较为易于维护 |
registered | 我们知道,对Erlang进程进行注册后,便可以用注册名来定位进程。这种手法常用于系统服务等场合。在.app文件中罗列出所有进程注册名并不会促使系统执行实际的注册操作,但这可以告知OTP系统哪个进程注册了哪个名字,从而为系统升级等操作提供便利,同时也可以尽早发现重复的注册名并给出警告 |
application | 必须在该应用启动前先行启动的所有应用。应用往往会依赖于其他应用。主动应用要求自己所依赖的所有应用在自己的生命周期开始之前先行启动并就绪。列表中各应用的顺序无关紧要OTP很智能,它会纵观整个系统并明辨每个应用的启动时机 |
mod | 告知OTP系统应该如何启动应用。该参数的值是一个元组,其内容为一个模块名以及一些可选的启动参数。(不要把通用配置信息写到这些参数中一请使用正规的配置文件来存放通用配置信息。)这个模块必须实现application行为模式. |
到目前为止,你的应用还需要一个启动人口,它必须是application行为模式的一个实现模块。
3.应用行为模块
每个主动应用都配有一个application行为模式的实现模块。该模块用于实现系统启动逻辑。它至少要负责根监督者的启动,该监督者将成为应用中其他所有进程的鼻祖。根据系统需要,应用行为模式模块还可以完成一些其他任务。现在,让我们将注意力集中到src/tr_app.erl文件中应用行为模式的实现上:
%% src/tr_app.erl
-module(tr_app).
%% 行为模式声明
-behaviour(application).
%% 应用行为模式的回调函数
-export([start/2, stop/1]).
%% 启动根监督者
start(_Type,_StartArgs) ->
case tr_sup:start_link() of
{ok, Pid} ->
{ok, Pid};
other ->
{error, other}
end.
stop(_State) ->
ok,
这个小模块应该很容易理解。在这里,你实现了一个application行为模式,该行为模式要求导出两个回调start/2和stop/1。(这个模块没有任何用户API,因此除了这两个回调以外这里别无其他导出函数。)
4.小结
总结一下,建立OTP应用要做3件事:
(1)遵循标准目录结构;
(2)添加用于存放应用元数据的.app文件;
(3)创建一个application行为模式实现模块,负责启动应用。
另外还有一个有待探讨的细节,就是应用行为模式中的start/2函数是如何启动根监督者的。主动应用的目的就在于启动一个或多个进程以完成特定的任务。为了加强控制,这些进程应该由监督者—也就是实现了supervisor行为模式的进程一统一派生和管理。
二:用监督者实现容错
监督者是Erlang/OTP的核心之一。主动OTP应用由一个或多个进程组成,它们相互协作共同完成任务。监督者间接启动这些进程,对这些进程负责,并在必要时重启它们。本质上说,在运行时,应用就是一棵由监督者和工作进程共同构成的进程树,树根就是根监督者。下图展示了一个可能的假想应用的进程结构:
某假想应用的进程树。这个例子包含一个根监督者、一个由根监督者直接派生的工作进程和一个子系统的监督者,该子系统本身又由另外两个工作进程组成
你可以通过编写supervisor行为模式的实现模块来创建监督者。如果工作进程本身就是基于OTP行为模式的(如tr_server),为其设置监督者将会很容易。gen_server、gen_event和gen_fsm等标准OTP工作者行为模式融人监督树的方式一点儿也不神奇一无非就是实现一些接口、遵循返回值方面的约定并设置好进程间的链接。幸运的是,你不必了解其中的详情。如果监督树中碰巧出现了未遵守标准行为模式的代码,你可以借助标准库中的supervisor_bridgei适配器来处理相关问题。
1.实现监督者
下面代码展示的是src/tr_sup.erl,也就是tcp_pc应用的根监督者的实现。相较于tapp,该模块要稍微复杂一些,特别是除了行为模式接口的回调之外,这个模块还提供了一个API函数。通过它你才能在rapp模块中启动监督者。(实际上,tr_sup:start._link()完全可以融人tr_app:start/2,不过我们倾向于将这部分职能隔离出来,而不是把监督者相关的逻辑混入_app模块。)
%% 根监督者实现 tr_sup.erl
-module(tr_sup).
-behaviour(supervisor).
%% API
-export ([start_link/0]).
%% Supervisor callbacks
-export([init/1]).
-define(SERVER, ?MODULE).
%% 启动监督者
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%% 指明如何启动和管理子进程
init([]) ->
Server = {tr_server, {tr_server, start_link, []},
permanent, 2000, worker, [tr_server]},
Children = [Server],
%% 指明监督者的行为
Restartstrategy = {one_for_one, 0, 1},
%% 返回监督规范
{ok, {Restartstrategy, Children}}.
start_link()API函数仅负责监督者的启动,具体来说就是以模块名为参数调用库函数,其中第一个调用参数是二元组{local, ?SERVER},用于让OTP库在本地节点上以tr_sup为注册名自动注册监督进程(SERVER的定义与MODULE相同)。第三个参数是传给回调函数init/1的启动参数。由于init/1无须任何输入参数,此处仅需传人一个空表。
2.监督者重启策略
init/1回调函数的返回值的格式为{ok,{RestartStrategy,Children}},其中Children是若干子进程规范组成的一个列表,这些规范比较复杂,我们后面再详述。RestartStrategy则比较简单,它只是一个三元组{How, Max, within},此处它的值是:
Restartstrategy {one_for_one, 0, 1}
这里的How取值为one_for_one,表示一旦有子进程退出,监督者将针对该进程,且仅针对该进程进行重启。该重启操作不会影响同时运行的其他进程。Max和within这两个值(此处分别取值为0和1)是相互关联的:它们共同确定了重启频率。第一个值指定的是最大重启次数,第二个值指定的是时间片。下图就是一个一对一重启策略的一个简单策略流程:
一对一重启策略:监督者在需要重启子进程时将各个子进程看作相互独立的个体。崩遗的子进程不会影响正常的子进程
3.编写子进程规范
子进程规范是一个用于描述受监督者管理的进程的元组。对于大多数监督者而言,子进程会随监督者的启动而启动并在监督者的生命周期结束时退出。对于单个需要监督的进程,init/1函数给出的描述如下:
Server = {tr_server, {tr_server, start_link, []},
permanent, 2000, worker, [tr_server]}
%% ---------------------------------------------------------------------------------
%% 子进程规范由6个元素组成:{ID,Start,Restart,Shutdown,Type,Modules}。
%%
%% (1)第一个元素D,是一个用于在系统内部标识各规范的项式。简单起见,此处采用的是模
%% 块名,即原子tr_server。
%%
%% (2)第二项Start,是一个用于启动进程的三元组{Module,Function,Arguments}。与
%% 调用内置函数spawn/3时一样,其中第一个元素是模块名,第二个元素是函数名,第三
%% 个元素是函数的调用参数列表。在这个例子中,监督者应调用tr_server:start_link()
%% 来启动子进程(也就是tr server)。
%%
%% (3)第三个元素Restart,用于指明子进程发生故障时是否需要重启。此处指定为permanent,
%% 因为你搭建的是需要长期运行的服务,无论出于任何原因导致进程终止都应重启进程。
%% (该选项还可取值为表示永不重启进程的temporary,.以及仅在进程意外终止时重启进程
%% 的transient。)
%%
%% (4)第四个元素Shutdown,用于指明如何终止进程。此处取值为一个整数(2000),表示终
%% 止进程时应采用软关闭策略,给进程留出一段自我了断的时间(以毫秒为单位),如果进
%% 程未能在指定时间内自行退出,将被无条件终止。该选项还可取值为bruta1_kil1,表
%% 示在关闭监督进程时立即终止子进程;以及infinity,主要用于子进程本身也同为监督
%% 者的情况,表示应给予子进程充分的时间自行退出。
%% (5)第五个值Iype,用于表示进程是监督者(supervisor)还是工作者(worker)。在整个
%% 监督树中,除了实现了supervisor行为模式的监督者进程以外,剩下的都是工作进程。
%% 随着应用的复杂度的提升,你可以按自己的喜好组织监督进程进而形成层级结构,以提
%% 供更细粒度的控制(参见图4-2)。监督者可以通过yPe字段识别子进程是否同为监督者。
%% 显然此处的服务器进程是工作进程。
%%
%% (6)第六个选项列出了该进程所依赖的模块。这部分信息仅用于在代码热升级时告知系统该
%% 以何种顺序升级各个模块。一般来说,只需列出子进程的主模块,在这里就是tr_server。
%% --------------------------------------------------------------------------------
搞定了这些,就完工了!这两个小模块费了我们不少口舌,但现在你已经逾越了最大的障碍。至此,希望能够对你全局有一个清晰的把握,并明确认识这些内容之间的关系和作用。