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