天天看點

記一次InfluxDB寫入抖動問題的排查和思考

前言

InfluxDB是當今最為

流行

的開源時序資料庫,廣泛應用于監控場景,因為監控資料的來源多樣,InfluxDB的資料寫傳入連結路也具有一定的複雜性。本文将分享一次由網絡狀況不佳而觸發的寫入抖動問題的排查過程,并且深入分析其背後所涉及到的技術原理。

記一次InfluxDB寫入抖動問題的排查和思考

問題的出現

某使用者回報其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連接配接沒有設定讀取逾時,是以阻塞幾天時間是很可能的。

真相大白

有了突破口,就可以進一步排查驗證,最終破解真相,下面是梳理出的問題爆發流程:

  1. InfluxDB服務端使用Go net/http庫實作,當用戶端發送一個請求到服務端,服務端在讀取HTTP header之後會傳遞給請求處理子產品,而此時HTTP body可能還沒有全部發送過來,因為核心緩沖區可能無法接受全部body資料。
  2. 當寫請求進入了處理隊列,會讀取 HTTP body,擷取需要寫入的資料。這裡的讀取邏輯有一個缺陷,就是沒有設定逾時!
  3. 問題的起點:當大量寫入請求湧入,部分請求會進入等待隊列,系統負載過高時,某些請求無法在2秒内處理完,這時用戶端就會直接斷開連接配接。
  4. 因為網絡丢包率很高,用戶端關閉TCP連接配接的FIN或者RST資料包有很高的機率會丢失,而一旦丢失就導緻了服務端遺留了半開連接配接,即用戶端已經釋放了連接配接,而服務端依然在維護這個tcp連接配接。
  5. 連接配接上的請求從等待隊列進入處理隊列後,會從連接配接上讀取http body,因為沒有逾時,這個讀操作會阻塞。
  6. 一旦阻塞,就意味着處理隊列的一個slot被占用了!
  7. 随着問題的積累(大約兩周的時間), 越來越多的slot被占用,InfluxDB的處理能力逐漸下降,更多的請求等待和逾時,如下圖所示
    記一次InfluxDB寫入抖動問題的排查和思考
  8. 最終,處理隊列被打滿,無法處理任何新請求,所有的請求在等待隊列中等待30s後傳回逾時錯誤給用戶端。

問題的來龍去脈搞清楚了,修複方案也很簡單,可以通過設定服務端的讀取逾時來避免長時間阻塞。

最後,有一個問題讀者有興趣可以思考下:為什麼半開連接配接上的read()調用在阻塞幾小時或者幾天這些不等的時間後傳回了? 

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,是以才會出現半開連接配接維持了很多天的現象。

寫在最後

本文分享了一次因TCP半開連接配接導緻,網絡丢包觸發的伺服器問題,深入剖析了TCP keep-alive機制。

下面是廣告時間。阿裡雲InfluxDB作為開源托管服務,在性能和穩定性方面做了大量優化,提供7*24的技術支援,是各種監控場景的絕佳存儲方案。目前推出了首購一進制體驗活動,歡迎大家試用。

記一次InfluxDB寫入抖動問題的排查和思考

繼續閱讀