Erlang集群的唯一标识管理
Erlang是一个非常强大的分布式平台,其中一大特点就是可以为集群中任意一个进程添加全 集群唯一标识。本文通过分析Erlang/OTP的代码,来介绍Erlang是如何完成全集群唯一名标 管理。也就是常说的进程名字管理。
Erlang中的global和local名字
在开发Erlang/OTP程序的时候,看到最多的就是gen_server,在调用 gen_server:start_link是,经常会看到{global,?MODULE}或{local,?MODULE}。那么这之间 有什么差异呢?
Erlang进程的名字
Erlang在创建的进程的时候,给予Erlang进程一个PID作为进程的标识。那么经常使用的命 名进程是怎么来的呢?是调用erlang:register
这个函数将原子和PID进行关联,从而产生了 命名的Erlang进程。而erlang:register
函数接收的第一个参数可以看到是一个原子,而不 是一个元组。那么gen_server为什么会使用一个元组呢?
gen_server是如何创建进程
先看下gen_server:start_link的代码
1 2 |
start_link(Name, Mod, Args, Options) -> gen:start(?MODULE, link, Name, Mod, Args, Options). |
从这里看到gen_server是调用gen模块进行进程创建的,那么gen模块又是如何创建进程的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
-spec start(module(), linkage(), emgr_name(), module(), term(), options()) -> start_ret(). start(GenMod, LinkP, Name, Mod, Args, Options) -> case where(Name) of undefined -> do_spawn(GenMod, LinkP, Name, Mod, Args, Options); Pid -> {error, {already_started, Pid}} end. -spec start(module(), linkage(), module(), term(), options()) -> start_ret(). start(GenMod, LinkP, Mod, Args, Options) -> do_spawn(GenMod, LinkP, Mod, Args, Options). %%----------------------------------------------------------------- %% Spawn the process (and link) maybe at another node. %% If spawn without link, set parent to ourselves 'self'!!! %%----------------------------------------------------------------- do_spawn(GenMod, link, Mod, Args, Options) -> Time = timeout(Options), proc_lib:start_link(?MODULE, init_it, [GenMod, self(), self(), Mod, Args, Options], Time, spawn_opts(Options)); do_spawn(GenMod, _, Mod, Args, Options) -> Time = timeout(Options), proc_lib:start(?MODULE, init_it, [GenMod, self(), self, Mod, Args, Options], Time, spawn_opts(Options)). do_spawn(GenMod, link, Name, Mod, Args, Options) -> Time = timeout(Options), proc_lib:start_link(?MODULE, init_it, [GenMod, self(), self(), Name, Mod, Args, Options], Time, spawn_opts(Options)); do_spawn(GenMod, _, Name, Mod, Args, Options) -> Time = timeout(Options), proc_lib:start(?MODULE, init_it, [GenMod, self(), self, Name, Mod, Args, Options], Time, spawn_opts(Options)). |
可以清楚的看到,使用的proc_lib,而proc_lib是对erlang:spawn_link
进行封装,以确保 初始化函数能正确运行,那么注册名字的秘密就在gen:init_it
中。在gen:init_it
中可以看 到一个内部函数name_register。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
name_register({local, Name} = LN) -> try register(Name, self()) of true -> true catch error:_ -> {false, where(LN)} end; name_register({global, Name} = GN) -> case global:register_name(Name, self()) of yes -> true; no -> {false, where(GN)} end; name_register({via, Module, Name} = GN) -> case Module:register_name(Name, self()) of yes -> true; no -> {false, where(GN)} end. |
此时此刻,可以看到global和local的明显差异。
local和global的区别
从上面的代码和对Erlang虚拟机的跟踪可以知道,erlang:register
管理的名字和进程PID关 联表只是调用者本地的Erlang虚拟机内的,不是整个集群中的。而global:register_name
是 通过global模块对集群中所有Erlang虚拟机进行操作。从这可以看出,Erlang语言本身并没 有所谓本地名字或集群名字的概念,而这个概念是OTP当中的(但是Erlang有本地节点进程 和远程节点进程的概念)。
Global模块分析
global模块功能
- 管理全局名字
- 管理全局锁
- 维护Erlang集群的互联互通
global模块启动
该模块是在Erlang节点启动的时候自动被启动的,并且会组册一个名为global_name_server 的进程。并且需要注意的是global模块本身就是一个gen_server,不过为了避免死循环, global模块使用gen_server注册的是本地名字。在global进程创建成功后,建立了大量的 ets表,其中global_names表,global_pid_names表就是用来管理全局命名的。
global名字注册
注册名字的时候,就是让所有节点执行{register,Name,Pid,Method}。可以看下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
register_name(Name, Pid) when is_pid(Pid) -> register_name(Name, Pid, fun random_exit_name/3). register_name(Name, Pid, Method0) when is_pid(Pid) -> Method = allow_tuple_fun(Method0), Fun = fun(Nodes) -> case (where(Name) =:= undefined) andalso check_dupname(Name, Pid) of true -> gen_server:multi_call(Nodes, global_name_server, {register, Name, Pid, Method}), yes; _ -> no end end, ?trace({register_name, self(), Name, Pid, Method}), gen_server:call(global_name_server, {registrar, Fun}, infinity). |
当gobal进程收到了{register,Name,Pid,Method}消息后,会向在global进程建立时建立的 另一个无名进程发送消息{trans_all_known, Fun, From},这个无名进程的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
loop_the_registrar() -> receive {trans_all_known, Fun, From} -> ?trace({loop_the_registrar, self(), Fun, From}), gen_server:reply(From, trans_all_known(Fun)); Other -> unexpected_message(Other, register) end, loop_the_registrar(). unexpected_message({'EXIT', _Pid, _Reason}, _What) -> %% global_name_server died ok; unexpected_message(Message, What) -> error_logger:warning_msg("The global_name_server ~w process " "received an unexpected message:\n~p\n", [What, Message]). |
这个进程会使用trans_all_known来执行传入的函数,trans_all_known函数代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
trans_all_known(Fun) -> Id = {?GLOBAL_RID, self()}, Nodes = set_lock_known(Id, 0), try %当锁住了所有的节点,才执行相关的操作 %全局的大锁呀,用多了性能还是比较差的 Fun(Nodes) after delete_global_lock(Id, Nodes) end. set_lock_known(Id, Times) -> Known = get_known(), Nodes = [node() | Known], %Boss是List中最后的那个元素 Boss = the_boss(Nodes), %% Use the same convention (a boss) as lock_nodes_safely. Optimization. %先锁定住Boss case set_lock_on_nodes(Id, [Boss]) of true -> %接这锁住剩下的节点 case lock_on_known_nodes(Id, Known, Nodes) of true -> Nodes; false -> del_lock(Id, [Boss]), random_sleep(Times), set_lock_known(Id, Times+1) end; false -> random_sleep(Times), set_lock_known(Id, Times+1) end. lock_on_known_nodes(Id, Known, Nodes) -> case set_lock_on_nodes(Id, Nodes) of true -> (get_known() -- Known) =:= []; false -> false end. set_lock_on_nodes(_Id, []) -> true; set_lock_on_nodes(Id, Nodes) -> case local_lock_check(Id, Nodes) of true -> Msg = {set_lock, Id}, {Replies, _} = gen_server:multi_call(Nodes, global_name_server, Msg), ?trace({set_lock,{me,self()},Id,{nodes,Nodes},{replies,Replies}}), check_replies(Replies, Id, Replies); false=Reply -> Reply end. |
可以看出执行流程是这样的,先锁住集群中排序最大的那个节点,如上锁成功,则让所有的 其余节点跟着上锁,如果上锁失败,则随机睡眠一段时间再接着尝试。如果当所有节点上都 拿到锁,就执行名字注册,并且执行注册后。由于使用try after语句进行包裹,在执行最 后一定会释放锁。
为什么这样上锁 首先全局的锁(GLOBAL_RID)是所有节点共享的,如果从随机的一个节点开始上锁,很容易 出现同时好几个节点都在上锁而发生锁冲突,那么大家就约定先上锁某一个节点,这样能快 速的发现锁的冲突。 其次,因为要在没给节点上的ets表中添加一个记录,如果不能在所有 参与节点上添加记录,会出现数据不一致的问题。 最后,不能只锁定一个约定的节点,考 虑到不稳定性,当节点出现异常无法连通的时候,那么这个锁的机制就无效了。