Erlang脱胎于电信业,Joe Armstrong在描述Erlang的设计要求时其中就提到了软件维护应该能在不停止系统的情况下进行。在实践中,我们也因为这种不停服务的热更新获益良多,终于不再用等到半夜没有人的时候再做更新了.那么如何进行热更新?Erlang又是如何做到热更新的呢?这就是我们本文要回答的问题.
如何进行热更新?
关于热更新(hot_swap)可以看一下维基百科的介绍:http://en.wikipedia.org/wiki/Hot_swapping,Erlang如何进行热更新呢?其实在Erlang文档在讲述rpc模块时就给出了这样一个在多个节点上进行热更新的示例:
%% Find object code for module Mod
{Mod, Bin, File} = code:get_object_code(Mod),
%% and load it on all nodes including this one
{ResL, _} = rpc:multicall(code, load_binary, [Mod, Bin, File,]),
%% and then maybe check the ResL list.%如果是在指定节点上执行热更新还可以这样:
{_Module, Binary, Filename} = code:get_object_code(Module),
rpc:call(Node, code, load_binary, [Module, Filename, Binary])%我们也可以这样:
network_load(Module)->
{_Module, Binary, Filename} = code:get_object_code(Module),
[rpc:call(Node, code, load_binary, [Module, Filename, Binary]) || Node <- nodes()],
ok.network_load(Node, Module)->
{_Module, Binary, Filename} = code:get_object_code(Module),
rpc:call(Node, code, load_binary, [Module, Filename, Binary]),
ok.
一起看一下上面的代码,code:get_object_code(Mod)方法会查询代码路径来定位指定模块所在的位置,返回值是{Module,Binary,FileName}即{模块名,模块的二进制数据,模块的文件信息}.这个方法的执行后我们就获得了指定模块的元数据和二进制数据,后面就是通过code:load_library/3来加载新的模块了.执行完load_library方法就大功告成,完成了热更新.那么,这些操作后面发生了什么呢?
如何做到的?
Erlang热更新的秘密其实都集中在code模块,code模块是Erlang Code Server暴露出来的对外接口其职责就是把已经编译好的模块加载到Erlang的运行时环境.
代码是如何被加载的?
运行时系统可以有embedded,interactive两种启动模式,默认是interactive模式启动,这两种模式在模块的加载策略上还是有很大差异的:embedded模式受应用场景的限制,模块的加载都是需要显示指定code server加载模块的顺序,比如按照启动脚本进行加载.而interactive模式在系统启动的时候只有部分代码会被加载,通常都是一些运行时自己需要的模块.其他的代码模块都是在第一次被使用的时候动态加载.调用一个方法的时候发现一个模块没有加载,code server就会搜索并加载模块.
这个搜索是如何进行的呢?在interactive模式,code server维护了一个搜索代码路径的列表,通常被称作代码路径code path.code模块的set_path(Path) get_path() add_path(Dir) add_pathz(Dir)add_patha(Dir)等一系列的方法就是来管理代码路径的. Kernel和Stdlib相关的文件夹都会首先加载,后面的模块如果出现和OTP模块重名,就会被OTP中同名的模块覆盖;换句话说kernel和stdlib中的模块是不会被覆盖的.
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模块还有一些用来查看版本冲突,检查模块加载状态的方法,等等;
总结一下:Erlang用两个版本的共存的方法来保证某一时刻总有一个版本可用,这样对外服务就不会停止,新的版本在后续调用中就会自动使用;如果同一个模块出现多个实例,依然按照新旧版本的规则进行更换.
下面我们通过两个开源项目的代码片段看看Erlang code模块在热更新的一些应用:
首先是mochiweb(地址:git://github.com/mochi/mochiweb.git)项目reloader模块这个模块的职责就是 automatically reloading modified modules.我们直接看它reload相关的代码:
reload_modules(Modules) ->
[begin code:purge(M), code:load_file(M) end || M <- Modules].
reload(Module) ->
io:format("Reloading ~p ...", [Module]),
code:purge(Module),
case code:load_file(Module) of
{module, Module} ->
io:format(" ok.~n"),
case erlang:function_exported(Module, test, 0) of
true ->
io:format(" - Calling ~p:test() ...", [Module]),
case catch Module:test() of
ok ->
io:format(" ok.~n"),
reload;
Reason ->
io:format(" fail: ~p.~n", [Reason]),
reload_but_test_failed
end;
false ->
reload
end;
{error, Reason} ->
io:format(" fail: ~p.~n", [Reason]),
error
end.
另外一个项目我们看smerl(地址:http://code.google.com/p/smerl/),这个项目的定位就是Simple Metaprogramming for Erlang, 它做得更为灵活,它甚至可以这样:
test_smerl() ->
M1 = smerl:new(foo),
{ok, M2} = smerl:add_func(M1, "bar() -> 1 + 1."),
smerl:compile(M2),
foo:bar(), % returns 2``
smerl:has_func(M2, bar, 0). % returns true
在smerl的compile方法中使用的依然是我们熟悉的code:purge(Module)和erlang:load_module(Module, Bin) 方法.