公司使用了 InfluxDB 作為監(jiān)控系統(tǒng)的存儲(chǔ)后端, 在一次上線后,發(fā)現(xiàn)與InfluxDB 使用的 8086 端口相關(guān)的 TCP 連接數(shù)竟然多大 6K+ ,有時(shí)候甚至?xí)平?1w ,這個(gè)數(shù)量對(duì)于一個(gè)只是在內(nèi)部使用的監(jiān)控系統(tǒng)來(lái)說(shuō), 無(wú)論如何都是無(wú)法接受的, 于是開(kāi)始一系列的排查過(guò)程. 本文記錄了這個(gè)問(wèn)題的主要解決過(guò)程,算是對(duì)這一次殺 bug 過(guò)程的一個(gè)總結(jié).
問(wèn)題描述
因?yàn)闃I(yè)務(wù)的需要, client 對(duì) InfluxDB 做 query 時(shí), 會(huì)經(jīng)過(guò) server 的一個(gè) proxy , 再由 proxy 發(fā)起到 Influxdb 的 query (http) 請(qǐng)求. 在我們的部署架構(gòu)中, proxy 與 InfluxDB 是部署在同一個(gè) server 上的. 使用命令
netstat -apn | grep 8086
可以看到大量處于 TIME_WAIT狀態(tài)的 tcp 連接
...
tcp6 0 0 127.0.0.1:8086 127.0.0.1:58874 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59454 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59084 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59023 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59602 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59027 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59383 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59053 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:58828 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:58741 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59229 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:58985 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59289 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59192 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59161 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59292 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59242 TIME_WAIT -
tcp6 0 0 127.0.0.1:8086 127.0.0.1:59430 TIME_WAIT -
...
使用命令
netstat -apn | grep 8086 | grep TIME_WAIT | wc -l
進(jìn)行計(jì)數(shù), 會(huì)發(fā)現(xiàn)連接數(shù)會(huì)不斷增加, 經(jīng)過(guò)多次測(cè)試, 在公司環(huán)境中連接數(shù)至少都會(huì)達(dá)到 6k+. 這個(gè)問(wèn)題必須要解決, 一方面是因?yàn)槊織l tcp 連接都會(huì)占用內(nèi)存, 另一方面系統(tǒng)的動(dòng)態(tài)端口數(shù)也是有限的.
很明顯這些連接幾乎都處在 TIME_WAIT 狀態(tài),所以在繼續(xù)往下走之前, 需要了解下 TIME_WAIT 這個(gè)關(guān)鍵字
TIME_WAIT
我們知道 一條 tcp 連接從開(kāi)始到結(jié)束會(huì)經(jīng)歷多個(gè)狀態(tài), 換句話說(shuō), 可以把 一條 tcp 連接看成是一個(gè) 狀態(tài)機(jī). 這個(gè)狀態(tài)圖如下:

可以看到, 凡是主動(dòng)進(jìn)行關(guān)閉 tcp 連接的一方, 都會(huì)經(jīng)過(guò) TIME_WAIT 這個(gè)狀態(tài).接下來(lái)再經(jīng)過(guò) 2MSL 的時(shí)間后內(nèi)核再完全釋放相應(yīng)的文件描述符和端口. (順便提一下, MSL 是最大分段壽命, 是一個(gè) TCP 分段可以存在于互聯(lián)網(wǎng)系統(tǒng)中的最大時(shí)間, 在 Linux 下可以用命令
cat /proc/sys/net/ipv4/tcp_fin_timeout
查看 MSL的數(shù)值)
到這個(gè)地方可以推斷出, 是 8086 端口(即 InfluxDB) 主動(dòng)關(guān)閉了 tcp 連接, 導(dǎo)致擠壓了大量的處于 TIME_WAIT 狀態(tài)下的連接在等待內(nèi)核釋放. 于是自然而然得會(huì)想到, 能不能限制 InfluxDB 能打開(kāi)的最大連接數(shù), 讓它盡可能復(fù)用每一條 tcp 連接?
果不其然, 在 InfluxDB 的配置文件中, 有這么一個(gè)配置項(xiàng)
# The maximum number of HTTP connections that may be open at once. connections that
# would exceed this limit are dropped. Setting this value to 0 disables the limit.
max-connection-limit = 0
我將其該為100, 然后重啟數(shù)據(jù)庫(kù).
然而并沒(méi)有什么卵用, 看來(lái)還是得繼續(xù)想辦法.
查看 InfluxDB 源碼
接下來(lái)使用 curl 查看 Influxdb 返回的 http header
HTTP/1.1 200 OK
Connection: close
Content-Type: application/json
Request-Id: 95e5b54b-6af3-11e7-8001-000000000000
X-Influxdb-Build: OSS
X-Influxdb-Version: 1.3.0rc1
Date: Mon, 17 Jul 2017 13:26:48 GMT
Transfer-Encoding: chunked
可以看到 Connection 字段被設(shè)置為 close .
因?yàn)橐呀?jīng)確定我們用 Go 編寫的用于對(duì) InfluxDB 發(fā)起 http 請(qǐng)求的 proxy 已經(jīng)正確復(fù)用了 tcp 連接 , 所以就感覺(jué)問(wèn)題應(yīng)該是處在了 InfluxDB 上, 所以接下來(lái)就開(kāi)始翻開(kāi) InfluxDB 的源碼查看究竟.
文章中提到的 InfluxDB 版本為 1.2 , 處理 query 請(qǐng)求的代碼在services/httpd/handler.go 中, 函數(shù)簽名為
func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user meta.User)
在其中發(fā)現(xiàn)一句代碼
rw.Header().Add("Connection", "close")
看到這里感覺(jué)一切豁然開(kāi)朗了, 確實(shí)是 Influxdb 主動(dòng)關(guān)閉的連接, 并且通知 client (也就是文中提到的 proxy ) 在請(qǐng)求完成后也關(guān)閉連接.
為了驗(yàn)證這一點(diǎn), 我將上面那句代碼注釋掉后重新編譯 InfluxDB, 在 client 正確復(fù)用連接的情況下, 連接數(shù)確實(shí)可以保持在 10 以內(nèi).
現(xiàn)在問(wèn)題就變成, InfluxDB 為何要這樣設(shè)計(jì) ?

后續(xù)
在 Github 的一個(gè) issue 上, 我向 InfluxDB 的官方反映了這個(gè)問(wèn)題, 官方也注意到了, 可能在接下來(lái)的 1.3 版本會(huì)修復(fù)這個(gè)問(wèn)題, 拭目以待.
--------- 更新 ------
Influxdb 之所以會(huì)強(qiáng)制關(guān)閉連接, 是因?yàn)?Go 對(duì)復(fù)用的連接使用
ResponseWriter.CloseNotify()
獲取通知的時(shí)候會(huì)有問(wèn)題, 于是他們強(qiáng)制 client 在后續(xù)請(qǐng)求中重新建立連接而不是選擇復(fù)用.不過(guò) InfluxDB 對(duì)連接的關(guān)閉通知做了一些另外的處理, 所以上面的那句代碼可以去掉.