公司接了一個快遞行業的單子,爲了保證局方能在雙十一期間系統能順利運行,也爲了證實我們應用能夠抗下雙十一的峯值,於是配合局方做了一次應對雙十一流量高峯全鏈路壓測。模擬局方接口發起sip協議,通過華爲雲的接入,並獲取sip-calluuid轉換爲sip頭,經防火牆、負載均衡器最終負載到每臺機器,其中一臺服務器的負載併發量爲1200路併發,持續壓測了十幾分鍾後,明顯發現服務響應時間變慢,通過promotheus監控查看到調局方接口的響應延遲5+ minutes,但我們應用用的RestTemplate設置了讀超時時間和寫超時時間是5s,怎麼算也不會到5m。通過lsof -I命令查看發現大量TCP請求出現了Time_Wait
爲什麼出現大量的Time_wait,這個還得從TCP的四次揮手說起,當tcp連接發起斷開請求的時候並不是立馬斷開,而是主動關閉的一方先發起FIN請求,等被動方進入CLOSE_WAIT後,主動放將TCP狀態改爲TIME_WAIT,等待2MSL後才最終關閉TCP連接。那麼TCP爲什麼設計TIME_WAIT,有兩點好處:1,可靠地實現TCP全雙工連接地終止;2,運行老地重發分節在網絡中消逝。那麼長連接出現大量地TIME_WAIT是爲了保證雙工通信時可靠的,我們java的http請求都是短鏈接,我們知道http請求是無狀態的,這裏TIME_WAIT是維持2MSL,這個可以查閱《TCP/IP詳解》,
翻閱網絡上的資料,如果linux系統中出現大量的TIME_WAIT,修改linux系統的內核參數可解決此問題。步驟如下:
vi /etc/sysctl.conf,減小keepalive的連接時間,加大SYNC隊列的長度,容納更多的網絡連接數。
net.ipv4.tcp_keepalive_time = 1200
#表示當keepalive起用的時候,TCP發送keepalive消息的頻度。缺省是2小時,改爲20分鐘。
net.ipv4.ip_local_port_range = 1024 65000
#表示用於向外連接的端口範圍。缺省情況下很小:32768到61000,改爲1024到65000。
net.ipv4.tcp_max_syn_backlog = 8192
#表示SYN隊列的長度,默認爲1024,加大隊列長度爲8192,可以容納更多等待連接的網絡連接數。
net.ipv4.tcp_max_tw_buckets = 5000
#表示系統同時保持TIME_WAIT套接字的最大數量,如果超過這個數字,TIME_WAIT套接字將立刻被清除並打印警告信息。
默認爲180000,改爲5000。對於Apache、Nginx等服務器,上幾行的參數可以很好地減少TIME_WAIT套接字數量,但是對於 Squid,效果卻不大。此項參數可以控制TIME_WAIT套接字的最大數量,避免Squid服務器被大量的TIME_WAIT套接字拖死。
修改linux參數後,再次開始壓測,但情況並不像資料說的那麼好,過了半個小時後請求又出現延遲,雖然情況好了很多,但已經超過了我們期望的每個請求響應時長在5s內,繼續查閱資料資料,發現我們使用的RestTemplate內部還是HttpClient,如果沒有配置連接數和併發數,默認的值少得可憐,修改內核中HttpClient的連接數,繼續壓測一切順利,每次響應時間在2s內。
/**
* 發送http請求,響應超時時間設置相對較長
* 應用於推送數據等對結果無依賴
* @return
*/
@Bean
public RestTemplate restTemplate() {
// 長連接保持30秒
PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
// 總連接數
pollingConnectionManager.setMaxTotal(800);
// 同路由的併發數
pollingConnectionManager.setDefaultMaxPerRoute(800);
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setConnectionManager(pollingConnectionManager);
// 重試次數,默認是3次,沒有開啓
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true));
// 保持長連接配置,需要在頭添加Keep-Alive
httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
List<Header> headers = new ArrayList<>();
headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"));
headers.add(new BasicHeader("Accept-Encoding", "gzip,deflate"));
headers.add(new BasicHeader("Accept-Language", "zh-CN"));
// headers.add(new BasicHeader("Connection", "Keep-Alive"));
httpClientBuilder.setDefaultHeaders(headers);
HttpClient httpClient = httpClientBuilder.build();
// httpClient連接配置,底層是配置RequestConfig
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
// 連接超時
clientHttpRequestFactory.setConnectTimeout(config.getConnectionTimeOut() );
// 數據讀取超時時間,即SocketTimeout
clientHttpRequestFactory.setReadTimeout(config.getSoketTimeOut());
// 連接不夠用的等待時間,不宜過長,必須設置,比如連接不夠用時,時間過長將是災難性的
clientHttpRequestFactory.setConnectionRequestTimeout(200);
// 緩衝請求數據,默認值是true。通過POST或者PUT大量發送數據時,建議將此屬性更改爲false,以免耗盡內存。
// clientHttpRequestFactory.setBufferRequestBody(false);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(clientHttpRequestFactory);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
return restTemplate;
}