ERLANG远端节点奔溃导致发消息进程堵消息问题探源

问题描述:在生产环境中出现一例性能问题,A和B两个结点运行在两台服务器上,A与B互联,A不断向B发送消息。B结点所在机器发生宕机,导致A结点中发送消息的进程赌消息。

追踪过程:通过erlang:process_info(erlang:whereis(Pid))发现current_function一直是gen:do_call/4。messages消息堆积到数十万级别。
源码分析:在代码中向远端发送消息的调用函数为erlang:send(Pid,Msg),Pid是属于远端结点的接收进程。对该函数做一个简单的测试,测试环境如下,利用两台机器192.168.8.206和192.168.8.207,分别在其上运行erlang节点。在B上运行一个接收进程,测试在B机器进程、结点、机器三者挂掉的情况下send的效率问题。
服务端代码如下:
-module(recv).
-export([start/0]).
start() ->
    erlang:spawn(fun() -> erlang:register(recv,self()),loop() end).
loop() ->
    receive
        Data ->
        io:format("~p~n",[Data]),
	    loop()
    end.
测定结果:

进程、结点、机器存活情况

进程、结点、机器都存活

进程挂掉,结点和机器存活

进程、结点挂掉,机器存活

机器挂掉

1000次send消耗时间(ms)

8.333

7.6

1108

2866366.6

 

结论:机器挂掉本身对rpc:call的超时影响非常,具体原因和erlang的Trap机制有关系,每次send在机器不在线的情况下超时能接近3秒,可以确认该问题就是导致behavior_server堵消息的关键因素。

追踪源码:

Eterm erl_send(Process *p, Eterm to, Eterm msg)
{
    Sint result = do_send(p, to, msg, !0);
    if (result > 0) {
     ERTS_VBUMP_REDS(p, result);
     BIF_RET(msg);
    } else switch (result) {
    case 0:
     BIF_RET(msg);
     break;
    case SEND_TRAP:
     BIF_TRAP2(dsend2_trap, p, to, msg); 
....
erlang:send/2函数最终会进入一个Trap的过程,Trap的作用可以参考余峰老大的博文http://mryufeng.iteye.com/blog/334744,为什么会进入Trap呢?在do_send的过程中调用到remote_send
static Sint remote_send(Process *p, DistEntry *dep,
   Eterm to, Eterm full_to, Eterm msg, int suspend)
{
    Sint res;
    int code;
    ErtsDSigData dsd;
    ASSERT(is_atom(to) || is_external_pid(to));
    code = erts_dsig_prepare(&dsd, dep, p, ERTS_DSP_NO_LOCK, !suspend);
    switch (code) {
      case ERTS_DSIG_PREP_NOT_ALIVE:
      case ERTS_DSIG_PREP_NOT_CONNECTED:
       res = SEND_TRAP;
  ....
ERTS_GLB_INLINE int
erts_dsig_prepare(ErtsDSigData *dsdp,
    DistEntry *dep,
    Process *proc,
    ErtsDSigPrepLock dspl,
    int no_suspend)
{
    int failure;
    if (!erts_is_alive)
     return ERTS_DSIG_PREP_NOT_ALIVE;
    if (!dep)
     return ERTS_DSIG_PREP_NOT_CONNECTED;
...

erlang的send操作用到的第二点,延迟操作,原因是在erlang:send的时候,结点之间的连接还未建立,这个send操作就不能继续,在下一次调度的时候首先执行结点连接操作,在结点建立连接之后再继续进行。这里Trap执行的函数就是erlang的dsend/2函数。

dsend(Pid, Msg) when is_pid(Pid) ->
    case net_kernel:connect(node(Pid)) of
     true -> erlang:send(Pid, Msg);
     false -> Msg
    end;

而其中net_kernel:connect/1实际上是调用了gen:do_call去建立TCP的连接,而一旦对方机器挂掉,TCP无法收到返回,于是要等待到超时才能退出,从而导致了net_kernel:connect/1本事是阻塞方式的。

解决方案:在远端机器可能存在不可靠的情况下使用erlang:send(Pid,Msg,[noconnect])代替erlang:send(Pid,Msg)。任何大量的对某些远端结点的send,call操作都要小心。如果机器会出现长时间宕机,都会可能造成本结点对对方结点的访问出现阻塞式的访问。

原因如下:noconnect参数

BIF_RETTYPE send_3(BIF_ALIST_3)
{...
    result = do_send(p, to, msg, suspend);
    if (result > 0) {
        ERTS_VBUMP_REDS(p, result);
     BIF_RET(am_ok);
    } else switch (result) {
    case 0:
     BIF_RET(am_ok);
     break;
    case SEND_TRAP:
   if (connect) {
       BIF_TRAP3(dsend3_trap, p, to, msg, opts);
   } else {
       BIF_RET(am_noconnect);
   }
....

在设置noconnect之后erlang发送到远端的消息就不需要等待对方连接建立而是直接在对方结点不存在的时候返回noconnect。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。