Erlang generic standard behaviours -- gen_server noblock call

时间:2022-06-01 21:03:07

在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 的方式.

block 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 进程的消息队列就会不断挤压, 大量的请求就会超时. 恶性循环, 使系统整体性能下降.

noblock call

rpc noblock call

在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 noblock call

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