1. 程式人生 > 實用技巧 >springcloud gateway 採用 netty作為服務容器中的bug

springcloud gateway 採用 netty作為服務容器中的bug

一、背景

可能大家在使用Spring Cloud Gateway構建微服務閘道器的時候,過五關斬六將,Reactor沒能難倒我們,鏈路追蹤沒能難倒我們,最後在上線之後發現許多奇妙的問題,這些奇妙的問題還無從下手,比如這個堆疊,深入使用過SCG的人一定不會陌生:

reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
  • 1
  • 2

類似的還有:
Connection prematurely closed DURING response 。。。等等

百度了一圈,鮮有人提供解決方案,有條件的Google了一把,跟著官方調整幾個引數,有用沒用也不得而知,最後反正就不了了之。

二、如何找答案

去SCG官方Issue中查詢一番,還不少,(這裡插一句,遇到問題可以先找前人的Issue,儘量不要提一些重複的問題,美德永存!)

然後發現了一句:

看來問題的根因不是在SCG,按照以往的經驗來看,Spencer Gibb老鐵是個實誠人,知之為知之,不知為不知,深得儒家精髓。召喚了Reactor-Netty的@violetagg,由此,我們知道了這個問題要去

https://github.com/reactor/reactor-netty/issues找答案了。其實題主也一樣,在Reactor-Netty下面也沒怎麼搞明白這個錯誤的產生原理,正好全域性異常處理器可以捕獲到這個異常,給呼叫方返回一個請求第三方出錯的統一結果,也不痛不癢。But,我是不會讓這種不明不白的問題程式上線的!其實仔細閱讀Reactor-Netty專案的Issue會總結出一些關鍵點,除了@violetagg指導大家如何如何切debug模式,如何如何找channel id之外,有一些總結性的話,我貼在下面,大家細品:

第一段:
I would recommend to configure maxIdleTime on the client side having in mind the keepAliveTimeout on Tomcat. Without such configuration Reactor Netty can receive the close event at any time between acquiring the connection from the pool and before actual sending of the request. Also you might want to switch to LIFO leasing strategy so that you will use always the most recently used connection.

第二段:
The connection is closed by Spring Framework WebClient’s new change for disposing the connection when a cancellation happens

第三段:
the connection was closed while still sending the request body

其實問題最本質的原因就是一個正常的請求在一些情況下被突然關閉了,題主也和大家一樣腦袋中也出現了更多的問號:“一些情況”是嘛情況?為什麼會被關閉?這樣的問題出現頻率不高,如何有效復現?why???

在眾多的Issue中,你一定也會注意到,這個異常和Reactor-Netty內部的HttpClient有莫大的關係。

三、原因剖析

SCG官方文件有說,設定請求第三方服務的連線超時和讀取超時實際上是設定的org.springframework.cloud.gateway.config.HttpClientProperties類屬性,接著挖下去,HttpClientProperties其實就是提高配置能力,為初始化reactor.netty.http.client.HttpClient做門面,其實這個配置類和你知道的HttpClient沒啥直接關係,它只是模擬出了類似HttpClient該有的一些機制,譬如連線池(使用過HttpClient的老鐵在線上出么蛾子的時候一定也把玩過它的連線/執行緒池引數)機制,HttpClientProperties裡面的pool屬性就是設定連線池相關的屬性的。

看到這裡,你只需要知道,SCG的底層Reactor-Netty會為請求例項建立連線池,以便後面發起請求不用重新建立請求,直接從中獲取即可。其實這也就是問題的根因,看下面的時序圖你就明白了:

這裡使用一個Spring Boot內建Tomcat作為服務提供方,使用者通過SCG訪問,SCG代理請求。
預設情況下,SCG內部建立的連線是不會被回收的,一直存在於記憶體中,而Spring Boot內建的Tomcat不一樣,預設在20s之後沒有資料互動,便會回收掉這個連線,在回收的時候恰巧碰到又來了請求,剛好又在SCG拿到這個連線來嘗試請求Tomcat,就會出現這個異常。
所以,不要指望在Reactor-Netty或是SCG中解決這個問題,這需要閘道器和後端服務配合解決,最大限度不出現這個異常。

四、解決方式

從上文的第一段原話就有解決方案:

第1步、加入JVM引數:
-Dreactor.netty.pool.leasingStrategy=lifo

第2步、SCG新增配置:
spring:
  cloud:
    gateway:
      httpclient:
        pool:
          maxIdleTime: 10000(根據需要調整)

第1步將獲取連線策略由預設的FIFO變更為LIFO,因為LIFO能夠確保獲取的連線最大概率是最近剛被用過的,也就是熱點連線始終是熱點連線,而始終用不到的連線就可以被回收掉,LRU的思想。

第2步是設定空閒請求在空閒多久後會被回收,這樣也就可以避免拿到舊連線剛好在請求途中被強行close了,這個時間的設定只要確保比你後端服務的connectTimeout小就行了,這樣能夠確保SCG回收請求在後端服務回收請求之前,就可以避免掉這個問題。

這樣設定後還會偶發這個異常,請排查你的所有後端服務是否connectTimeout都比maxIdleTime大,或者嘗試調整maxIdleTime。另外,本身這是個概率性偶發問題,如果你的架構是題主舉的這個例子類似,題主這樣設定後,幾乎看不到這個異常出現了,徹底根除這個頑疾,請看懂時序圖再提問題。另外,如果你的架構不太一樣,你需要找到你的請求為什麼在請求途中被突然關閉的原因,這可能不是Reactor-Netty的問題,而是你的服務的問題。

版本說明:
題主之前使用的SCG版本是Greenwich.SR2版本,對應的Spring Boot版本是2.1.6.RELEASE,這個版本對應的Reactor-Netty版本是v0.8.9.RELEASE,這個版本的Reactor-Netty是沒有提供設定maxIdleTime這個選項的。

Reactor-Netty是在v0.9.5.RELEASE版本開始提供設定

所以以上的配置請下面的版本當中使用:
Spring Cloud:Hoxton.SR1及以上(SCG 2.2.1.RELEASE及以上)
Reactor-Netty:v0.9.5.RELEASE及以上
Spring Boot:2.2.2.RELEASE及以上

注意:v0.9.6.RELEASE版本的maxIdleTime有個bug,可能不生效,需要升級到v0.9.7.RELEASE版本以上

單純使用Reactor-Netty的同學也可以在reactor.netty.resources.ConnectionProvider找到配置方式。

另外,v0.9.10.RELEASE版本做了連線提前關閉的重試機制,讓出現這個異常的機率變得微乎其微