1. 程式人生 > >ERLANG遠端節點奔潰導致發訊息程序堵訊息問題探源

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的效率問題。

服務端程式碼如下:

1

2

3

4

5

6

7

8

9

10

-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堵訊息的關鍵因素。

追蹤原始碼:

1

2

3

4

5

6

7

8

9

10

11

12

13

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

1

2

3

4

5

6

7

8

9

10

11

12

13

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;

  ....

1

2

3

4

5

6

7

8

9

10

11

12

13

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函式。

1

2

3

4

5

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引數

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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。