[Erlang 0055] Erlang Shared Data using mochiglobal

简介:

%% @doc Abuse module constant pools as a "read-only shared heap" (since erts 5.6)

 

    Erlang 进程之间的消息发送都是通过数据拷贝实现的,只有一个例外就是同一个Erlang节点内的 refc binaries.关于Erlang二进制相关的内容可以参看[Erlang 0024]Erlang二进制数据处理 和 [Erlang 0032] Erlang Binary的内部实现 .消息向另外一个Erlang节点发送,首先会编码成Erlang外部数据格式(Erlang External Format)然后通过TCP/IP Socket 发送.接收到消息的节点进行消息解码然后派发到具体的进程.Erlang中就没有全局变量,像这位老兄遇到的问题,我们怎么办? Erlang中想要共享数据怎么办?
 
  1. 在当前进程内共享状态数据,首先应该想到的是使用gen_server, gen_server创建之初初始化Loop State,然后在后续操作行为中被使用,更新;
    [Erlang 0023] 理解Erlang/OTP gen_server  
  2. 在当前进程内共享数据可以使用进程字典Process Dictionary,数据保存在当前进程
    进程字典无锁,哈希实现,内容参与GC,速度很快但是几乎所有的教材里面都不建议使用Erlang进程字典,主要是担心这样会造成全局变量,带来了副作用;实际应用中对于一些一次性读入就不再变化的数据,或者变动频率非常低的数据,会放在进程字典中.速度那么快,又符合我们的应用场景,用用又何妨?
    看霸爷的测试:进程字典到底有多快 百万条级别,插入100ns, 查询40ns. 而ets的速度大概是us,差了一个数量级别。

  3. 跨进程共享数据,可以使用ETS表,从ETS表读取数据是通过内存拷贝实现的
    在Erlang VM中ETS有自己的内存管理系统,拥有独立于进程的数据区域;换句话说ETS的操作就是通过内存拷贝完成的读写;这样要拷贝的数据大小就很重要了.
    看这篇论文: A Study of Erlang ETS Table Implementations
   mochiweb项目的mochiglobal模块提供了另外一种方法:它把需要的常量编译到新的模块,Erlang Code Server加载这个模块,就可以在各个进程之间共享数据了;同一节点内避免了数据的拷贝,先看一下是怎么用的吧:
复制代码
Eshell V5.9  (abort with ^G)
1> mochiglobal:put(abc,'this_the_app_config_value_never_change').
ok
2> mochiglobal:get(abc).                                         
this_the_app_config_value_never_change
3> mochiglobal:put(abc,'ho_changed_fml').                        
ok
4> mochiglobal:get(abc).                 
ho_changed_fml
5>
复制代码
 
使用非常简单,我们可以把它看作Erlang节点内一个全局的Key_Value服务;上面说会把常量值编译到新的模块,这个什么意思呢?看下面的模块:
-module(abc).
-export([term/0]).

term() ->
         this_the_app_config_value_never_change.
 
动态编译的模块就是这样一个简单的实现,调用abc:term().返回值this_the_app_config_value_never_change.换句话说,我们调用mochiglobal:get(abc).实际上是执行了类似于abc:term()这样一个方法;我们还是在shell中看一下调用,因为动态编译的结果还略有不同:
复制代码
Eshell V5.9  (abort with ^G)
1> abc:term().
this_the_app_config_value_never_change
2> abc:module_info().
[{exports,[{term,0},{module_info,0},{module_info,1}]},
{imports,[]},
{attributes,[{vsn,[242773849471402131574616398046036072850]}]},
{compile,[{options,[{outdir,"/box/mochi-mochiweb-b277802"}]},
           {version,"4.8"},
           {time,{2012,4,19,9,32,18}},
           {source,"/box/mochi-mochiweb-b277802/abc.erl"}]}]
复制代码
 
下面就要看看mochiglobal是怎么实现的了
复制代码
-spec compile(atom(), any()) -> binary().
compile(Module, T) ->
    {ok, Module, Bin} = compile:forms(forms(Module, T),
                                      [verbose, report_errors]),
    Bin.

-spec forms(atom(), any()) -> [erl_syntax:syntaxTree()].
forms(Module, T) ->
    [erl_syntax:revert(X) || X <- term_to_abstract(Module, term, T)].

-spec term_to_abstract(atom(), atom(), any()) -> [erl_syntax:syntaxTree()].
term_to_abstract(Module, Getter, T) ->
    [%% -module(Module).
     erl_syntax:attribute(
       erl_syntax:atom(module),
       [erl_syntax:atom(Module)]),
     %% -export([Getter/0]).
     erl_syntax:attribute(
       erl_syntax:atom(export),
       [erl_syntax:list(
         [erl_syntax:arity_qualifier(
            erl_syntax:atom(Getter),
            erl_syntax:integer(0))])]),
     %% Getter() -> T.
     erl_syntax:function(
       erl_syntax:atom(Getter),
       [erl_syntax:clause([], none, [erl_syntax:abstract(T)])])].
复制代码
      上面的代码term_to_abstract完成了句法构造,erl_syntax:revert将句法转成句法树,然后 compile:forms完成编译;term_to_abstract方法的三个参数Module是模块名,它实际上是做了一个key值加前缀的拼接list_to_atom("mochiglobal:" ++ atom_to_list(K)).也就是说实际上mochiglobal:get(abc).调用产生的
动态模块名是mochiglobal:abc,实际中我们几乎不会这样给模块命名,最大程度上避免了和已有命名模块冲突;注意在shell中我们调用的时候要加一下单引号'mochiglobal:abc'否则会有语法错误.Getter参数是硬编码了原子term,参数T就是我们对应的Value值了;了解了这些我们在Erlang Shell中操练一下:
复制代码
     Eshell V5.9  (abort with ^G)
1> mochiglobal:put(abc,'this_the_app_config_value_never_change').
ok
2> abc:term().  %%%是加了前缀的 并没有abc这个模块
** exception error: undefined function abc:term/0
3> mochiglobal:abc:term().  %%%这样有语法错误
* 1: syntax error before: ':'
3> 'mochiglobal:abc':term().  %%%这样OK
this_the_app_config_value_never_change
4> mochiglobal:put(abc,'new_value_now').                         
ok
5> 'mochiglobal:abc':term().            
new_value_now
6> 'mochiglobal:abc':module_info(). %%看一下动态模块的元数据信息
[{exports,[{term,0},{module_info,0},{module_info,1}]},
{imports,[]},
{attributes,[{vsn,[241554446202275028379059762912985937376]}]},
{compile,[{options,[]}, %%注意这里并没有源代码的路径
           {version,"4.8"},
           {time,{2012,4,19,9,52,33}},
           {source,"/box/mochi-mochiweb-b277802/ebin"}]}]
复制代码
 
那我们重新复制又是怎么实现的呢?联系上面的实现,容易想到其实是走了一个代码热更新的过程
复制代码
-spec get(atom()) -> any() | undefined.
%% @equiv get(K, undefined)
get(K) ->
    get(K, undefined).

-spec get(atom(), T) -> any() | T.
%% @doc Get the term for K or return Default.
get(K, Default) ->
    get(K, Default, key_to_module(K)).

get(_K, Default, Mod) ->
    try Mod:term()
    catch error:undef ->
            Default
    end.

-spec put(atom(), any()) -> ok.
%% @doc Store term V at K, replaces an existing term if present.
put(K, V) ->
    put(K, V, key_to_module(K)).

put(_K, V, Mod) ->
    Bin = compile(Mod, V),
    code:purge(Mod),
    {module, Mod} = code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin),
    ok.
复制代码
类似的,执行delete方法实际上就是Erlang Code Server执行了模块的移除:
复制代码
-spec delete(atom()) -> boolean().
%% @doc Delete term stored at K, no-op if non-existent.
delete(K) ->
    delete(K, key_to_module(K)).

delete(_K, Mod) ->
    code:purge(Mod),
    code:delete(Mod).
复制代码
     通过分析mochiglobal的实现,我们知道了它的实现机制,并且知道这种实现成本还是比较高的,每一次复制都是走了一次热更新的过程,所以它适用的场景是数据几乎不动的情况;mochiglobal同一节点内避免了数据的拷贝,一些我们希望避免大数据拷贝的场景可以考虑使用,换一个角度去想,一些配置型静态数据可以放在ETS中,更好的策略是用工具直接生成Erlang模块文件.
 
 
相关话题
 

目录
相关文章
[Erlang 0127] Term sharing in Erlang/OTP 上篇
之前,在 [Erlang 0126] 我们读过的Erlang论文 提到过下面这篇论文:  On Preserving Term Sharing in the Erlang Virtual Machine地址: http://user.
1324 0
[Erlang 0125] Know a little Erlang opcode
  Erlang源代码编译为beam文件,代码要经过一系列的过程(见下面的简图),Core Erlang之前已经简单介绍过了Core Erlang,代码转换为Core Erlang,就容易拨开一些语法糖的真面目了.
2305 0
|
机器学习/深度学习 Shell Go