热更新原理
文章参考自:
https://blog.csdn.net/mycwq/article/details/13290757
https://blog.csdn.net/mycwq/article/details/41175237
https://blog.csdn.net/mycwq/article/details/43372687
Part 1: 热更api
Erlang有以下几组API提供选择:
第一种热更新方式:
{Module, Binary, Filename} = code:get_object_code(Module),
code:load_binary(Module, Filename, Binary).
第二种热更新方式:
code:purge(Module), code:load_file(Module).
第三种热更新方式:
code:soft_purge(Module) andalso code:load_file(Module).
前面两种从erlang内部实现上来说是一样的,区别是对外接口不同。第三种和前面两种的区别是,如果当前仍有进程占用模块时是否杀掉进程强制更新,第三种则选择不更新。
三种方式实际上都是经历三个过程
方法 | 作用 |
---|---|
do_purge | 清除旧的模块代码 |
mod_to_bin | 获取模块二进制数据 |
try_load_module | 加载模块 |
Part 2: 热更新原理
erlang文档有说明:
The code of a module can exist in two variants in a system: current and old. When a module is loaded into the system for the first time, the code becomes ‘current’. If then a new instance of the module is loaded, the code of the previous instance becomes ‘old’ and the new instance becomes ‘current’.
意思是,erlang每个模块都能保存2份代码,当前版本’current’和旧版本’old’,当模块第一次被加载时,代码就是’current’版本。如果有新的代码被加载,'current’版本代码就变成了’old’版本,新的代码就成了’current’版本
这样,就算代码在热更新,有进程在调用这个模块,执行的代码也不会受影响。热更新后,这个进程执行的代码没有改变,只不过代码被标记成’old’版本。而新的进程调用这个模块时,只会访问’current’版本的代码。而’old’版本的代码如果没有进程再访问,就会在下次热更新被系统清除掉。
erlang用两个版本共存的方法来保证任何时候总有一个版本可用,这样,对外服务就不会停止。
Part 3: 例子
热更新的完整api为:
c:c(Mod): 包含 编译,去除旧代码,加载新的二进制代码
c(Mod) ->
compile:file(Mod), %% 编译erl成beam文件
code:purge(Mod), %% 清理模块(同时杀掉运行'old'代码的进程,'current'的不受影响)
code:load_file(Mod). %% 加载beam代码到vm
c:l(Mod): 少了编译过程
l(Mod) ->
code:purge(Mod),
code:load_file(Mod).
一个热更问题
-module(t).
-compile(export_all).
start() ->
Pid = spawn(fun() -> loop() end),
register(t, Pid).
loop() ->
receive
Msg ->
io:format("~p~n", [Msg])
end,
loop().
在shell中, 运行结果如下
7> t:start().
true
8> erlang:monitor(process, whereis(t)). %%进程监控
#Ref<0.0.0.56>
9> whereis(t).
<0.40.0>
10> l(t). %%第1次热更
{module,t}
11> whereis(t).
<0.40.0>
12> l(t). %%第2次热更
{module,t}
13> whereis(t).
undefined
14> flush().
Shell got {'DOWN',#Ref<0.0.0.56>,process,<0.40.0>,killed}
ok
热更新2次后,进程就被kill掉了.
解决方法1:
http://erlang.org/doc/reference_manual/code_loading.html#id86381
To change from old code to current code, a process must make a
fully qualified function call.
翻译: 要将当前标记为old的代码改变为current, 进程需要进行一次完整的函数调用(MFA).
-module(t).
-compile(export_all).
start() ->
Pid = spawn(fun() -> loop() end),
register(t, Pid).
loop() ->
receive
code_switch ->
t:loop();
Msg ->
io:format("~p~n", [Msg])
end,
loop().
就是在热更新后, 例如l(t),给这个进程发消息, code_switch ,这样进程会调用 t:loop().
这里,loop()和t:loop()有什么区别呢?
erlang根据模块划分,函数分本地调用和外部调用,其中,本地调用是调用本模块内的函数,函数可以不导出,调用形式为Atom(Args);外部调用就是调用别的模块函数,函数必须导出,调用形式为 Module:Function(Args).
MFA.
在erlang VM中,进程调用模块的过程是先加载这个模块当前版本的代码再执行,如果进程一直都是本地调用,那么所有操作都是在进程当前运行的代码中完成。换句话,这个过程中进程不会去加载新的代码。打破这种局面的就是外部调用,MFA.
解决方法2: 函数调用都改成MFA形式:
-module(t).
-compile(export_all).
start() ->
Pid = spawn(fun() -> t:loop() end),
register(t, Pid).
loop() ->
receive
Msg ->
io:format("~p~n", [Msg])
end,
t:loop().
这样做会有一些性能问题:
1.外部调用的开销比本地调用大一点。外部调用时通过指针找到这个模块函数的导出地址,当模块热更时,就会修改这个指针指向的地址。内部调用是上下文跳转,对比少了一个指针查找,外加原子锁的开销。
2.外部调用的函数代码加载的时间稍微长一点,需要获取外部函数在导出函数表的地址,避免在执行时才去导出函数表查找函数地址造成开销。
Part 4: 实际应用方法.
目前项目中热更的方法是, 通过make:files对源代码进行编译,
然后通过c:l(Mod)对文件热更.