排查 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 錯誤

分析慢 SQL 發現,那條語句之所以執行耗時如此之長,是因爲 MySQL 數據庫中數據類型爲 VARCAHR 的字段接受了 Long 數據類型的條件,造成了隱式類型轉換,無法使用索引,進而引發了全表掃描。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章