文章目录
- 1. 报错现象
- 2. 排查过程
- 2.1 Connection reset by peer 的原因
- 2.2 syscall:read(..) failed: Connection reset by peer 错误
- 3. 最终原因
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
连接关闭后仍然向其传输数据引起的,但是对端关闭连接的原因却是未知
异常 | 原因 |
| 该异常发生在服务器端进行 |
| 该异常发生在客户端进行 |
| 该异常在客户端和服务器均可能发生,原因是己方主动关闭了连接后(调用了 |
| 该异常在客户端和服务器端均有可能发生,大致分为两个,第一个是当前端的 Socket 收到对端的 RST 报文后仍然读数据(对端因为异常退出而引起关闭会发送 RST,例如 Socket 设置读超时 60 秒,60秒之内没有心跳交互就会触发超时,异常关闭连接),引发 |
| 该异常在客户端和服务器均有可能发生,当前端在读写数据前断开连接(如当前端的程序准备写入数据到 Socket,结果发起IO调用后程序异常退出),则抛出该异常 |
2.2 syscall:read(…) failed: Connection reset by peer 错误
继续搜索其他关键字,然后兜兜转转找到了 github
上 reactor-netty 的 issue。github
上其他开发者贴出的报错内容与笔者遇到的几乎完全一致,仔细阅读下来,发现其他开发者遇到这个问题主要是以下两种解决方式:
- 禁用长连接
- 修改负载均衡策略为最小连接数策略
从 comment 来看,这主要是涉及到了 reactor-netty
的连接池机制。我们知道 netty
是基于 nio
的框架,它在处理连接请求的时候使用了一个连接池来保证并发吞吐。通过定制 ClientHttpConnector
的长连接属性为 false
,保证了连接池线程不被长时间占用,这种方法在其他开发者使用的场景中似乎能有效解决这个错误
3. 最终原因
查看 github
上的 comment,总觉得其他开发者的场景与我们并不完全一致,但是一时也没有什么思路。leader 在内部群里喊了一声,到了晚上终于有同事从 log 中发现了端倪。因为服务中有打印 SQL 语句的插件,通过 log 发现有一条语句执行了整整 60s,而执行该语句的线程与之后报出错误的线程号一致,至此一切豁然开朗
-
reactor-netty
连接池分配了线程reactor-http-epoll-1
处理一个请求A,reactor-http-epoll-1
处理过程中因为慢 SQL 一直阻塞了 60s,在此期间同一个接口被高频率访问,连接池中的其他线程也被分配来处理同一类请求,然后也因为慢 SQL 阻塞住。在连接池中的线程都被阻塞住的时候,新的请求过来,连接池中已经没有线程可以对其进行处理,请求端因此一直被 hold,直到超时后主动关闭了 Socket。这之后服务端连接池线程终于处理完慢 SQL 请求,再来处理积压的请求,从连接中读取对端请求数据时却发现连接已经被关闭,就报出了Connection reset by peer
错误。本次排查得到的经验是,如果服务报出 Connection reset by peer 错误,首先检查是不是服务中有执行特别慢的动作阻塞了线程
分析慢 SQL 发现,那条语句之所以执行耗时如此之长,是因为 MySQL 数据库中数据类型为 VARCAHR 的字段接受了 Long 数据类型的条件,造成了隐式类型转换,无法使用索引,进而引发了全表扫描。