問(wèn)題描述
啟動(dòng) 0.9.2 版本的 hugegraph-server 和 hugegraph-studio扭粱,隨便執(zhí)行兩條查詢語(yǔ)句悟狱,然后停止 hugegraph-server滔蝉,再啟動(dòng)壳炎,提示 “8080 端口被占用”搁痛。
問(wèn)題定位
Step 1
最開始沒(méi)仔細(xì)想长搀,主要也是因?yàn)閷?duì) TCP 這塊沒(méi)啥實(shí)際經(jīng)驗(yàn),乍一看以為是 studio 每次執(zhí)行 gremlin 建立一個(gè)連接且連接未關(guān)閉導(dǎo)致的問(wèn)題鸡典。于是簡(jiǎn)單的在 studio
每個(gè)請(qǐng)求處理完成后將 HugeClient 關(guān)閉掉源请,即 finally{ client.close() }
,然后測(cè)試彻况,每個(gè)請(qǐng)求處理完谁尸,連接都正常關(guān)閉,然后停止 hugegraph-server
再重啟纽甘,果然就沒(méi)問(wèn)題了良蛮,看來(lái)這個(gè)問(wèn)題很簡(jiǎn)單嘛。
但是這么搞總覺得太暴力了悍赢,按照書上和博客的說(shuō)法决瞳,連接的建立/釋放都是不小的開銷货徙,咱能不能重用他呢?
Step 2
在 studio 中重用 HugeClient 也很簡(jiǎn)單皮胡,簡(jiǎn)單點(diǎn)的話痴颊,把它定義為靜態(tài)的,然后請(qǐng)求入口處以單例的形式獲取 HugeClient 即可屡贺。我在 loader 中已經(jīng)寫過(guò)一個(gè)
HugeClientWarpper蠢棱,所以這里也很快完成。再次測(cè)試甩栈,除第一次請(qǐng)求創(chuàng)建了一個(gè)連接外泻仙,后面的請(qǐng)求都沒(méi)有創(chuàng)建新的連接,看起來(lái)節(jié)約了開銷量没。然后停止
hugegraph-server 再重啟玉转,結(jié)果又提示了 “8080 端口被占用”。再查看 studio 進(jìn)程的 TCP 連接使用情況允蜈,發(fā)現(xiàn)還是有一個(gè)處于 CLOSE_WAIT 狀態(tài)的連接冤吨,
并且這個(gè)連接一直不會(huì)關(guān)閉。
除了停止 hugegraph-server 會(huì)產(chǎn)生 CLOSE_WAIT 的連接外饶套,讓 hugegraph-server 閑置一會(huì)也會(huì)在 studio 進(jìn)程中產(chǎn)生 CLOSE_WAIT 狀態(tài)的連接漩蟆,但是只要
studio 再請(qǐng)求一次,那個(gè) CLOSE_WAIT 狀態(tài)的連接會(huì)消失妓蛮,然后產(chǎn)生一個(gè)新的 ESTABLISHED 狀態(tài)的連接怠李。
走到這里其實(shí)會(huì)發(fā)現(xiàn)兩個(gè)問(wèn)題:
- 為什么即使不停止 hugegraph-server 也會(huì)產(chǎn)生 CLOSE_WAIT 狀態(tài)的連接?
- 為什么 CLOSE_WAIT 狀態(tài)的連接不會(huì)自己消失蛤克,而是要等到 studio 再請(qǐng)求一次才會(huì)消失捺癞?
Step 3
要解釋第一個(gè)問(wèn)題,得找到 TCP 連接關(guān)閉的四次握手時(shí)序圖
可以看到构挤,連接關(guān)閉中只有被動(dòng)關(guān)閉的一方才會(huì)出現(xiàn) CLOSE_WAIT 狀態(tài)髓介,所以肯定是 hugegraph-server 主動(dòng)關(guān)閉了連接。
然后發(fā)現(xiàn) RestServer 有一個(gè) KeepAlive IdleTimeout筋现,閑置超過(guò)此時(shí)間的連接會(huì)被 jersey 關(guān)閉唐础。
The time in seconds to keep an inactive connection alive.
這個(gè)參數(shù)的默認(rèn)值為 30 秒,我們將其修改為 60矾飞,驗(yàn)證確實(shí)符合預(yù)期一膨。
這里插一句題外話,KeepAlive 除了有 IdleTimeout 參數(shù)洒沦,還有一個(gè) Max Requests Count豹绪。
The max number of HTTP requests allowed to be processed on one keep-alive connection.
這個(gè)參數(shù)的意思是:當(dāng)一個(gè)連接處理的請(qǐng)求數(shù)超過(guò)該值了,就將其關(guān)閉申眼。默認(rèn)為 256瞒津,我們將其修改為 5蝉衣,但是經(jīng)過(guò)調(diào)試發(fā)現(xiàn),在處理完第 6 個(gè)請(qǐng)求之后才會(huì)關(guān)閉連接巷蚪,
而不是我們?cè)O(shè)置的 5买乃。調(diào)試代碼:
private boolean checkKeepAliveRequestsCount(final HttpContext httpContext) {
if (!allowKeepAlive) {
return false;
}
final KeepAliveContext keepAliveContext = keepAliveContextAttr.get(httpContext);
final int requestsProcessed = keepAliveContext.requestsProcessed++;
final int maxRequestCount = keepAlive.getMaxRequestsCount();
final boolean isKeepAlive = (maxRequestCount == -1 ||
keepAliveContext.requestsProcessed <= maxRequestCount);
if (requestsProcessed == 0) {
if (isKeepAlive) { // New keep-alive connection
KeepAlive.notifyProbesConnectionAccepted(keepAlive,
keepAliveContext.connection);
} else { // Refused keep-alive connection
KeepAlive.notifyProbesRefused(keepAlive, keepAliveContext.connection);
}
}
return isKeepAlive;
}
關(guān)鍵在于keepAliveContext.requestsProcessed <= maxRequestCount
,當(dāng)處理完第一個(gè)請(qǐng)求钓辆,keepAliveContext.requestsProcessed
的值為1,
第二個(gè)請(qǐng)求值為2 ... 第五個(gè)請(qǐng)求為5肴焊,仍然是滿足 <= 5
條件的前联,這時(shí)仍然認(rèn)為這個(gè)連接是 KeepAlive 的,直到處理完第六個(gè)才不滿足條件娶眷,才會(huì)進(jìn)行關(guān)閉連接的操作似嗤。
我覺得這是一個(gè) BUG,至少這個(gè)參數(shù)的說(shuō)明與表現(xiàn)不符届宠。但是找了半天也沒(méi)找到該往 github 的哪個(gè)倉(cāng)庫(kù)反饋烁落。
Step 4
為什么 CLOSE_WAIT 狀態(tài)的連接不會(huì)自己消失,而是要等到 studio 再請(qǐng)求一次才會(huì)消失豌注?
這里得跟蹤 HttpClient 的代碼伤塌,我們以服務(wù)端關(guān)閉了連接,studio 的連接變成了 CLOSE_WAIT 之后作為調(diào)試起始點(diǎn)轧铁,在PoolingHttpClientConnectionManager
和AbstractConnPool
中獲取連接和關(guān)閉連接的代碼處加了很多斷點(diǎn)每聪,經(jīng)過(guò)一番折騰,終于發(fā)現(xiàn)齿风,在AbstractConnPool
的getPoolEntryBlocking
方法中找到了第二個(gè)
問(wèn)題的答案药薯。下面給出關(guān)鍵代碼:
private E getPoolEntryBlocking() {
...
while (entry == null) {
...
else if (this.validateAfterInactivity > 0) {
if (entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()) {
if (!validate(entry)) {
entry.close();
}
}
}
...
}
...
}
從if (!validate(entry))
一直往里跟會(huì)走到AbstractHttpClientConnection
的isStale()
方法,該方法會(huì)嘗試從socket
的InputBuffer
中讀一點(diǎn)數(shù)據(jù)救斑,
讀不到就認(rèn)為stale
童本,然后validate(entry)
驗(yàn)證失敗,調(diào)用entry.close()
將連接關(guān)閉脸候。
說(shuō)到這里基本上已經(jīng)弄明白了上面這兩個(gè)問(wèn)題穷娱,但其實(shí)還有一個(gè)問(wèn)題不太清楚。
- 服務(wù)端主動(dòng)關(guān)閉連接導(dǎo)致客戶端處于 CLOSE_WAIT 狀態(tài)纪他,但是根據(jù)四次握手的流程圖鄙煤,為什么服務(wù)端沒(méi)有卡在 FIN-WAIT1 的狀態(tài),而是直接就消失了茶袒,就好像是正常關(guān)閉了一樣梯刚。