问题
测试环境正常,正常环境使用了阿里云的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后,忽略来自前述负载均衡服务地址段相关访问导致的连接错误。
影响范围
目前看来,该问题会造成
- 日志中出现大量连接相关错误信息;
- 占用部分可用的并发连接数量;
- 浪费一定的 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 问题排查,最终解决了问题
在SLB里用http做健康检查
curl -i -u guest:guest http://localhost:15672/api/vhosts
这个在slb监控里面可以配置吗?
在什么地方配置用户密码呢