在Erlang 系统中,经常需要gen_server 进程来处理共享性的数据,也就是总希望一个gen_server 进程来为多个普通进程提供某种通用性的服务,这也是gen_server 设计的初衷.但是,由于公平调度的原因,在Erlang体系中,每个process 能获得的资源都是同等的:同等的CPU时间片(还有默认情况下同等的初始化内存). 也就是gen_server 进程只能获得1/(N+1)的CPU时间片,为N个进程提供通用性的服务,而无法违背公平调度的原则使gen_server 进程获得更多的资源.这也就是经常说的,Erlang 单进程性能差,Erlang 单进程单点.
为了解决上面提到的问题,目前有几种方案:
1, gen_server 进程组,就是利用多个功能相同的gen_server 进程形成group,以获得更多的进程资源;
2, 提高关键进程(热点进程)的进程优先级,保证热点进程的调度执行;
3, 避免gen_server 进程中消息队列(请求)的堆积,使用noblock call 的方式.
gen_server 进程组, gen_server pool的方式在社区中使用广泛,pool 相关的开源项目就有poolboy pooler 等, RabbitMQ 使用了worker_pool 的方式,使用与schedulers 等数量的进程组成 gen_server(2) 进程组.
在开源社区中,很少见有项目改变某些进程的进程优先级,反而在Erlang 源代码中, net_kernel 进程, 使用了max 的进程优先级, 来保证net_kernel 进程的调度执行.
而在rpc module 中,使用了call以及block_call 两种不同的请求方式(其中call 即为noblock call),同时,net_kernel module 的call 请求也采用noblock call 的方式.
在分析noblock call 之前,有必要分析下block call 方式的特点以及优缺点.
以下代码是rpc module 中处理block call 的代码片段
1 handle_call({block_call, Mod, Fun, Args, Gleader}, _To, S) -> 2 MyGL = group_leader(), 3 set_group_leader(Gleader), 4 Reply = 5 case catch apply(Mod,Fun,Args) of 6 {'EXIT', _} = Exit -> 7 {badrpc, Exit}; 8 Other -> 9 Other 10 end, 11 group_leader(MyGL, self()), % restore 12 {reply, Reply, S};
可以看到rpc module 在处理block call 请求时,基本的模式就是有请求达到时立即顺序处理,而在这种时候有其他请求达到,就只能存储在rpc 进程的消息队列中,待此次请求处理结束后才能将下一个请求从进程消息队列中检出,然后进行处理.
在请求是有状态的情况下,这种处理方式能够保证请求处理的状态性, 这也就是这种处理方式的优点. 但这种 block call 处理方式的缺点很明显, 当请求数过大, rpc module 单次请求耗时时, rpc 进程的消息队列就会不断挤压, 大量的请求就会超时. 恶性循环, 使系统整体性能下降.
在rpc module 中, 还有一种处理call 请求的方式.
1 handle_call({call, Mod, Fun, Args, Gleader}, To, S) -> 2 handle_call_call(Mod, Fun, Args, Gleader, To, S);
1 handle_call_call(Mod, Fun, Args, Gleader, To, S) -> 2 RpcServer = self(), 3 %% Spawn not to block the rpc server. 4 {Caller,_} = 5 erlang:spawn_monitor( 6 fun () -> 7 set_group_leader(Gleader), 8 Reply = 9 %% in case some sucker rex'es 10 %% something that throws 11 case catch apply(Mod, Fun, Args) of 12 {'EXIT', _} = Exit -> 13 {badrpc, Exit}; 14 Result -> 15 Result 16 end, 17 RpcServer ! {self(), {reply, Reply}} 18 end), 19 {noreply, gb_trees:insert(Caller, To, S)}.
在handle_call_call 的实现中,rpc 进程spawn_monitor (L5)一个新的进程处理实际的请求(L11), 而后立即重新进入 gen_server 的MAIN loop 中, 继而处理其他的请求.在被spawn_monitor 的新进程处理结束后, 将处理结果发回给rpc 进程(L17).
rpc 进程在handle_info callback 函数中,处理spawn_monitor 进程的放回结果, 并将结果通过gen_server:reply/2 发回给调用进程,完成本地请求的处理.
1 handle_info({Caller, {reply, Reply}}, S) -> 2 case gb_trees:lookup(Caller, S) of 3 {value, To} -> 4 receive 5 {'DOWN', _, process, Caller, _} -> 6 gen_server:reply(To, Reply), 7 {noreply, gb_trees:delete(Caller, S)} 8 end; 9 none -> 10 {noreply, S} 11 end;
net_kernel module 是Erlang 分布式特性中最为重要的一个模块,net_kernel 进程的优先级为max .
1 init({Name, LongOrShortNames, TickT}) -> 2 process_flag(trap_exit,true), 3 case init_node(Name, LongOrShortNames) of 4 {ok, Node, Listeners} -> 5 process_flag(priority, max), 6 Ticktime = to_integer(TickT), 7 Ticker = spawn_link(net_kernel, ticker, [self(), Ticktime]), 8 {ok, #state{name = Name, 9 node = Node, 10 type = LongOrShortNames, 11 tick = #tick{ticker = Ticker, time = Ticktime}, 12 connecttime = connecttime(), 13 connections = 14 ets:new(sys_dist,[named_table, 15 protected, 16 {keypos, 2}]), 17 listen = Listeners, 18 allowed = [], 19 verbose = 0 20 }}; 21 Error -> 22 {stop, Error} 23 end.
在进程init 的时候,设置了process 的 priority 为 max (L6).
net_kernel 其中一个作用是处理 remote node spawn process . 见:
1 %% 2 %% The spawn/4 BIF ends up here. 3 %% 4 handle_call({spawn,M,F,A,Gleader},{From,Tag},State) when is_pid(From) -> 5 do_spawn([no_link,{From,Tag},M,F,A,Gleader],[],State); 6 7 %% 8 %% The spawn_link/4 BIF ends up here. 9 %% 10 handle_call({spawn_link,M,F,A,Gleader},{From,Tag},State) when is_pid(From) -> 11 do_spawn([link,{From,Tag},M,F,A,Gleader],[],State); 12 13 %% 14 %% The spawn_opt/5 BIF ends up here. 15 %% 16 handle_call({spawn_opt,M,F,A,O,L,Gleader},{From,Tag},State) when is_pid(From) -> 17 do_spawn([L,{From,Tag},M,F,A,Gleader],O,State);
而处理 使用的都是noblock call 的方式.
1 do_spawn(SpawnFuncArgs, SpawnOpts, State) -> 2 [_,From|_] = SpawnFuncArgs, 3 case catch spawn_opt(?MODULE, spawn_func, SpawnFuncArgs, SpawnOpts) of 4 {'EXIT', {Reason,_}} -> 5 async_reply({reply, {'EXIT', {Reason,[]}}, State}, From); 6 {'EXIT', Reason} -> 7 async_reply({reply, {'EXIT', {Reason,[]}}, State}, From); 8 _ -> 9 {noreply,State} 10 end.
当创建新的工作进程(L3)正常时,net_kernel 进程立即{noreply, State} 进入gen_server 的MAIN loop .而 L3处 的spawn_func 的处理如下:
1 %% This code is really intricate. The link will go first and then comes 2 %% the pid, This means that the client need not do a network link. 3 %% If the link message would not arrive, the runtime system shall 4 %% generate a nodedown message 5 6 spawn_func(link,{From,Tag},M,F,A,Gleader) -> 7 link(From), 8 gen_server:reply({From,Tag},self()), %% ahhh 9 group_leader(Gleader,self()), 10 apply(M,F,A); 11 spawn_func(_,{From,Tag},M,F,A,Gleader) -> 12 gen_server:reply({From,Tag},self()), %% ahhh 13 group_leader(Gleader,self()), 14 apply(M,F,A).
在L8 或者L12 处, 使用gen_server:reply/2 返回给调用进程, 完成此次请求的处理.
在使用gen_server 进程时,要充分考虑到Erlang 单进程的效率问题, 密切关注进程的message_queue_len , 防止因进程消息队列积压,导致的进程请求超时引发的系统整体性能下降.
当然,解决这类问题的方式还有:
1, pool
2, 借助ets尽可能将处理放在请求进程本身而不是gen_server 单进程(借鉴Ejabberd)
总之,就是要对整体系统的单点密切关注,尽可能消除之.
1, https://github.com/chrismoos/hash-ring/pull/11
2, http://jlouisramblings.blogspot.com/2013/01/how-erlang-does-scheduling.html
3, https://www.erlang-solutions.com/resources/webinars/understanding-erlang-scheduler
4, http://blog.yufeng.info/archives/1438
5, http://erlangdisplay.iteye.com/blog/433843
6, https://github.com/redink/Emysql/commits/feature/add_pool_mgr