《supervisor.erl 源码解读》

简介: erlang程序员研究OTP,如同C++程序员研究STL一样重要。

Why?

erlang程序员研究OTP,如同C++程序员研究STL一样重要。

监督者模式,在任何一个项目都必不可少。在阅读开源项目的时候,只有了解每一个细节,才能体会到妙处。

supervisor实际上是基于gen_server的系统进程,监控子进程的退出状态并设置一定的重启机制。

init函数

同样的,init是同步调用。

4adf2b9a25da2f698f475af355a03838d900fb9c.png

在这个例子里Mod模块是一个sup程序,它的启动会调用supervisor:start_link,而start_link实际上调用的gen_server:start_link并存入Mod模块的名字和参数.

spawn出来的gen进程会先调用supervisor:init函数. 接着把gen进程设置为系统进程, 这样就可以捕获子进程退出信号, 然后根据Args里的Mod模块名和参数,再次调用到Mod:init.

Mod的init函数返回的是一个{ok, SupFlags, StartSpec}的元组. SupFlags是supervisor管理的进程的启动策略和可重启的范围窗口,StartSpec是一个列表,保存多个子进程的MFA等信息.

接下来的init_state函数会把SupFlags, StartSpec存储在gen进程的State里, gen进程在整个生命周期一直围绕着这个State. 我们来看看State的结构,明白这个结构,supervisor就很好理解了.

-record(state, {name,
        strategy               :: strategy(),
        children = []          :: [child_rec()],
        dynamics               :: ?DICT(pid(), list()) | ?SET(pid()),
        intensity              :: non_neg_integer(),
        period                 :: pos_integer(),
        restarts = [],
            module,
            args}).

strategy、intensity、period和SupFlags里信息一一对应,module、args就是这个supervisor的模块名字和参数。

额外的字段说明: restarts用于存储子进程的每个重启时间点, 这个后面会解释.

dynmiacs用于当策略是simple_one_for_one策略时存储子进程的信息的字典,子进程的异常退出和重启都要用到到它;对于其它的策略,子进程信息存储在children里,它是一个child记录的列表.

-record(child, {% pid is undefined when child is not running
            pid = undefined :: child()
                             | {restarting, pid() | undefined}
                             | [pid()],
        name            :: child_id(),
        mfargs          :: mfargs(),
        restart_type    :: restart(),
        shutdown        :: shutdown(),
        child_type      :: worker(),
        modules = []    :: modules()}).

child记录和StartSpec里一一对应很好理解不解释了.

要注意: 对于非simple_one_for_one策略StartSpec里可以有多个子进程spec, 而如果是simple_one_for_one策略只能有一个进程的spec.下面的simple_one_for_one类策略的StartSpec保存函数可看到, 如果列表里的成员个数超过一个则报出bad_start_spec的错误.

init_dynamic(State, [StartSpec]) ->
    case check_startspec([StartSpec]) of
    ....
init_dynamic(_State, StartSpec) ->
    {stop, {bad_start_spec, StartSpec}}.

这里有个技巧:[StartSpec] 匹配的是只包含一个元素的列表。

存储好State信息后,gen进程根据策略的不同走到不同的分支, 如果策略是simple_one_for_one, 则在init的时候不启动任何子进程, 而由之后的start_child函数启动;对于其它的策略,则是遍历children列表把子程一一启动,并填充#child.pid字段. 等子进程都准备好了就发送ack通知调用进程,子进程都已经准备好了让我们开始吧,于是调用进程就成功退出。

start_child 函数

start_child 是同步调用。

c68a17450ea21f05c83ff90b47986a0efad78b03.png

start_child实际上是通过gen_server:call来通知gen进程启动一个子进程,然后gen进程回调调用handle_call函数。

handle_call({start_child, EArgs}, _From, State) when ?is_simple(State) ->
    #child{mfa = {M, F, A}} = hd(State#state.children),
    Args = A ++ EArgs,
    case do_start_child_i(M, F, Args) of
...

gen进程接收到消息后, 会从State里获取需要启动的子进程的MFA, 最后调用apply(M,F,A)启动这个子进程。子进程的参数来自于ssupervisor:start_link和supervisor:start_child的两次参数组合, do_start_child_i(M, F, Args) 就是启动这个子进程的函数, 之后把{Pid, Args}信息再存储在#state.dynmiac里方便重启或者which_children展示子进程状态,.

进程异常退出

由于gen进程是一个系统进程,所以子进程退出之际会向gen进程发送’EXIT’消息。根据gen_server的特点,这个消息会触发gen进程调度handle_info函数.

handle_info({'EXIT', Pid, Reason}, State) ->
    case restart_child(Pid, Reason, State) of
    {ok, State1} ->
        {noreply, State1};
    {shutdown, State1} ->
        {stop, shutdown, State1}
    end;

于是gen进程就开始重启子进程.

restart_child

45d64865e188691f7205b84b2c86a9ebc27dddc6.png

重启进程会根据消息里Pid查找到进程的信息, 如前面所述, 对与simple_one_for_one策略的进程信息来自于从#state.dynmaics, 而其它的是从#state.children里读取MFA,RestartType等信息.

根据RestartType的不同决定是否要重启

do_restart(permanent, Reason, Child, State) ->
    report_error(child_terminated, Reason, Child, State#state.name),
    restart(Child, State);
do_restart(_, normal, Child, State) ->
    NState = state_del_child(Child, State),
    {ok, NState};
do_restart(_, shutdown, Child, State) ->
    NState = state_del_child(Child, State),
    {ok, NState};
do_restart(_, {shutdown, _Term}, Child, State) ->
    NState = state_del_child(Child, State),
    {ok, NState};
do_restart(transient, Reason, Child, State) ->
    report_error(child_terminated, Reason, Child, State#state.name),
    restart(Child, State);
do_restart(temporary, Reason, Child, State) ->
    report_error(child_terminated, Reason, Child, State#state.name),
    NState = state_del_child(Child, State),
    {ok, NState}.

可以看到:

如果是permananet则永远重启;

transient仅在子进程退出Reason非normal、shutdown才重启;

而对于temprory状态则永远不重启,并删除State里的这个子进程的信息.

接着来到restart(Child, State)函数.

这个函数首先通过add_restart函数记录一个重启时间点到#state.restarts里,这样#state.restarts里保存着最近的重启时间点。计算当前时间的period时间之内的重启时间点的个数是否超过intensity, 如果超过则表示在period时间内子进程重启的次数超过maxintensity,是则放弃这次重启,否则继续重启来到restart函数.

restart函数有会根据#state.strategy来决定是重启单个进程还是所有进程,最后调用到do_start_child或者do_start_child_i函数,然后就是 apply(M,F,A)于是进程就重启了,然后再更新State里的信息等收尾工作。

which_children

这个命令很有用,可以看到一个supervisor进程下有多少个子进程和他的状。

相关文章
|
文件存储
PB调用WebService示例(含源码)
说起PowerBuilder,可能大家都会嗤之以鼻,然后说一句:“哥们,还用呢啊”?记得以前看过的电影“功夫熊猫“里说:存在即是合理。我想说得是,世界上如果这个东西或这件事情存在,一定有它的道理,好像扯得有些远啊。
1790 0
|
网络协议 NoSQL Linux
阿里云 Linux 内核优化实战(sysctl.conf 和 ulimits )
一、sysctl.conf优化Linux系统内核参数的配置文件为 /etc/sysctl.conf 和 /etc/sysctl.d/ 目录。其读取顺序为: /etc/sysctl.d/ 下面的文件按照字母排序;然后读取 /etc/sysctl.conf 。
9455 1
|
存储 关系型数据库 MySQL
一个项目用5款数据库?MySQL、PostgreSQL、ClickHouse、MongoDB区别,适用场景
一个项目用5款数据库?MySQL、PostgreSQL、ClickHouse、MongoDB——特点、性能、扩展性、安全性、适用场景比较
|
安全 网络协议 Shell
Telnet:简介、工作原理及其优缺点
【8月更文挑战第19天】
2384 0
|
机器学习/深度学习 人工智能 Ubuntu
|
存储 运维 监控
超级好用的C++实用库之日志类
超级好用的C++实用库之日志类
225 0
|
网络协议 安全 Linux
Telnet协议:远程终端协议的基础知识
Telnet协议:远程终端协议的基础知识
1651 2
|
开发框架 Dart API
Flutter引擎工作原理:深入解析FlutterEngine
【4月更文挑战第26天】FlutterEngine是Flutter应用的关键,负责Dart代码转换为原生代码,管理应用生命周期、渲染和事件处理。它初始化Flutter运行时环境,加载并编译Dart代码,创建渲染树,处理事件并实现跨平台兼容。通过理解其工作原理,开发者能更好地掌握Flutter应用内部机制并优化开发。随着Flutter生态系统发展,FlutterEngine将持续提供强大支持。
1082 1
|
消息中间件 监控 负载均衡
ZeroMQ综合指南
ZeroMQ综合指南
3169 0
|
存储 NoSQL 关系型数据库
何时使用MongoDB而不是MySql
MySQL 和 MongoDB 是两个可用于存储和管理数据的数据库管理系统。MySQL 是一个关系数据库系统,以结构化表格格式存储数据。相比之下,MongoDB 以更灵活的格式将数据存储为 JSON 文档。两者都提供性能和可扩展性,但它们为不同的应用场景提供了更好的性能。
483 1
何时使用MongoDB而不是MySql