InfluxDB是當今最為流行的開源時序數據庫,廣泛應用於監控場景,因為監控數據的來源多樣,InfluxDB的數據寫入鏈路也具有一定的復雜性。本文將分享一次由網絡狀況不佳而觸發的寫入抖動問題的排查過程,並且深入分析其背后所涉及到的技術原理。

問題暴露
某用戶反饋其InfluxDB實例的寫入性能出現抖動,大量寫入請求失敗,從監控數據看也證實了用戶反饋的問題,之前一直平穩的寫入性能曲線出現了十分明顯的抖動,甚至跌零,如下圖所示:

出現此類問題,首先需要確認最近是否有變更,因為大多數軟件問題都是由變更觸發的。然而用戶確認其客戶端沒有任何變更,而服務端這邊也沒有任何運維操作,所以問題就變得棘手,到底是什么原因導致的呢?
InfluxDB的寫入控制
在介紹排查歷程之前,先簡單描述一下InfluxDB的寫入流控機制,以便於讀者理解。

如上圖所示,InfluxDB的寫入通過HTTP/1.1協議實現,並發處理的請求數是有限制的,由一個長度可配置的定長隊列來控制,每個請求處理完成之后會將結果(成功或失敗)返回給客戶端。如果處理隊列滿了,寫入請求會進入一個等待隊列,按照FIFO方式進行服務。請求在等待隊列中的等待是有timeout機制的,超過30s就會返回超時錯誤給客戶端。這種模式很容易理解,大家可以想象為商場買東西時結賬,有固定數量的收銀台,客戶排成一個隊,每個收銀台服務完一個客戶后會接待隊列中最前面的一個客戶。
問題分析
發現這種異常情況之后,立刻從下面幾方面開始排查:
-
首先自然是分析日志,InfluxDB的HTTP訪問日志記錄了每個寫請求的處理耗時,也就是從收到請求到返回response給客戶端的時間。不出意外,從日志中發現了大量HTTP請求處理超時,也就是請求在等待隊列中超過30s后返回timeout錯誤給客戶端;實例監控也顯示寫入量下跌甚至跌零。由此判斷,等待隊列的消費(出隊)能力已經很低甚至喪失,而請求處理模塊是負責消費等待隊列中的請求的,很可能是寫請求的處理邏輯出了問題。
-
進一步分析日志,發現了一個略感意外的有趣現象,即有少量請求的處理時間長達數小時甚至幾天,這顯然是不正常的,這些請求是在從等待隊列進入處理隊列之后運行了幾天時間才結束;因為等待隊列的處理邏輯十分簡單,超過30s就會返回了,不會出現出現超長的等待。為什么服務端處理一個請求會耗時幾天?要直接回答這個問題顯然要通讀整個處理流程的代碼,這不是一件容易的事。但是這些超長的請求處理日志為問題分析提供了另一個切入點。
-
通過對服務端網絡連接進行分析,發現存在大量處於established狀態的TCP連接。同用戶溝通發現,這些連接的遠端,也就是客戶端程序所在的主機上,並沒有看到對應的tcp連接信息。基於對tcp協議的了解,可以知道這些TCP連接已經變成了半開(half-open)連接(詳細解釋在下文會給出)。出現這么多的半開連接顯然是不正常的,所以馬上與用戶溝通,了解其使用場景,發現其客戶端較多,而且分布在不同地域,包括海外,而幾個海外地址的網絡連通性較差,測試發現丟包率甚至超過60%,這些丟包率高的客戶端恰恰就是前文提到的tcp半開連接所對應的遠端地址。另外一個重要信息是,客戶端的http超時設置很短,只有2秒鍾,超時就會斷開連接,所以客戶端斷開連接后服務端並沒有關閉對應的連接。
這個發現成了問題的突破口。如果服務端在半開連接上進行數據讀取,是會阻塞的,而influxdb的http連接沒有設置讀取超時,所以阻塞幾天時間是很可能的。
問題根因
有了突破口,就可以進一步排查驗證,最終破解真相,下面是梳理出的問題爆發流程:
-
InfluxDB服務端使用Go net/http庫實現,當客戶端發送一個請求到服務端,服務端在讀取HTTP header之后會交付給請求處理模塊,而此時HTTP body可能還沒有全部發送過來,因為內核緩沖區可能無法接受全部body數據。
-
當寫請求進入了處理隊列,會讀取 HTTP body,獲取需要寫入的數據。這里的讀取邏輯有一個缺陷,就是沒有設置超時!
-
問題的起點:當大量寫入請求涌入,部分請求會進入等待隊列,系統負載過高時,某些請求無法在2秒內處理完,這時客戶端就會直接斷開連接。
-
因為網絡丟包率很高,客戶端關閉TCP連接的FIN或者RST數據包有很高的概率會丟失,而一旦丟失就導致了服務端遺留了半開連接,即客戶端已經釋放了連接,而服務端依然在維護這個tcp連接。
-
連接上的請求從等待隊列進入處理隊列后,會從連接上讀取http body,因為沒有超時,這個讀操作會阻塞。
-
一旦阻塞,就意味着處理隊列的一個slot被占用了!
-
隨着問題的積累(大約兩周的時間), 越來越多的slot被占用,InfluxDB的處理能力逐步下降,更多的請求等待和超時,如下圖所示:

- 最終,處理隊列被打滿,無法處理任何新請求,所有的請求在等待隊列中等待30s后返回超時錯誤給客戶端。
問題的來龍去脈搞清楚了,修復方案也很簡單,可以通過設置服務端的讀取超時來避免長時間阻塞。
TCP半連接&KeepAlive
半開連接問題是TCP協議中比較常見的異常情況,其描述可以參考rfc793。
An established connection is said to be "half-open" if one of the TCPs has closed or aborted the connection at its end without the knowledge of the other, or if the two ends of the connection have become desynchronized owing to a crash that resulted in loss of memory. Such connections will automatically become reset if an attempt is made to send data in either direction. However, half-open connections are expected to be unusual, and the recovery procedure is mildly involved.
半開連接的原因可能是遠端的意外崩潰,比如主機掉電,或者網絡故障,也有可能是惡意程序有意為之;無論如何,對於服務器而言,半開連接意味着資源消耗,因為內核需要維護tcp連接信息,所以需要一種機制來探測tcp連接的對端是否還活着。一般來說,設計網絡應用時,應用層都會使用心跳機制來檢測遠端的狀態,以確保在沒有數據傳輸的情況下也能及時發現遠端異常。
而keep-alive是TCP提供的,通過發送空數據包來驗證連接可用性的機制。嚴格來說,keep-alive不是TCP協議的,但是大多數TCP的實現都支持這種機制。
對於Linux系統來說,一般都會開啟。具體的keepalive配置參數可以從proc 文件獲取到:
~]# cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
~]# cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
~]# cat /proc/sys/net/ipv4/tcp_keepalive_probes
9
參數含義如下:
tcp_keepalive_intvl (integer; default: 75; since Linux 2.4)
The number of seconds between TCP keep-alive probes.
tcp_keepalive_probes (integer; default: 9; since Linux 2.2)
The maximum number of TCP keep-alive probes to send before
giving up and killing the connection if no response is
obtained from the other end.
tcp_keepalive_time (integer; default: 7200; since Linux 2.2)
The number of seconds a connection needs to be idle before TCP
begins sending out keep-alive probes. Keep-alives are sent
only when the SO_KEEPALIVE socket option is enabled. The
default value is 7200 seconds (2 hours). An idle connection
is terminated after approximately an additional 11 minutes
(9 probes an interval of 75 seconds apart) when keep-alive is enabled.
Note that underlying connection tracking mechanisms and
application timeouts may be much shorter.
也就是說,如果一個tcp連接上有7200秒(2小時)沒有數據傳輸,keepalive探測就會開啟,並且每75秒進行一次探測,如果連續9次探測失敗,就會關閉這個連接。這個keepalive配置是全局的,不能針對每個socket獨立設置,靈活性不足,而且兩個小時的間隔對一般應用來說有點過長了,所以應用層的心跳機制還是需要的。
需要注意的是,即使系統開啟了tcp keepalive,一個tcp連接也需要顯式的設置SO_KEEPALIVE參數才能真正開啟keepalive!因為RFC1122中有如下描述:
4.2.3.6 TCP Keep-Alives
Implementors MAY include "keep-alives" in their TCP
implementations, although this practice is not universally
accepted. If keep-alives are included, the application MUST
be able to turn them on or off for each TCP connection, and
they MUST default to off.
InfluxDB的服務端沒有對每個連接開啟keep-alive,所以才會出現半開連接維持了很多天的現象。
