原文:
https://blog.csdn.net/haicunzaima/article/details/84529735
【問題現象】
系統上線后出現TCP連接數超過預期閥值,最高值達到8K左右,新上線代碼中包含了一文件上傳操作,使用的是apache的commons-httpclient包。
【問題分析】
1、先確認是否存在連接未關閉問題引起的。
觀察發現,TCP連接數不是一直在增長,而是會有所下降。並且當業務低峰期TCP連接數TCP連接數會降到100左右,這說明TCP連接還是會關閉。
2、確定居高不下的TCP使用情況
使用"netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'"命令發現,處於ESTABLISHED狀態的連接數最多,在查看了一下處於ESTABLISHED狀態的目的IP,基本上都是文件服務器的IP,這說明還是跟新增加的文件上傳操作有關。但是按照代碼的邏輯來看,文件上傳操作是多線程處理的,一個線程處理一個上傳操作,線程池中一共有10個線程,照此分析正常的話應該有10個左右與文件服務器的鏈接,不應該出現幾千個鏈接。因此懷疑是連接沒有主動釋放,而是等待連接超時才開始釋放。
3、為什么會連接超時
查看了文件上傳部分代碼,主要代碼如下:
HttpClient client = new HttpClient();
MultipartPostMethod method = new MultipartPostMethod(config .getUploadInterface());
try{
client.executeMethod(method);
}catch (Exception e){
throw e;
}finally{
method.releaseConnection();
}
從代碼里看是已經釋放連接了,但是從結果上看沒有釋放連接,那就產生一個問題,這個地方真的能釋放連接嗎?我們在釋放連接后面增加一行測試代碼來看看:
HttpConnection conn = client.getHttpConnectionManager().getConnection(client.getHostConfiguration());
System.out.println(conn.isOpen());
打印出的結果是true,也就是說雖然調用了releaseConnection,但是並沒有釋放連接!!
4、分析commons-httpclient相關代碼
現在懷疑是我們使用的方式不對了,繼續分析一下commons-https包中相關代碼,首先看一下method.releaseConnection()的代碼實現:
public void releaseConnection() {
try {
if (this.responseStream != null) {
try {
// FYI - this may indirectly invoke responseBodyConsumed.
this.responseStream.close();
} catch (IOException ignore) {
}
}
} finally {
ensureConnectionRelease();
}
}
private void ensureConnectionRelease() {
if (responseConnection != null) {
responseConnection.releaseConnection();
responseConnection = null;
}
}
經過debug發現responseStream為null,並且responseConnection也為null,這樣改調用就沒有實際意義。那么我們應該怎么來釋放連接呢?
5、繼續分析代碼
我們發現在org.apache.commons.httpclient.HttpMethodDirector類的第208行已經在finally中釋放連接了:
finally {
if (this.conn != null) {
this.conn.setLocked(false);
}
// If the response has been fully processed, return the connection
// to the pool. Use this flag, rather than other tests (like
// responseStream == null), as subclasses, might reset the stream,
// for example, reading the entire response into a file and then
// setting the file as the stream.
if (
(releaseConnection || method.getResponseBodyAsStream() == null)
&& this.conn != null
) {
this.conn.releaseConnection();
}
}
public void releaseConnection(HttpConnection conn) {
if (conn != httpConnection) {
throw new IllegalStateException("Unexpected release of an unknown connection.");
}
finishLastResponse(httpConnection);
inUse = false;
// track the time the connection was made idle
idleStartTime = System.currentTimeMillis();
}
這個地方我們可以看到了所謂的釋放連接並不是真的釋放,還是return the connection to pool,照此分析,我們每個線程中new了一個HttpClient類,而每個HttpClient類中的鏈接都是沒有close的,只是歸還到httpClient中的pool而已,這些連接也必須等到連接超時才會被釋放,由此可以分析出來連接數上漲的原因。那么我們應該怎么使用呢?按照代碼的設計,看起來httpclient應該是單例的,但是在httpClient類的javadoc中並沒有關於線程安全方面的說明,為此我們再回到官網上看相關文檔,在文檔(http://hc.apache.org/httpclient-3.x/performance.html)上我們看到如下的說明:
HttpClient is fully thread-safe when used with a thread-safe connection manager such as MultiThreadedHttpConnectionManager
這說明在多線程環境下應該使用一個全局單例的HttpClient,並且使用MultiThreadHttpConnectionManager來管理Connection。
【相關結論】
1、HttpClient內部使用了池化技術,內部的鏈接是為了復用。在多線程條件下,可以使用一個全局的HttpClient實例,並且使用MultiThreadHttpConnectionManager來管理Connection。
2、使用開源軟件之前一定要讀讀相關代碼,看看官方推薦使用方式。
3、在解決此問題后,讀了讀httpclient中其他包中的代碼,在讀的時候發現對於理解http協議幫助很大,特別是文件上傳,長連接,auth鑒權等。
https://q.cnblogs.com/q/90727/
你想用多個HttpClient又不想讓多個TCP。這個是悖論,不可能的。
官方推薦是一個域使用一個HttpClient。
按你所說,你的header一直變動是不應該的,資源條件應該放在url里面,數據應該放在content里面,你header來回變動是做什么用的呢?即使變動也無非是header變動。
你在找一種能讓你的錯誤的使用方法變得不出錯的辦法……
你的意思是Reponse回復的時候帶Header么?發送的時候,Header你盡管改,沒有問題的。假設這樣一個場景,你Header目前是h1,你r1在請求的時候用的是h1,此時,r2要請求,計划也用h1,但是,同時你r3要用h2請求,導致你的r2用的不是h1而是和r3一樣都用的是h2。你的意思是擔心這樣的問題么?
你同一個域下,一個請求應該是可以一直或者在將來的持續一段時間內都生效可重復的,否則冪等性就沒有了。票據正常也是放在Auth里面的,也是放在頭中,所以,你的操作沒有問題。只是,你的票據不應該每次都變的吧,如果每次都變,你並發都做沒辦法並發。
如果過期——說明,不變是常態,但是在一定情況下會變,這種情況,你直接改就可以了呀。
- 事件背景
- 問題過程
- 案情回顧
- 深入排查
- 案情總結
事件背景
我在鳳巢團隊獨立搭建和運維的一個高流量的推廣實況系統,是通過HttpClient 調用大搜的實況服務。最近經常出現Address already in use (Bind failed)的問題。很明顯是一個端口綁定沖突的問題,於是大概排查了一下當前系統的網絡連接情況和端口使用情況,發現是有大量time_wait的連接一直占用着端口沒釋放,導致端口被占滿(最高的時候6w+個),因此HttpClient建立連接的時候會出現申請端口沖突的情況。
具體情況如下:
於是為了解決time_wait的問題,網上搜索了些許資料加上自己的思考,於是認為可以通過連接池來保存tcp連接,減少HttpClient在並發情況下隨機打開的端口數量,復用原來有效的連接。但是新的問題也由連接池的設置引入了。
問題過程
在估算連接池最大連接數的時候,參考了業務高峰期時的請求量為1分鍾1.2w pv,接口平響為1.3s(復雜的廣告推廣效果模擬系統,在這種場景平響高是業務所需的原因),因此qps為12000*1.360=260
然后通過觀察了業務日志,每次連接建立耗時1.1s左右, 再留70%+的上浮空間(怕連接數設置小出系統故障),最大連接數估計為2601.11.7約等於500
為了減少對之前業務代碼最小的改動,保證優化的快速上線驗證,仍然使用的是HttpClient3.1 的MultiThreadedHttpConnectionManager,設置核心代碼如下:
public void init() { connectionManager = new MultiThreadedHttpConnectionManager(); HttpConnectionManagerParams managerParams = new HttpConnectionManagerParams(); managerParams.setMaxTotalConnections(500); // 最大連接數 connectionManager.setParams(managerParams); client = new HttpClient(connectionManager);}
然后在線下手寫了多線程的測試用例,測試了下並發度確實能比沒用線程池的時候更高,然后先在我們的南京機房小流量上線驗證效果,效果也符合預期之后,就開始整個北京機房的轉全。結果轉全之后就出現了意料之外的系統異常。。。
情回顧
在當天晚上流量轉全之后,一起情況符合預期,但是到了第二天早上就看到用戶群和相關的運維群里有一些人在反饋實況頁面打不開了。這個時候我在路上,讓值班人幫忙先看了下大概的情況,定位到了耗時最高的部分正是通過連接池調用后端服務的部分,於是可以把這個突發問題的排查思路大致定在圍繞線程池的故障來考慮了。
於是等我到了公司,首先觀察了一下應用整體的情況:
- 監控平台的業務流量表現正常,但是部分機器的網卡流量略有突增
- 接口的平響出現了明顯的上升
- 業務日志無明顯的異常,不是底層服務超時的原因,因此平響的原因肯定不是業務本身
- 發現30個機器實例竟然有9個出現了掛死的現象,其中6個北京實例,3個南京實例
深入排查
由於發現了有近 1/3的實例進程崩潰,而業務流量沒變,由於RPC服務對provider的流量進行負載均衡,所以引發單台機器的流量升高,這樣會導致后面的存活實例更容易出現崩潰問題,於是高優看了進程掛死的原因。由於很可能是修改了HttpClient連接方式為連接池引發的問題,最容易引起變化的肯定是線程和CPU狀態,於是立即排查了線程數和CPU的狀態是否正常。
CPU狀態
如圖可見Java進程占用cpu非常高,是平時的近10倍。
線程數監控狀態
圖中可以看到多個機器大概在10點初時,出現了線程數大量飆升,甚至超出了虛擬化平台對容器的2000線程數限制(平台為了避免機器上的部分容器線程數過高,導致機器整體夯死而設置的熔斷保護),因此實例是被虛擬化平台kill了。之前為什么之前在南京機房小流量上線的時候沒出現線程數超限的問題,應該和南京機房流量較少,只有北京機房流量的1/3有關。
回滾之前tcp連接情況
回滾之后tcp連接情況
發現連接線程的並發度果然小很多了,這個時候要再確認一下是否是連接池設置導致的原因,於是將沒回滾的機器進行jstack了,對Java進程中分配的子線程進行了分析,終於可以確認問題。
jstack狀態
從jstack的日志中可以很容易分析出來,有大量的線程在等待獲取連接池里的連接而進行排隊,因此導致了線程堆積,因此平響上升。由於線程堆積越多,系統資源占用越厲害,接口平響也會因此升高,更加劇了線程的堆積,因此很容易出現惡性循環而導致線程數超限。
那么為什么會出現並發度設置過小呢?之前已經留了70%的上浮空間來估算並發度,這里面必定有蹊蹺!
於是我對源碼進行了解讀分析,發現了端倪:
如MultiThreadedHttpConnectionManager源碼可見,連接池在分配連接時調用的doGetConnection方法時,對能否獲得連接,不僅會對我設置的參數maxTotalConnections進行是否超限校驗,還會對maxHostConnections進行是否超限的校驗。
於是我立刻網上搜索了下maxHostConnections的含義:每個host路由的默認最大連接,需要通過setDefaultMaxConnectionsPerHost來設置,否則默認值是2。
所以並不是我對業務的最大連接數計算失誤,而是因為不知道要設置DefaultMaxConnectionsPerHost而導致每個請求的Host並發連接數只有2,限制了線程獲取連接的並發度(所以難怪剛才觀察tcp並發度的時候發現只有2個連接建立 )
案情總結
到此這次雪崩事件的根本問題已徹底定位,讓我們再次精煉的總結一下這個案件的全過程:
- 連接池設置錯參數,導致最大連接數為2
- 大量請求線程需要等待連接池釋放連接,出現排隊堆積
- 夯住的線程變多,接口平響升高,占用了更多的系統資源,會加劇接口的耗時增加和線程堆積
- 最后直至線程超限,實例被虛擬化平台kill
- 部分實例掛死,導致流量轉移到其他存活實例。其他實例流量壓力變大,容易引發雪崩。
關於優化方案與如何避免此類問題再次發生,我想到的方案有3個:
- 在做技術升級前,要仔細熟讀相關的官方技術文檔,最好不要遺漏任何細節
- 可以在網上找其他可靠的開源項目,看看別人的優秀的項目是怎么使用的。比如github上就可以搜索技術關鍵字,找到同樣使用了這個技術的開源項目。要注意挑選質量高的項目進行參考
- 先在線下壓測,用控制變量法對比各類設置的不同情況,這樣把所有問題在線下提前暴露了,再上線心里就有底了
以下是我設計的一個壓測方案:
- 測試不用連接池和使用連接池時,分析整體能承受的qps峰值和線程數變化
- 對比setDefaultMaxConnectionsPerHost設置和不設置時,分析整體能承受的qps峰值和線程數變化
- 對比調整setMaxTotalConnections,setDefaultMaxConnectionsPerHost 的閾值,分析整體能承受的qps峰值和線程數變化
- 重點關注壓測時實例的線程數,cpu利用率,tcp連接數,端口使用情況,內存使用率
綜上所述,一次連接池參數導致的雪崩問題已經從分析到定位已全部解決。在技術改造時我們應該要謹慎對待升級的技術點。在出現問題后,要重點分析問題的特征和規律,找到共性去揪出根本原因。
最近修改同事代碼時遇到一個問題,通過 httpclient 默認配置產生的 httpclient 如果不關閉,會導致連接無法釋放,很快打滿服務器連接(內嵌 Jetty 配置了 25 連接上限),主動關閉問題解決;后來優化為通過連接池生成 httpclient 后,如果關閉 httpclient 又會導致連接池關閉,后面新的 httpclient 也無法再請求,這里總結遇到的一些問題和疑問。
官網示例中的以下三個 close 分別釋放了什么資源,是否可以省略,以及在什么時機調用,使用連接池時有區別么?
作為 RPC 通信客戶端,如何復用 TCP 連接?
一、資源釋放
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
try {
// do something useful
} finally {
instream.close();
}
}
} finally {
response.close();
}
// httpclient.close();
首先需要了解默認配置 createDefault 和使用了 custom 連接池(文章最后的 HttpClientUtil)兩種情況的區別,通過源碼可以看到前者也創建了連接池,最大連接20個,單個 host最大2個,但是區別在於每次創建的 httpclient 都自己維護了自己的連接池,而 custom 連接池時所有 httpclient 共用同一個連接池,這是在 api 使用方面需要注意的地方,要避免每次請求新建連接池、關閉連接池,造成性能問題。
The difference between closing the content stream and closing the response is that the former will attempt to keep the underlying connection alive by consuming the entity content while the latter immediately shuts down and discards the connection.
第一個 close 是讀取 http 正文的數據流,類似的還有響應寫入流,都需要主動關閉,如果是使用 EntityUtils.toString(response.getEntity(), "UTF-8"); 的方式,其內部會進行關閉。如果還有要讀/寫的數據、或不主動關閉,相當於 http 請求事務未處理完成,這時通過其他方式關閉(第二個 close)相當於異常終止,會導致該連接無法被復用,對比下面兩段日志。
第一個 close 未調用時,第二個 close 調用,連接無法被復用,kept alive 0。
o.a.http.impl.execchain.MainClientExec : Connection can be kept alive indefinitely
h.i.c.DefaultManagedHttpClientConnection : http-outgoing-0: Close connection
o.a.http.impl.execchain.MainClientExec : Connection discarded
h.i.c.PoolingHttpClientConnectionManager : Connection released: [id: 0][route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 0 of 2; total allocated: 0 of 20]
第一個 close 正常調用時,第二個 close 調用,連接可以被復用,kept alive 1。
o.a.http.impl.execchain.MainClientExec : Connection can be kept alive indefinitely
h.i.c.PoolingHttpClientConnectionManager : Connection [id: 0][route: {}->http://127.0.0.1:8080] can be kept alive indefinitely
h.i.c.DefaultManagedHttpClientConnection : http-outgoing-0: set socket timeout to 0
h.i.c.PoolingHttpClientConnectionManager : Connection released: [id: 0][route: {}->http://127.0.0.1:8080][total kept alive: 1; route allocated: 1 of 2; total allocated: 1 of 20]
第二個 close 是強行制止和釋放連接到連接池,相當於對第一個 close 的保底操作(上面關閉了這個似乎沒必要了?),結合上面引用的官方文檔寫到 immediately shuts down and discards the connection,這里如果判斷需要 keep alive 實際也不會關閉 TCP 連接,因為通過 netstat 可以看到,第二段日志后在終端可以繼續觀察到連接:
# netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51003 ESTABLISHED
tcp4 0 0 127.0.0.1.51003 127.0.0.1.8080 ESTABLISHED
在 SOF 上可以搜到這段話,但是感覺和上面觀察到的並不相符?
The underlying HTTP connection is still held by the response object to allow the response content to be streamed directly from the network socket. In order to ensure correct deallocation of system resources, the user MUST call CloseableHttpResponse#close() from a finally clause. Please note that if response content is not fully consumed the underlying connection cannot be safely re-used and will be shut down and discarded by the connection manager.
第三個 clsoe,也就是 httpclient.close 會徹底關閉連接池,以及其中所有連接,一般情況下,只有在關閉應用時調用以釋放資源(補充:當 httpClientBuilder.setConnectionManagerShared(true) 時,並不會關閉連接池)。
二、連接復用
根據 http 協議 1.1 版本,各個 web 服務器都默認支持 keepalive,因此當 http 請求正常完成后,服務器不會主動關閉 tcp(直到空閑超時或數量達到上限),使連接會保留一段時間,前面我們也知道 httpclient 在判斷可以 keepalive 后,即使調用了 close 也不會關閉 tcp 連接(可以認為 release 到連接池)。為了管理這些保留的連接,以及方便 api 調用,一般設置一個全局的連接池,並基於該連接池提供 httpclient 實例,這樣就不需要考慮維護 httpclient 實例生命周期,隨用隨取(方便狀態管理?),此外考慮到 http 的單路性,一個請求響應完成結束后,該連接才可以再次復用,因此連接池的最大連接數決定了並發處理量,該配置也是一種保護機制,超出上限的請求會被阻塞,也可以配合熔斷組件使用,當服務方慢、或不健康時熔斷降級。
最后還有一個問題,觀察到 keepalive 的 tcp 連接過一段時間后會變成如下狀態:
# netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51866 FIN_WAIT_2
tcp4 0 0 127.0.0.1.51866 127.0.0.1.8080 CLOSE_WAIT
可以看出服務器經過一段時間,認為該連接空閑,因此主動關閉,收到對方響應后進入 FIN_WAIT_2 狀態(等待對方也發起關閉),而客戶端進入 CLOSE_WAIT 狀態后卻不再發起自己這一方的關閉請求,這時雙方處於半關閉。官方文檔解釋如下:
One of the major shortcomings of the classic blocking I/O model is that the network socket can react to I/O events only when blocked in an I/O operation. When a connection is released back to the manager, it can be kept alive however it is unable to monitor the status of the socket and react to any I/O events. If the connection gets closed on the server side, the client side connection is unable to detect the change in the connection state (and react appropriately by closing the socket on its end).
這需要有定期主動做一些檢測和關閉動作,從這個角度考慮,默認配置產生的 HttpClient 沒有這一功能,不應該用於生產環境,下面這個監控線程可以完成該工作,包含它的完整的 HttpUtil 從文章最后連接獲取。
public static class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(30 * 1000);
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
最后展示一個完整的示例,首先多線程發起兩個請求,看到創建兩個連接,30秒之后再發起一個請求,可以復用之前其中一個連接,另一個連接因空閑被關閉,隨后最后等待 2 分鍾后再發起一個請求,由於之前連接已過期失效,重新創建連接。
並發兩個請求
16:54:44.504 [ Thread-4] : Connection request: [route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 0 of 150; total allocated: 0 of 150]
16:54:44.504 [ Thread-5] : Connection request: [route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 0 of 150; total allocated: 0 of 150]
16:54:44.515 [ Thread-5] : Connection leased: [id: 1][route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 2 of 150; total allocated: 2 of 150]
16:54:44.515 [ Thread-4] : Connection leased: [id: 0][route: {}->http://127.0.0.1:8080][total kept alive: 0; route allocated: 2 of 150; total allocated: 2 of 150]
16:54:44.517 [ Thread-5] : Opening connection {}->http://127.0.0.1:8080
16:54:44.517 [ Thread-4] : Opening connection {}->http://127.0.0.1:8080
16:54:44.519 [ Thread-4] : Connecting to /127.0.0.1:8080
16:54:44.519 [ Thread-5] : Connecting to /127.0.0.1:8080
16:54:44.521 [ Thread-5] : Connection established 127.0.0.1:52421127.0.0.1:8080
16:54:44.521 [ Thread-4] : Connection established 127.0.0.1:52420127.0.0.1:8080
....
16:54:49.486 [ main] : [leased: 2; pending: 0; available: 0; max: 150]
16:54:49.630 [ Thread-4] : Connection can be kept alive indefinitely
16:54:49.630 [ Thread-5] : Connection can be kept alive indefinitely
16:54:49.633 [ Thread-4] : Connection [id: 0][route: {}->http://127.0.0.1:8080] can be kept alive indefinitely
16:54:49.633 [ Thread-5] : Connection [id: 1][route: {}->http://127.0.0.1:8080] can be kept alive indefinitely
16:54:49.633 [ Thread-4] : http-outgoing-0: set socket timeout to 0
16:54:49.633 [ Thread-5] : http-outgoing-1: set socket timeout to 0
16:54:49.633 [ Thread-4] : Connection released: [id: 0][route: {}->http://127.0.0.1:8080][total kept alive: 1; route allocated: 2 of 150; total allocated: 2 of 150]
16:54:49.633 [ Thread-5] : Connection released: [id: 1][route: {}->http://127.0.0.1:8080][total kept alive: 2; route allocated: 2 of 150; total allocated: 2 of 150]
16:54:54.488 [ main] : [leased: 0; pending: 0; available: 2; max: 150]
#netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.52421 ESTABLISHED
tcp4 0 0 127.0.0.1.8080 127.0.0.1.52420 ESTABLISHED
tcp4 0 0 127.0.0.1.52421 127.0.0.1.8080 ESTABLISHED
tcp4 0 0 127.0.0.1.52420 127.0.0.1.8080 ESTABLISHED
下一個請求
16:55:14.489 [ Thread-6] : Connection request: [route: {}->http://127.0.0.1:8080][total kept alive: 2; route allocated: 2 of 150; total allocated: 2 of 150]
16:55:14.491 [ Thread-6] : http-outgoing-1 << "[read] I/O error: Read timed out"
16:55:14.491 [ Thread-6] : Connection leased: [id: 1][route: {}->http://127.0.0.1:8080][total kept alive: 1; route allocated: 2 of 150; total allocated: 2 of 150]
16:55:14.491 [ Thread-6] : http-outgoing-1: set socket timeout to 0
16:55:14.492 [ Thread-6] : http-outgoing-1: set socket timeout to 8000
.....
16:55:19.501 [ main] : [leased: 1; pending: 0; available: 1; max: 150]
16:55:19.504 [ Thread-6] : Connection can be kept alive indefinitely
16:55:19.504 [ Thread-6] : Connection [id: 1][route: {}->http://127.0.0.1:8080] can be kept alive indefinitely
16:55:19.505 [ Thread-6] : http-outgoing-1: set socket timeout to 0
16:55:19.505 [ Thread-6] : Connection released: [id: 1][route: {}->http://127.0.0.1:8080][total kept alive: 2; route allocated: 2 of 150; total allocated: 2 of 150]
16:55:24.504 [ main] : [leased: 0; pending: 0; available: 2; max: 150]
#netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.52421 ESTABLISHED
tcp4 0 0 127.0.0.1.8080 127.0.0.1.52420 ESTABLISHED
tcp4 0 0 127.0.0.1.52421 127.0.0.1.8080 ESTABLISHED
tcp4 0 0 127.0.0.1.52420 127.0.0.1.8080 ESTABLISHED
復用了上面的連接,下面是隨后逐步超時的日志。
16:55:39.513 [ main] : [leased: 0; pending: 0; available: 2; max: 150]
16:55:44.491 [ Thread-8] : Closing expired connections
16:55:44.492 [ Thread-8] : Closing connections idle longer than 30 SECONDS
16:55:44.492 [ Thread-8] : http-outgoing-0: Close connection
16:55:44.518 [ main] : [leased: 0; pending: 0; available: 1; max: 150]
....
16:56:09.535 [ main] : [leased: 0; pending: 0; available: 1; max: 150]
16:56:14.499 [ Thread-8] : Closing expired connections
16:56:14.499 [ Thread-8] : Closing connections idle longer than 30 SECONDS
16:56:14.499 [ Thread-8] : http-outgoing-1: Close connection
16:56:14.540 [ main] : [leased: 0; pending: 0; available: 0; max: 150]
分別對應狀態如下,可以看到復用了 52421,隨后 52420 空閑超時被回收,以及最后 52421 也被回收。
#netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.52421 ESTABLISHED
tcp4 0 0 127.0.0.1.52421 127.0.0.1.8080 ESTABLISHED
tcp4 0 0 127.0.0.1.52420 127.0.0.1.8080 TIME_WAIT
...
#netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.52421 127.0.0.1.8080 TIME_WAIT
最后一個請求后,日志省略,可以看到是新的連接 52443。
netstat -n | grep tcp4 | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.52443 ESTABLISHED
tcp4 0 0 127.0.0.1.52443 127.0.0.1.8080 ESTABLISHED
參考:
相關資源:STM32F4+LWIP+HTTPclient(TCPclient)+DHCP+DNS_tcpclientlwip...
Windows Socket 最大連接數
Socket 編程時,單機最多可以建立多少個 TCP 連接,受到操作系統的影響。
Windows 下單機的TCP連接數受多個參數影響:
最大TCP連接數
[HKEY_LOCAL_MACHINE \System \CurrentControlSet \Services \Tcpip \Parameters] TcpNumConnections = 0x00fffffe (Default = 16,777,214)
以上注冊表信息配置單機的最大允許的TCP連接數,默認為 16M。這個數值看似很大,這個並不是限制最大連接數的唯一條件,還有其他條件會限制到TCP 連接的最大連接數。
最大動態端口數
TCP客戶端和服務器連接時,客戶端必須分配一個動態端口,默認情況下這個動態端口的分配范圍為 1024-5000 ,也就是說默認情況下,客戶端最多可以同時發起3977 個Socket 連接。我們可以修改如下注冊表來調整這個動態端口的范圍
[HKEY_LOCAL_MACHINE \System \CurrentControlSet \Services \Tcpip \Parameters] MaxUserPort = 5000 (Default = 5000, Max = 65534)
最大TCB 數量
系統為每個TCP 連接分配一個TCP 控制塊(TCP control block or TCB),這個控制塊用於緩存TCP連接的一些參數,每個TCB需要分配 0.5 KB的pagepool 和 0.5KB 的Non-pagepool,也就說,每個TCP連接會占用 1KB 的系統內存。
系統的最大TCB數量由如下注冊表設置決定
[HKEY_LOCAL_MACHINE \System \CurrentControlSet \Services \Tcpip \Parameters] MaxFreeTcbs = 2000 (Default = RAM dependent, but usual Pro = 1000, Srv=2000)
非Server版本,MaxFreeTcbs 的默認值為1000 (64M 以上物理內存)
Server 版本,這個的默認值為 2000。
也就是說,默認情況下,Server 版本最多同時可以建立並保持2000個TCP 連接。
最大TCB Hash table 數量
TCB 是通過Hash table 來管理的,下面注冊表設置決定了這個Hash table 的大小
HKEY_LOCAL_MACHINE \System \CurrentControlSet \services \Tcpip \Parameters] MaxHashTableSize = 512 (Default = 512, Range = 64-65536)
這個值指明分配 pagepool 內存的數量,也就是說,如果MaxFreeTcbs = 1000 , 則 pagepool 的內存數量為 500KB
那么 MaxHashTableSize 應大於 500 才行。這個數量越大,則Hash table 的冗余度就越高,每次分配和查找 TCP 連接用時就越少。這個值必須是2的冪,且最大為65536.
參考:
IBM WebSphere Voice Server 在windows server 2003 下的典型配置
MaxUserPort = 65534 (Decimal)
MaxHashTableSize = 65536 (Decimal)
MaxFreeTcbs = 16000 (Decimal)
這里 MaxHashTableSize 被配置為比MaxFreeTcbs 大4倍,這樣可以大大增加TCP建立的速度。
