徹底解決SpringCloud TCP連線過多未釋放問題!

參與專案中使用springcloud gateway,並結合nacos,做網關實現路由轉送以及負載平衡的功能。專案運行一段時間後,出現springcloud gateway服務的TCP連接過多未釋放的問題,針對此問題,查閱相關資料,匯總TCP連接、Http Keep-Alive和Tcp Keepalive的基礎知識,並結合操作系統windows和linux對keepalive參數的設定問題,最後落到部分的應用程式服務到部分的應用程式為基礎, gateway設定keepalive參數並進行了驗證有效,其他有關作業系統以及Nginx與Tomcat的設定問題,尚需後續實踐過程中進行驗證。於此進行記錄,以便後續繼續研究與驗證,也為後來者提供參考借鑒,文中不免疏漏之處,望讀者予以指正,不勝感激!

1.TCP連接介紹

為實現資料的可靠傳輸,TCP要在應用程式過程間建立傳輸連線。它是在兩個傳輸用戶之間建立一種邏輯聯繫,使得通訊雙方都確認對方為自己的傳輸連接端點。

1.1 建立連線—三次握手

建立連接前,伺服器端首先被動打開其熟知的端口,對端口進行偵聽。當客戶端要和伺服器端建立連線時,發起一個主動開啟連接埠的請求(該連接埠一般為臨時連接埠);然後進入三次握手的過程。

圖片圖片

第一次握手:建立連線時,客戶端發送SYN包(seq=x)到伺服器,並進入SYN_SEND 狀態,等待伺服器確認;

第二次握手:伺服器收到SYN包,必須確認客戶的SYN(ack=x+1),同時自己也發送一個SYN 包(seq=y),即SYN+ACK 包,此時伺服器進入SYN_RECV 狀態;

第三次握手:客戶端收到伺服器的SYN+ACK 包,向伺服器發送確認包ACK(ack=y+1),此包發送完畢,客戶端和伺服器進入ESTABLISHED 狀態,完成三次握手。

1.2 釋放連結—四次揮手

圖片圖片

  • 第一次揮手:Client發送一個FIN,用來關閉Client到Server的資料傳送,Client進入FIN_WAIT_1狀態。
  • 第二次揮手:Server收到FIN後,發送一個ACK給Client,確認序號為收到序號+1(與SYN相同,一個FIN佔用一個序號),Server進入CLOSE_WAIT狀態。
  • 第三次揮手:Server發送一個FIN,用來關閉Server到Client的資料傳送,Server進入LAST_ACK狀態。
  • 第四次揮手:Client收到FIN後,Client進入TIME_WAIT狀態,接著發送一個ACK給Server,確認序號為收到序號+1, Server進入CLOSED狀態,完成四次揮手。

此時TCP連線還沒釋放掉,必須經過時間等待計時器設定的時間2MSL後,Client才進入到連線關閉狀態。

說明:

2MSL即兩倍的MSL,TCP的TIME_WAIT狀態也稱為2MSL等待狀態,當TCP的一端發起主動關閉,在發出最後一個ACK包後,即第3次握手完成後發送了第四次握手的ACK包後就進入了TIME_WA IT狀態,必須在此狀態上停留兩倍的MSL時間,等待2MSL時間主要目的是害怕最後一個ACK包對方沒收到,那麼對方在超時後將重發第三次握手的FIN包,主動關閉端接到重發的FIN包後可以再發一個ACK應答包。在TIME_WAIT狀態時兩端的連接埠不能使用,要等到2MSL時間結束才可以繼續使用。當連線處於2MSL等待階段時任何遲到的封包都將被丟棄。不過在實際應用中可以透過設定SO_REUSEADDR選項達到不必等待2MSL時間結束再使用此連接埠。

如果有大量的連接,每次在連接、關閉時都要三次握手,四次揮手,會很明顯會造成性能低下。

2. KeepAlive與Keep-Alive介紹

TCP的KeepAlive和HTTP的Keep-Alive是完全不同的概念,不能混為一談。其實HTTP的KeepAlive寫法是Keep-Alive,跟TCP的KeepAlive寫法上也有不同。

2.1 Http Keep-Alive

Http協定採用「請求-應答」模式,當使用普通模式,即非Keep-Alive模式時,每個請求/應答,客戶端和伺服器都要新建一個連接,完成之後立即斷開連接;當使用Keep-Alive模式時,Keep-Alive功能使客戶端到伺服器端的持續有效,當伺服器建立連接的後繼請求時,KeepAlive

http1.0中預設是關閉的,需要在http頭加入”Connection: Keep-Alive”,才能啟用Keep-Alive;http 1.1中預設啟用Keep-Alive,如果加入」Connection: close “才關閉。目前大部分瀏覽器都是用http1.1協議,也就是說預設都會發起Keep-Alive的連線請求了,所以是否能完成一個完整的Keep- Alive連線就看伺服器設定。

開啟Keep-Alive的優缺點:

• 優點:Keep-Alive模式更有效率,因為避免了連線建立和釋放的開銷。

• 缺點:長時間的Tcp連線容易導致系統資源無效佔用,浪費系統資源。

2.2 Tcp KeepAlive

連線建立之後,如果客戶端一直不發送數據,或者隔很長時間才發送一次數據,當連接很久沒有數據報文傳輸時如何去確定對方還在線,到底是掉線了還是確實沒有數據傳輸,連接還需不需要保持,這種情況在TCP協議設計中是需要考慮到的。 TCP協定透過一種巧妙的方式去解決這個問題,當超過一段時間之後,TCP自動發送一個數據為空的報文(偵測包)給對方,如果對方回應了這個報文,說明對方還在線,連接可以繼續保持,如果對方沒有報文返回,並且重試了多次之後則認為鏈接丟失,沒有必要保持連接丟失。

tcp KeepAlive是TCP的一種偵測TCP連接狀況的保鮮機制。 tcp KeepAlive保鮮定時器,支援三個系統核心設定參數:

net.ipv4.tcp_keepalive_intvl = 15  
net.ipv4.tcp_keepalive_probes = 5  
net.ipv4.tcp_keepalive_time = 1800
  • 1.
  • 2.
  • 3.

KeepAlive是TCP保鮮定時器,當網路兩端建立了TCP連線之後,閒置(雙方沒有任何資料流發送到來)了tcp_keepalive_time後,伺服器就會嘗試向客戶端發送偵測包,來判斷TCP連線狀況(有可能客戶端崩潰、強制關閉了應用程式、主機不可達等等)。如果沒有收到對方的回答(ack包),則會在tcp_keepalive_intvl後再次嘗試發送偵測包,直到收到對方的ack,如果一直沒有收到對方的ack,一共會嘗試tcp_keepalive_probes次,每次的間隔時間在這裡分別是15s, 30s, 45s, 60s, 75s。如果嘗試tcp_keepalive_probes,依然沒有收到對方的ack包,則會丟棄該TCP連線。 TCP連線預設閒置時間是2小時,一般設定為30分鐘就足夠了。

3.作業系統有關Keepalive參數設定

3.1 Linux系統

  • tcp_keepalive_time 7200// 距離上次傳送資料多少時間未收到新封包判斷為開始偵測,單位秒,預設7200s(沒必要頻繁,浪費資源)。
  • tcp_keepalive_intvl 75// 偵測開始每多少時間傳送心跳包,單位秒,預設75s。
  • tcp_keepalive_probes 9// 發送幾次心跳包對方未回應則close連接,預設9次。可透過對下列對應的設定檔進行修改參數:/proc/sys/net/ipv4/tcp_keepalive_time/proc/sys/net/ipv4/tcp_keepalive_intvl/proc/sys/net/ipv4/tcp_keepalive_probes

3.2 Windows系統

  • KeepAliveTimeKeepAliveTime的值控制系統嘗試驗證空閒連線是否仍然完好的頻率。如果該連線在一段時間內沒有活動,那麼系統會發送保持連線的訊號,如果網路正常且接收方是活動的,它就會回應。如果需要對遺失接收方的情況敏感,也就是說需要更快發現是否遺失了接收方,請考慮減少該值。而如果長期不活動的空閒連線的出現次數較多,但遺失接收方的情況出現較少,那麼可能需要增加該值以減少開銷。預設情況下,如果空閒連線在7200000毫秒(2小時)內沒有活動,系統就會發送保持連線的訊息。 通常建議將該值設為1800000毫秒,從而丟失的連接會在30分鐘內被偵測到。具體操作:瀏覽至HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters登錄子鍵,在Parameters子鍵下建立或修改名為KeepAliveTime的REG_DWORD值,為此值設定適當的毫秒數。
  • KeepAliveIntervalKeepAliveInterval的值表示未收到另一方對「保持連線」訊號的回應時,系統重複發送「保持連線」訊號的頻率。在無任何回應的情況下,連續發送「保持連線」訊號的次數超過TcpMaxDataRetransmissions(下文將介紹)的值時,將放棄該連線。如果網路環境較差,允許較長的回應時間,則考慮增加該值以減少開銷;如果需要盡快驗證是否已遺失接收方,則考慮減小該值或TcpMaxDataRetransmissions值。缺省情況下,在未收到回應而重新傳送「保持連線」的訊號之前,系統會等待1000毫秒(1秒),可以根據特定需求修改,具體操作:瀏覽至HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\ParametersGREWREFid鍵,在ParametersInternetid值下建立名稱為適當的數量。

4.常用服務端配置Keepalive參數

4.1 Nginx設定Keepalive

當使用nginx作為反向代理時,為了支援長連接,需要做到兩點:

  • 從client到nginx的連接是長連接
  • 從nginx到server的連線是長連接

4.1.1 從Client 到Nginx 的連接是長連接

http {
    # 客户端连接的超时时间, 为 0 时禁用长连接。 tcp连接在传送完最后一个响应后,还需要hold住 keepalive_timeout秒后仍没有新的http请求,才开始关闭这个连接
    keepalive_timeout 120s;
    # 在一个长连接上可以服务的最大请求数目, 当达到最大请求数目且所有已有请求结束后, 连接被关闭, 默认为 100, 即每个连接的最大请求数
    keepalive_request 10000;
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

keepalive_requests:參數的真實意義,是指一個keepalive建立之後,nginx就會為這個連線設定一個計數器,記錄這個keep alive的長連線上已經接收並處理的客戶端請求的數量。如果達到這個參數設定的最大值時,則nginx會強行關閉這個長連接,逼迫客戶端不得不重新建立新的長連接。大多數情況下當QPS(每秒請求數)不是很高時,預設值100湊合夠用。但是,對於某些QPS比較高(例如超過10000QPS,甚至達到30000,50000甚至更高) 的場景,預設的100就顯得太低。簡單計算一下,QPS=10000時,客戶端每秒發送10000個請求(通常建立有多個長連線),每個連線只能最多跑100次請求,意味著平均每秒鐘就會有100個長連線因此被nginx關閉。同樣意味著為了維持QPS,客戶端不得不每秒重新新建100個連線。因此,就會發現有大量的TIME_WAIT的socket連接(即使此時keep alive已經在client和nginx之間生效)。因此對於QPS較高的場景,非常有必要加大這個參數,以避免出現大量連接被產生再拋棄的情況,減少TIME_WAIT。

4.1.2 從Nginx 到Server(upstream) 的長連接

為了讓nginx和後端server(nginx稱為upstream)之間保持長連接,典型設定如下:(預設nginx訪問後端都是用的短連接(HTTP1.0),一個請求來了,Nginx 新開一個端口和後端建立連接,後端執行完畢後主動關閉該鏈接)

http {
    upstream  BACKEND {
        server   192.168.0.1:8080  weight=1 max_fails=2 fail_timeout=30s;
        server   192.168.0.2:8080  weight=1 max_fails=2 fail_timeout=30s;
        keepalive 300;        // 这个很重要!
    }
server {
        listen 8080 default_server;
        server_name "";
        location /  {
            proxy_pass http://BACKEND;
            proxy_http_version 1.1;         // 这两个最好也设置
            proxy_set_header Connection "";
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

這裡keepalive的意思不是開啟、關閉長連接的開關;也不是用來設定超時的timeout;更不是設定長連線池最大連線數。官方解釋:The connections parameter sets the maximum number of idle keepalive connections to upstream servers connections(設定到upstream伺服器的空閒keepalive連線的最大數量) When this number is exceeded, the least recently 連線的最大數量) When this number is exareceeded, the least recently 連接的最大數量) When this number is exareceeded, the least recently ends connections. should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open.(特別提醒:keepalive指令不會限制一個nginx worker進程到行程到伺服器連接的總數)

keepalive: 這個參數是nginx 連接後端的連接池中的最大空閒連接數, 例如設定為300; 如果nginx 為了滿足請求的qps; 創建了1000 個連接的連接池, 這時候只有500 個請求過來, 那麼1000- 500 = 500x 那麼連接 500 個空閒。就會根據這個配置; 斷開200 個請求連接; 那麼這個時候就只有800 個連接的連接池, 如果下次過來了1000 個請求, 那麼nginx 又會開始創建連接; 所有這個數值的配置要小心配置

4.1.3 Nginx出現大量TIME_WAIT的情況

1)導致nginx端出現大量TIME_WAIT的情況有兩種:keepalive_requests設定比較小,高並發下超過此值後nginx會強制關閉和客戶端保持的keepalive長連接;(主動關閉連接後導致n ginx出現TIME_WAIT)keepalive設定的比較小(空閒數太小),導致高並發下nginx會頻繁出現連線數震盪(超過該值會關閉連線),不停的關閉、開啟和後端server保持的keepalive長連線;

2)導致後端server端出現大量TIME_WAIT的情況:nginx沒有開啟和後端的長連接,即:沒有設定proxy_http_version 1.1;和proxy_set_header Connection “”;從而導致後端server每次關閉連接,高並發下就會出現server端出現大量TIME_WAIT

4.2 Tomcat設定Keepalive

瀏覽器在請求的頭部添加Connection:Keep-Alive,以此告訴伺服器 “我支持長連接,你支持的話就和我建立長連接吧”,而倘若伺服器的確支持長連接,那麼就在響應頭部添加“Connection:Keep-Alive”,從而告訴瀏覽器“我的確支持,那我們建立長連接吧”。伺服器也可以透過Keep-Alive:timeout=10, max=100 的頭部告訴瀏覽器「我希望10 秒算超時時間,最長不能超過100 秒」。

在Tomcat 裡是允許配置長連接的,配置conf/server.xml 文件,配置Connector 節點,該節點負責控制瀏覽器與Tomcat 的連接,其中與長連接直接相關的有兩個屬性,它們分別是:keepAliveTimeout,它表示在Connector 關閉連接前,Connector 為另外一個請求Keep Alive 所等待的微妙數字,Connector 和預設值一樣;另一個是maxKeepAliveRequests,它表示HTTP/1.0 Keep Alive 和HTTP/1.1 Keep Alive / Pipeline 的最大請求數目,如果設定為1,將會禁用掉Keep Alive 和Pipeline,如果設定為小於0 的數,Keep Alive 的最大請求數將沒有限制。也就是說在Tomcat 裡,預設長連線是開啟的,當我們想要關閉長連線時,只要將maxKeepAliveRequests 設定為1 就可以。

Tomcat在server.xml 中的Connector 元素中:

  • • keepAliveTimeout:單位是milliseconds,表示在下次要求過來之前,tomcat保持該連線多久。這就是說假如客戶端不斷有請求過來,且未超過過期時間,則該連線將一直保持。
  • • maxKeepAliveRequests:最大長連線個數(1表示停用,-1表示不限制個數,預設100個。一般設定在100~200之間),表示該連線最大支援的請求數。超過該請求數的連線也會關閉(此時就會傳回一個Connection: close頭給客戶端)。

4.3 Netty設定Keepalive

4.3.1 SO_KEEPALIVE

Socket參數。是否啟用心跳保活機制,即連結保活。 啟用此功能時,TCP會主動探測空閒連線的有效性。

在雙方TCP套接字建立連線後(即都進入ESTABLISHED狀態)並且在兩個小時左右(預設的心跳間隔是7200s即2小時)上層沒有任何資料傳輸的情況下,這套機制才會被啟動。

預設值:Netty預設會關閉此功能,即值為:false 。

代碼設定:

bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
  • 1.

說明:

  • • 如果一方已經關閉或異常終止連接,而另一方卻不知道,我們將這樣的TCP連接稱為半打開的。 TCP透過保活定時器(KeepAlive)來偵測半開啟連線。
  • • 在高併發的網路伺服器中,常常會出現漏掉socket的情況,對應的結果有一種情況就是出現大量的CLOSE_WAIT狀態的連線。這個時候,可以透過設定KEEPALIVE選項來解決這個問題。
  • • 設定SO_KEEPALIVE選項來開啟KEEPALIVE,然後透過TCP_KEEPIDLE、TCP_KEEPINTVL和TCP_KEEPCNT設定keepalive的開始時間、間隔、次數等參數。
  • • 當然,也可以透過設定/proc/sys/net/ipv4/tcp_keepalive_time、tcp_keepalive_intvl和tcp_keepalive_probes等核心參數來達到目的,但是這樣的話,會影響所有的socket。

4.3.2 SpringCloud Gateway設定Keepalive

@Configuration
public class NettyConfig {
    @Bean
    public NettyServerCustomizer nettyServerCustomizer() {
        return httpServer -> httpServer.tcpConfiguration(tcpServer -> {
            tcpServer= tcpServer.option(ChannelOption.SO_KEEPALIVE, true);
            tcpServer = tcpServer.doOnBind(serverBootstrap ->
                    BootstrapHandlers.updateConfiguration(serverBootstrap, "channelIdle", (connectionObserver, channel) -> {
                        ChannelPipeline pipeline = channel.pipeline();
                        pipeline.addLast(new ReadTimeoutHandler(5, TimeUnit.MINUTES));
                    }));
            return tcpServer;
        });
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

5.參考資料

[1] https://www.cnblogs.com/xpfeia/p/10885726.html

[2] https://zhuanlan.zhihu.com/p/51560184

[3] https://blog.csdn.net/tennysonsky/article/details/45622395

[4] http://www.manongjc.com/detail/24-rcqeqovotuetucc.html

[5] https://www.jianshu.com/p/394a7883a139

[6] https://blog.csdn.net/bluetjs/article/details/80966148

[7] http://www.ttlsa.com/windows/parameter-optimization-of-tcp-under-windows-system/

[8] https://www.cnblogs.com/sunsky303/p/10648861.html