阿里云SLB导致rabbitmq Error on AMQP connection <0.19464.506>: enotconn (socket is not connected)

问题

测试环境正常,正常环境使用了阿里云的SLB后出现报错:

Error on AMQP connection <0.19464.506>: enotconn (socket is not connected)

频率大概是1分钟一条

原因总结

tcp监控检查有2种方式

方式1

aliyunSLB->RabbitMQ: SYN
RabbitMQ->aliyunSLB: SYN,ACK
aliyunSLB->RabbitMQ: ACK
aliyunSLB->RabbitMQ: RST,ACK

方式2

HAProxy->RabbitMQ: SYN
RabbitMQ->HAProxy: SYN,ACK
HAProxy->RabbitMQ: RST,ACK

区别

两者其实存在一定区别:根据 TCP/IP 协议中的描述,被动打开连接的一方,在收到三次握手的最后一个 ACK 时才进入 ESTABLISHED 状态,进而才能够在用户态通过 accept 取走;对比上面两种实现方式,aliyunSLB 的实现方式为先完成三次握手,再通过 RST 强行终止连接,RabbitMQ 看到的情况为:有客户端向自己建立了连接,又莫名其妙的断开;而 HAProxy 的实现方式为通过 RST 直接终止三次握手,RabbitMQ 在业务层面不会感知到这种行为;

阿里云给出的方案

正常的TCP三次握手,LVS节点服务器在收到后端ECS返回的SYN+ACK数据包后,会进一步发送ACK数据包,随后立即发送RST数据包中断TCP连接。
该实现机制可能会导致后端ECS认为相关TCP连接出现异常(非正常退出),并在业务软件如Java连接池等日志中抛出相应的错误信息,如Connection reset by peer。

解决方案:

TCP监听采用HTTP方式进行健康检查。
在后端ECS配置了获取客户端真实IP后,忽略来自前述负载均衡服务地址段相关访问导致的连接错误。

阿里云SLB健康检查流程

影响范围

目前看来,该问题会造成

  • 日志中出现大量连接相关错误信息;
  • 占用部分可用的并发连接数量;
  • 浪费一定的 Erlang VM 调度处理时间(处理这种不必要逻辑所浪费的 reduction 时间片);
  • (在相关进程的创建销毁过程中)可能会造成一定程度的内存消耗(和 GC 处理方式有关系);

源码分析

在 rabbit_reader.erl 中


...
%% 正式开始 TCP + AMQP 协议处理
start_connection(Parent, HelperSup, Deb, Sock) ->
    ...
    %% 获取当前 TCP 连接两端的 ip 和 port 信息,拼接成连接信息字符串
    Name = case rabbit_net:connection_string(Sock, inbound) of
               {ok, Str}         -> Str;
               %% 在获取时,触发 socket 错误,认为当前连接已不存在
               %% 注意:这里没有输出异常日志
               {error, enotconn} -> rabbit_net:fast_close(Sock),
                                    exit(normal);
               {error, Reason}   -> socket_error(Reason),
                                    rabbit_net:fast_close(Sock),
                                    exit(normal)
           end,
    ...
    %% 输出异常日志的地方
    %% 关键:上面 rabbit_net:connection_string 中调用的就是 rabbit_net:socket_ends
    %% 同样的代码上面没有报错,而此处会报错,说明在两段临近代码的执行间发生了 TCP 连接断开!
    {PeerHost, PeerPort, Host, Port} =
        socket_op(Sock, fun (S) -> rabbit_net:socket_ends(S, inbound) end),
    ...
    done.
...
socket_op(Sock, Fun) ->
    case Fun(Sock) of
        {ok, Res}       -> Res;
        {error, Reason} -> %% 输出错误日志
                           socket_error(Reason),
                           %% 关闭 TCP 连接
                           rabbit_net:fast_close(Sock),
                           %% 正常退出 rabbit_reader 进程
                           exit(normal)
    end.
...
socket_error(Reason) when is_atom(Reason) ->
    log(error, "Error on AMQP connection ~p: ~s~n",
        [self(), rabbit_misc:format_inet_error(Reason)]);
...

在 rabbit_net.erl 中

...
connection_string(Sock, Direction) ->
    %% 获取 socket 连接两端 ip 和 port 信息
    case socket_ends(Sock, Direction) of
        {ok, {FromAddress, FromPort, ToAddress, ToPort}} ->
            {ok, rabbit_misc:format(
                   "~s:~p -> ~s:~p",
                   [maybe_ntoab(FromAddress), FromPort,
                    maybe_ntoab(ToAddress),   ToPort])};
        Error ->
            Error
    end.

socket_ends(Sock, Direction) ->
    {From, To} = sock_funs(Direction),
    %% 获取 tcp 通信两端的 ip 和 port
    case {From(Sock), To(Sock)} of
        {{ok, {FromAddress, FromPort}}, {ok, {ToAddress, ToPort}}} ->
            {ok, {rdns(FromAddress), FromPort,
                  rdns(ToAddress),   ToPort}};
        {{error, _Reason} = Error, _} ->
            Error;
        {_, {error, _Reason} = Error} ->
            Error
    end.
...

最终解决方案

最后我们的解决方案是
在SLB里用http做健康检查
curl -i -u guest:guest http://localhost:15672/api/vhosts
请求这个api接口,如果接口返回200,表示服务存在

因为15672和5672是同一个进程监听的,5672就是rabbitMQ的端口,所以使用此方案可以解决上述的错误问题

参考了这篇文章线上 RabbitMQ 问题排查,最终解决了问题

已有 2 条评论
  1. 花荣

    在SLB里用http做健康检查
    curl -i -u guest:guest http://localhost:15672/api/vhosts
    这个在slb监控里面可以配置吗?

  2. 花荣

    在什么地方配置用户密码呢

添加新评论