排查 reactor-netty 报错 Connection reset by peer 的过程

1. 报错现象

组内一个服务从 spring-webmvc 框架切换到 spring-webflux,在线上跑了一段时间后偶现如下错误 log 。log 中 L:/10.0.168.212:8805 代表了本地服务所在的服务器 IP 和 端口,R:/10.0.168.38:47362 表示发起请求的服务所在的服务器 IP 和 端口,整体看错误似乎是 从 10.0.168.38:47362 发起的对服务端的请求因为对端连接被重置的原因失败了。这种情况常见的原因是服务端繁忙,然而检查服务调用的监控,发现调用量正常,并不足以构成服务繁忙的条件

2020-05-110 10:35:38.462 ERROR reactor-http-epoll-1 [] reactor.netty.tcp.TcpServer.error(300) - [id: 0x230261ae, L:/10.0.168.212:8805 - R:/10.0.168.38:47362] onUncaughtException(SimpleConnection{
    
    channel=[id: 0x230261ae, L:/10.0.168.212:8805 - R:/10.0.168.38:47362]})
io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer
	at io.netty.channel.unix.FileDescriptor.readAddress(..)(Unknown Source)

2. 排查过程

2.1 Connection reset by peer 的原因

这种错误几乎没有遇到过,首先想到的当然是网上搜索错误关键字,然后找到了如下内容。很明显Connection reset by peer 就是服务端在对端 Socket 连接关闭后仍然向其传输数据引起的,但是对端关闭连接的原因却是未知

异常 原因
java.net.BindException:Address already in use: JVM_Bind 该异常发生在服务器端进行new ServerSocket(port)操作时,原因是端口已经被启动,并进行监听。此时用netstat –an命令,可以看到本地已在使用状态的端口, 只需要找一个没有被占用的端口就能解决该问题
java.net.ConnectException: Connection refused: connect 该异常发生在客户端进行 new Socket(ip, port)操作时,原因是无法找到该 ip 地址的机器(也就是从当前机器不存在到指定 ip 的路由),或者是该 ip 存在,但找不到指定的端口进行监听
java.net.SocketException: Socket is closed 该异常在客户端和服务器均可能发生,原因是己方主动关闭了连接后(调用了 Socketclose 方法)再对网络连接进行读写操作
java.net.SocketException: (Connection reset 或者 Connect reset by peer) 该异常在客户端和服务器端均有可能发生,原因有两个,第一个是如果一端的 Socket 被关闭(或主动关闭或者因为异常退出而引起的关闭,Socket默认连接60秒,60秒之内没有进行心跳交互,即读写数据,就会自动关闭连接),另一端仍发送数据,发送的第一个数据包引发该异常 (Connect reset by peer)。另一个是一端退出,但退出时并未关闭该连接,另一端如果再从连接中读数据则抛出该异常(Connection reset)。简单说就是在连接断开后的读和写操作引起的
java.net.SocketException: Broken pipe 该异常在客户端和服务器均有可能发生,在第 4 个异常的第一种情况中(Connect reset by peer),如果再继续写数据则抛出该异常

2.2 syscall:read(…) failed: Connection reset by peer 错误

继续搜索其他关键字,然后兜兜转转找到了 githubreactor-netty 的 issue。github 上其他开发者贴出的报错内容与笔者遇到的几乎完全一致,仔细阅读下来,发现其他开发者遇到这个问题主要是以下两种解决方式:

  • 禁用长连接
  • 修改负载均衡策略为最小连接数策略

从 comment 来看,这主要是涉及到了 reactor-netty 的连接池机制。我们知道 netty是基于 nio (参考Java IO模型及示例)的框架,它在处理连接请求的时候使用了一个连接池来保证并发吞吐。通过定制 ClientHttpConnector的长连接属性为 false ,保证了连接池线程不被长时间占用,这种方法在其他开发者使用的场景中似乎能有效解决这个错误

3. 最终原因

查看 github 上的 comment,总觉得其他开发者的场景与我们并不完全一致,但是一时也没有什么思路。leader 在内部群里喊了一声,到了晚上终于有同事从 log 中发现了端倪。因为服务中有打印 SQL 语句的插件,通过 log 发现有一条语句执行了整整 60s,而执行该语句的线程与之后报出错误的线程号一致,至此一切豁然开朗

  • reactor-netty 连接池分配了线程reactor-http-epoll-1处理一个请求Areactor-http-epoll-1处理过程中因为慢 SQL 一直阻塞了 60s,在此期间同一个接口被高频率访问,连接池中的其他线程也被分配来处理同一类请求,然后也因为慢 SQL 阻塞住。在连接池中的线程都被阻塞住的时候,新的请求过来,连接池中已经没有线程可以对其进行处理,请求端因此一直被 hold,直到超时后主动关闭了 Socket。这之后服务端连接池线程终于处理完慢 SQL 请求,再来处理积压的请求,完成后把数据发送往请求端,却发现连接已经被关闭,就报出了Connection reset by peer 错误。本次排查得到的经验是,如果服务报出 Connection reset by peer 错误,首先检查是不是服务中有执行特别慢的动作阻塞了线程

分析慢 SQL 发现,那条语句之所以执行耗时如此之长,是因为 MySQL 数据库中数据类型为 VARCAHR 的字段接受了 Long 数据类型的条件,造成了隐式类型转换,无法使用索引,进而引发了全表扫描。

猜你喜欢

转载自blog.csdn.net/weixin_45505313/article/details/106071050