转载自http://blog.youkuaiyun.com/zhangzhizhen1988/article/details/8005563
Erlang原本脱胎于电信行业,Jow Armstrong 在描述Erlang的设计要求时期中就提到了“软件维护应该能在不停止系统的情况下进行。”在实践中,我们也因为这种不停服务的热更新获益良多,终于不用再等到半夜没人的时候再做更新了,对于一些紧急的bug修复,热更新实在是一把圣剑(至秦玩dota的时候喜欢出圣剑),热更新的具体使用,以及它具体的运行过程是怎样的呢?
在我们自己使用热更新之前,曾经读过这么一篇博客,博主叫 坚强2002,博文叫 erlang热更新,在这篇博文中,对erlang的 code server 对于运行的代码进行热更新的细节作了很详细的分析,非常感谢这位坚强2002热心的分享,结合这篇图文并茂的博文和erlang的官方文档上的介绍后,我才得以对erlang 的热更新有更为直观的印象。
erlang 热更新的秘密都集中在code模块,code模块是Erlang code server 暴露出来的对外接口,其职责就是把已经编译好的模块加载到运行时环境。在erlang 的内核kernel中有着这么一个code server 它负责代码的运行时管理,我们在erlang的控制台输入 appmon:start().
然后点击kerner 我们就能看到神秘的code server 模块了
最左边的就是了。
代码的加载方式:
erlang在启动系统时,有embedded和interactive两种启动模式,默认是interactive模式启动,也可以通过设置启动参数把它设置成embedded启动:%erl -mode embedded
这些在官方文档中都有说明。这两种方式有什么区别呢?
embedded模式受应用场景的先知,模块的加载都是需要显示指定code server 加载模块的顺序,一般在启动脚本中指定加载顺序。当然也可以在启动之后再使用code模块的函数来作明确的加载。
然而
interactive模式在系统启动的时候只有部分代码会被加载,通常都是一些运行时自己需要的模块,其他的代码模块都是在第一次被调用的时候动态加载,当调用一个方法时发现一个模块没有加载,code server 就会搜索并加载模块。
在interactive模式下,code server维护了一个搜索代码路径的列表,通常称作 code path, code 模块的 set_path(Path) , get_path(), add_path(Dir), add_pathz(Dir), add_patha(Dir) 等一系列方法就是用来管理代码路径的.
Erlang运行时本身是code server 正常运行的前提,热更新也依赖于这个基础设施的稳定可靠,为了防止从新加载模块影响到运行时本身,kernel、stdlib、compiler这三个文件夹被标记为sticky。这意味着如果你尝试重新加载这些模块运行时会发出警告并拒绝执行,取消sticky文件夹使用 -nostick 启动选项,code模块提供了 stick_dir(Dir)、unstick_dir(Dir)、is_sticky(Module) 方法来查看那些文件夹是sticky的,判断一个文件夹是否为sticky。
代码的版本切换
代码版本有两个概念 当前版本代码'current'和老版本代码'old',一旦模块被加载就变成'current',再有一个版本过来被加载,之前的版本就变成'old',新加载的变成'current'.这时候,两个版本还是同时存在,新的请求执行的时候会使用新的版本,而老版本的代码还会被使用因为还有其他模块的调用'old'版本中(比如方法中间有一个timer:sleep会导致进程在这个方法驻留).如果再进行一次热更新,这时就有第三个实例被加载,code server就会终止掉还在驻留在'old'版本代码依赖的进程.然后第三个实例成为'current',之前版本的'current'被标记成'old'.
code模块中有一个purge(Module)的方法,用于清理旧版本的模块,移除被标记成'old'的版本,如果还有进程在占用旧版本的代码,这些进程将首先被干掉.还有一个soft_purge的方法,这个方法仅仅处理那些没有被占用的'old'版本的代码.code模块还有一些用来查看版本冲突,检查模块加载状态的方法,等等;
有一点需要注意的是要触发热更新,需要使用完全限定方式调用(fully qualified function call);
-module(m). -export([loop/0]). loop() -> receive code_switch -> m:loop(); Msg -> ... loop() end.
Notice: To make the process change code, send the messagecode_switchto it. The process then will make a fully qualified call tom:loop()and change to current code. Note thatm:loop/0must be exported.
到这儿,就不得不停下来说一下 c:l(Module).指令了,在erlang的控制台敲这个指令之后,其实是执行了两个操作,第一步是code:purge(Module). 第二部便是code:load_file(Module). 之所以这么强调这个指令,是由于我们曾在这儿栽过跟头,执行c:l(Module).的时候,会kill掉正在运行这块代码的进程,而这种kill,是非正常kill,也就是说,它不会理睬该进程的其他信息,因此如果该进程有数据未存档的话,那就悲剧了....
发现这个之后,采用了一种安全的在控制台进行热更新的方式,指令:code:soft_purge(Module) andalso code:load_file(Module). erlang的andalso 会先去判断 code:soft_purge(Module) 的返回值,如果返回false,则说明有进程当前正在执行这一块代码,则不执行后面的load_file,否则,load_file(Module)完成热更新。
之后我看了一下code:purge(Module) 和 code:soft_purge(Module) 的底层实现:
先看purge(Module).
- -specpurge(Module)->boolean()when
- Module::module().
- purge(Mod)whenis_atom(Mod)->call({purge,Mod}).%%call调用
- call(Req)->
- code_server:call(code_server,Req).%%这里转到code_server模块进行处理
- call(Name,Req)->
- Name!{code_call,self(),Req},%%这里Mod作为参数传递
- receive
- {?MODULE,Reply}->
- Reply
- end.
- loop(#state{supervisor=Supervisor}=State0)->
- receive
- {code_call,Pid,Req}->
- casehandle_call(Req,{Pid,call},State0)of%%接收到code_call的call请求后在这里处理
- {reply,Res,State}->
- reply(Pid,Res),
- loop(State);
- {noreply,State}->
- loop(State);
- {stop,Why,stopped,State}->
- system_terminate(Why,Supervisor,[],State)
- end;
- {'EXIT',Supervisor,Reason}->
- system_terminate(Reason,Supervisor,[],State0);
- {system,From,Msg}->
- handle_system_msg(running,Msg,From,Supervisor,State0);
- {'DOWN',Ref,process,_,Res}->
- State=finish_on_load(Ref,Res,State0),
- loop(State);
- _Msg->
- loop(State0)
- end.
- handle_call({purge,Mod0},{_From,_Tag},St0)->
- do_mod_call(fun(M,St)->
- do_purge(M),St}%%这里的do_purge实际上就是实际执行的函数
- end,Mod0,false,St0);
- do_mod_call(Action,Module,_Error,St)whenis_atom(Module)->%%do_mod_call是实现do_purge的一种方式
- Action(Module,St);
- do_mod_call(Action,Module,Error,St)->
- trylist_to_atom(Module)of
- Atomwhenis_atom(Atom)->
- Action(Atom,St)
- catch
- error:badarg->
- {reply,Error,St}
- end.
- <preclass="html"name="code"></pre><br>
- <p>%%do_purge(Module)<br>
- %%Killallprocessesrunningcodefrom*old*Module,andthenpurgethe<br>
- %%module.Returntrueifanyprocesseskilled,elsefalse.</p>
- <p>do_purge(Mod0)-><br>
- Mod=to_atom(Mod0),<br>
- caseerlang:check_old_code(Mod)of<br>
- false->false;<br>
- true->do_purge(processes(),Mod,false)<br>
- end.</p>
- <p>do_purge([P|Ps],Mod,Purged)-><br>
- caseerlang:check_process_code(P,Mod)of<br>
- true-><br>
- Ref=erlang:monitor(process,P),<br>
- exit(P,kill),%%这里就是为什么当有进程正在执行这段代码时,进程会被kill掉的原因了<br>
- receive<br>
- {'DOWN',Ref,process,_Pid,_}->ok<br>
- end,</p>
- <preclass="html"name="code"></pre><p>do_purge(Ps,Mod,true);</p><p>false-></p><p>do_purge(Ps,Mod,Purged)</p><p>end;</p><p>do_purge([],Mod,Purged)-></p><p>catcherlang:purge_module(Mod),</p><p>Purged.</p>
在purge时,其实对所有的进程进行了一个遍历,来判断是否有进程正在调用当前代码。此时函数erlang:check_process_code(P, Mod).的作用就是判断进程P当前有没有正在调用Mod模块的函数。
而我们再来看code:soft_purge(Module) 的代码:
- -specsoft_purge(Module)->boolean()when
- Module::module().
- soft_purge(Mod)whenis_atom(Mod)->call({soft_purge,Mod}).
- call(Req)->
- code_server:call(code_server,Req).
- call(Name,Req)->
- Name!{code_call,self(),Req},
- receive
- {?MODULE,Reply}->
- Reply
- end.
- loop(#state{supervisor=Supervisor}=State0)->
- receive
- {code_call,Pid,Req}->
- casehandle_call(Req,{Pid,call},State0)of
- {reply,Res,State}->
- reply(Pid,Res),
- loop(State);
- {noreply,State}->
- loop(State);
- {stop,Why,stopped,State}->
- system_terminate(Why,Supervisor,[],State)
- end;
- {'EXIT',Supervisor,Reason}->
- system_terminate(Reason,Supervisor,[],State0);
- {system,From,Msg}->
- handle_system_msg(running,Msg,From,Supervisor,State0);
- {'DOWN',Ref,process,_,Res}->
- State=finish_on_load(Ref,Res,State0),
- loop(State);
- _Msg->
- loop(State0)
- end.
- <spanhandle_call({soft_purge,Mod0},{_From,_Tag},St0)->
- do_mod_call(fun(M,St)->
- {reply,do_soft_purge(M),St}
- end,Mod0,true,St0);
- do_soft_purge(Mod)->
- caseerlang:check_old_code(Mod)of
- false->true;
- true->do_soft_purge(processes(),Mod)
- end.do_soft_purge([P|Ps],Mod)->
- caseerlang:check_process_code(P,Mod)of
- true->false;
- %%注意这里,如果有进程在使用这个模块的代码,则直接返回了false,注意它跟code:purge(Mod).处理方式的不同
- false->do_soft_purge(Ps,Mod)
- end;
- do_soft_purge([],Mod)->
- catcherlang:purge_module(Mod),
- true.
到此,这篇文章也就也把自己经历过的问题讲述得差不多了,强调的是,在控制台进行单个文件的热更新时,最安全的指令是用
code:soft_purge(Module) andalso code:load_file(Module).