[Erlang 0055] Erlang Shared Data using mochiglobal

来源:互联网 发布:买的域名怎么使用 编辑:程序博客网 时间:2024/04/29 09:24

  %% @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').ok2> mochiglobal:get(abc).                                         this_the_app_config_value_never_change3> mochiglobal:put(abc,'ho_changed_fml').                        ok4> mochiglobal:get(abc).                 ho_changed_fml5>
 
使用非常简单,我们可以把它看作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_change2> 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').ok2> abc:term().  %%%是加了前缀的 并没有abc这个模块** exception error: undefined function abc:term/03> mochiglobal:abc:term().  %%%这样有语法错误* 1: syntax error before: ':'3> 'mochiglobal:abc':term().  %%%这样OKthis_the_app_config_value_never_change4> mochiglobal:put(abc,'new_value_now').                         ok5> 'mochiglobal:abc':term().            new_value_now6> '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模块文件.
 
 
相关话题
 
[1] erlang gen_server with a large state
[2] Speed-up and best practices: Using ets for per-module pre-computed data
[3] https://github.com/mochi/mochiweb/blob/master/src/mochiglobal.erl