erlang应用脚本stop分析

erlang应用脚本stop分析

其实这篇文章的名字应该是如何安全关闭erlang应用更加科学。

erlang应用脚本生成

使用rebar工具,创建一个erlang节点后,
<pre>
./rebar create-node nodeid=hook_heroes
</pre>
然后在rel目录里面,执行打包命令
<pre>
./rebar generate
</pre>
会生成完整的应用包,目录如下:
<pre>
bin erts-6.0 lib log releases
</pre>
bin里面,有一个启动脚本名字和节点名字一样的,这里是hook_heroes

停止服务的时候,目前使用
<pre>
./hook_heroes stop
</pre>

对于hook_heroes stop分析

hook_heroes stop调用如下
<pre>
%%Tell nodetool to initiate a stop
$NODETOOL stop
ES=$?
if [ "$ES" -ne 0 ]; then
exit $ES
fi
</pre>
这里的nodetool来自
<pre>
NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool
</pre>
即erts包下面的nodetool脚本,传入的参数stop
nodetool是一个escript脚本,作用就是“Helper Script for interacting with live nodes”

<pre>
case RestArgs of
["getpid"] ->
io:format("~p\n",
[list_to_integer(rpc:call(TargetNode, os, getpid, []))]);
["ping"] ->
io:format("pong\n");
["stop"] ->
io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]);
.......
</pre>

可以看到,直接使用的是rpc:call()方法:调用TargetNode的init模块的stop方法,传入的参数为[],下面来看看init模块的stop方法。

init模块的stop()方法调用

init 模块的文档给的解释是:“Coordination of System Startup”,
stop方法的注释是:
<pre>
All applications are taken down smoothly, all code is unloaded, and all ports are closed before the system terminates
</pre>
显然就是用来系统关闭的,关键是需要看看他是怎么关闭系统的。

函数入口:

<pre>
stop() -> init ! {stop,stop}, ok.
</pre>
给init模块发送自己发送一个{stop,stop}消息,

init自己循环接收消息
<pre>
loop(State) ->
receive
{'EXIT',Pid,Reason} ->
Kernel = State#state.kernel,
terminate(Pid,Kernel,Reason), %% If Pid is a Kernel pid, halt()!
loop(State);
{stop,Reason} ->
stop(Reason,State);
{From,fetch_loaded} -> %% The Loaded info is cleared in
Loaded = State#state.loaded, %% boot_loop but is handled here
From ! {init,Loaded}, %% anyway.
loop(State);
{From, {ensure_loaded, _}} ->
From ! {init, not_allowed},
loop(State);
Msg ->
loop(handle_msg(Msg,State))
end.
</pre>

匹配到{stop,Reason},进入stop(Reason,State)这里调用,Reason为stop,
来打这里
<pre>
stop(Reason,State) ->
BootPid = State#state.bootpid,
{_,Progress} = State#state.status,
State1 = State#state{status = {stopping, Progress}},
clear_system(BootPid,State1),
do_stop(Reason,State1).
</pre>
重点看下clear_system函数和do_stop函数

clear_system()函数

clear_system()这里的作用就是关闭虚拟机中的进程,只用三个函数调用
<pre>
clear_system(BootPid,State) ->
Heart = get_heart(State#state.kernel), %A
shutdown_pids(Heart,BootPid,State), %B
unload(Heart). %C
</pre>

A和C都是在处理erlang启动参数heart,其意义在vm.args有说明
<pre>
Heartbeat management; auto-restarts VM if it dies or becomes unresponsive
(Disabled by default..use with caution!)
-heart
</pre>
一般情况下,不使用-heart
我们这里只看shutdown_pids()怎么做的。

shutdown_pids()函数

<pre>
shutdown_pids(Heart,BootPid,State) ->
Timer = shutdown_timer(State#state.flags),
catch shutdown(State#state.kernel,BootPid,Timer,State),
kill_all_pids(Heart), % Even the shutdown timer.
kill_all_ports(Heart),
flush_timout(Timer).
</pre>

这里首先关闭定时器,然后关闭kernel进程,然后再kill其余的进程。

关闭kernel进程
<pre>
%%
%% A kernel pid must handle the special case message
%% {'EXIT',Parent,Reason} and terminate upon it!
%%
shutdown_kernel_pid(Pid, BootPid, Timer, State) ->
Pid ! {'EXIT',BootPid,shutdown},
shutdown_loop(Pid, Timer, State, []).
</pre>

什么是erlang的kernel进程?

这句话是重点: A kernel pid must handle the special case message and terminate upon it!
那么什么是kernel进程呢?
看下bin/start.script
<pre>
...
{kernelProcess,heart,{heart,start,[]}},
{kernelProcess,error_logger,{error_logger,start_link,[]}},
{kernelProcess,application_controller,
{application_controller,start,
[{application,kernel,
...
</pre>

这些带kernelProcess标签的进程都是, 特别是application!

来自http://blog.yufeng.info/archives/1411

故supervisor_tree收到的是{'EXIT',BootPid,shutdown}

kill其余的进程:
<pre>
kill_all_pids(Heart) ->
case get_pids(Heart) of
[] ->
ok;
Pids ->
kill_em(Pids),
kill_all_pids(Heart) % Continue until all are really killed.
end.
</pre>
最终跟下去,使用的是
<pre>
exit(Pid,kill)
</pre>
向各个进程发送kill消息。

supervisor terminate方法

supervisor中的terminate()方法如下:
<pre>
-spec terminate(term(), state()) -> 'ok'.

terminate(_Reason, #state{children=[Child]} = State) when ?is_simple(State) ->
terminate_dynamic_children(Child, dynamics_db(Child#child.restart_type,
State#state.dynamics),
State#state.name);
terminate(_Reason, State) ->
terminate_children(State#state.children, State#state.name).
</pre>

分为simple_one_for_one和非simple_one_for_one两种情况。
terminate_dynamic_children()方法:
<pre>
...
EStack = case Child#child.shutdown of
brutal_kill ->
?SETS:fold(fun(P, _) -> exit(P, kill) end, ok, Pids),
wait_dynamic_children(Child, Pids, Sz, undefined, EStack0);
infinity ->
?SETS:fold(fun(P, _) -> exit(P, shutdown) end, ok, Pids),
wait_dynamic_children(Child, Pids, Sz, undefined, EStack0);
Time ->
?SETS:fold(fun(P, _) -> exit(P, shutdown) end, ok, Pids),
TRef = erlang:start_timer(Time, self(), kill),
wait_dynamic_children(Child, Pids, Sz, TRef, EStack0)
end,
...
</pre>

可以看出ChildSpec中的ShowDown字段的设置对于关闭子进程的影响:
brutal_kill:发送kill消息,这个消息是不能捕捉的。即使如果worker设置了process_flag(trap_exit, true),仍然不会收到{'EXIT',_FROM,REASON}这个消息;
infinity和Time都会向监督的worker进程发送shutdown信号,这里worker做了 process_flag(trap_exit, true),自然会收到{'EXIT',_FROM,REASON}。唯一的区别是infinity会一直等待,Time会设置一个超时:如果超时过了,那么supervisor会发送kill信号,直接杀死。
根据上面的分析,不难和erlang文档中对于gen_server terminate()方法
<pre>
If the gen_server is part of a supervision tree and is ordered by its supervisor to terminate, this function will be called with Reason=shutdown if the following conditions apply:

the gen_server has been set to trap exit signals, and
the shutdown strategy as defined in the supervisor's child specification is an integer timeout value, not brutal_kill.
</pre>

supervisor何时调用terminate()方法

最后一个问题来了,supervisor何时调用terminate()方法?之前分析到,关闭kernel进程的时候,supervisor监控树进程会收到来自BootPid的{'EXIT',BootPid,shutdown}消息。我们知道supervisor实际上一个gen_server,那么去看看他的handle_info()方法好了。

<pre>
-spec handle_info(term(), state()) ->
{'noreply', state()} | {'stop', 'shutdown', state()}.

handle_info({'EXIT', Pid, Reason}, State) ->
case restart_child(Pid, Reason, State) of %重启child
{ok, State1} -> %A
{noreply, State1};
{shutdown, State1} -> %B
{stop, shutdown, State1}
end;

handle_info(Msg, State) ->
error_logger:error_msg("Supervisor received unexpected message: pn",
[Msg]),
{noreply, State}.
</pre>

这里代码显然都是handle_info child发送过来的信号,调用restart_child()。在跟踪restart_child()进去,也没有看出原因:因为传入Pid并不是Child,而是BootPid,总是会走到A分支,也就是说不会调用terminate方法。这里陷入困境。
后来翻阅了supervisor文档,发现居然没有terminate()方法的说明,再次陷入困境。
最后,想起supervisor实际上一个gen_server,应该去看看gen_server()文档对于terminate()方法地说明。
<pre>
...
Even if the gen_server is not part of a supervision tree, this function will be called if it receives an 'EXIT' message from its parent. Reason will be the same as in the 'EXIT' message.
...
</pre>
这里说明,只要gen_server收到了来自parent的'EXIT' message,terminate()方法就会调用。符合之前分析地:
<pre>
{'EXIT',BootPid,shutdown}
</pre>
至于BootPid和SuperVisor是否是parent关系,这里暂时没时间探究:不过一定会是,否则,顶层的sup一定要有人通知关闭啊,而且BootPid从命名来看,相当有可能。这里留一个坑后面填上,主要是init:start()的启动。

其它
  • 之前代码中的player进程的child_spec的show_down写的是brutal_kill,这里显然写错了;那么应用关闭的时候,自然不会调用terminate方法
  • Erlang OTP之terminate 深入分析这篇文章是基于erlang 14A版本的,他建议使用one_for_one。原因很简单,erlang 14A中,supervisor的terminate()函数如下
    <pre>
    terminate(_Reason, State) ->
    terminate_children(State#state.children, State#state.name),
    ok.
    </pre>
    对于17版本,可以看出,这里没有处理单独simple_one_for_one的情况。因为simple_one_for_one和one_for_one的child信息在supervisor里面存储的是不一样的:前者child存储在dynamics属性,
    后者存储在children属性。erlang 14A的版本只处理了children里面的child,对于simple_one_for_one的child直接没有处理。
    对于这篇文章的实验,我在自己电脑上也做了实验,确实和他的结果不一致。
参考资料
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • erlang应用脚本stop分析 其实这篇文章的名字应该是如何安全关闭erlang应用更加科学。 erlang应用...
    randyjia阅读 1,210评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • supervisor 是由python语言编写、基于linux操作系统的一款服务器管理工具,用以监控服务器的运行,...
    每次哭都笑着奔跑阅读 6,276评论 6 14
  • 下标语法概念: 类和结构体以及枚举能够声明下标快捷访问集合中的成员。例如数组someArray[index]或者字...
    成功的失败者阅读 984评论 0 0
  • 夏季的高原无疑是安静的,静的让人心烦意乱、让人撕心裂肺、让人不知所措,那静有种说不出的味道,是沉到心底里,掏空了细...
    木皮叔阅读 509评论 1 6