攜程容器偶發性超時問題案例分析

隨着攜程的應用大規模在生產上用容器部署,各種上規模的問題都慢慢浮現,其中比較難定位和解決的就是偶發性超時問題,下面將分析目前爲止我們遇到的幾種偶發性超時問題以及排查定位過程和解決方法,希望能給遇到同樣問題的小夥伴們以啓發。

 

問題描述

某一天接到用戶報障說,Redis集羣有超時現象發生,比較頻繁,而訪問的QPS也比較低。緊接着,陸續有其他用戶也報障Redis訪問超時。在這些報障容器所在的宿主機裏面,我們猛然發現有之前因爲時鐘漂移問題升級過內核到4.14.26的宿主機ServerA,心裏突然有一絲不詳的預感。

 

初步分析

因爲現代軟件部署結構的複雜性以及網絡的不可靠性,我們很難快速定位“connect timout”或“connectreset by peer”之類問題的根因。

歷史經驗告訴我們,一般比較大範圍的超時問題要麼和交換機路由器之類的網絡設備有關,要麼就是底層系統不穩定導致的故障。從報障的情況來看,4.10和4.14的內核都有,而宿主機的機型也涉及到多個批次不同廠家,看上去沒頭緒,既然沒什麼好辦法那就抓個包看看吧。

圖1

 

圖2

 

圖1是App端容器和宿主機的抓包,圖2是Redis端容器和宿主機的抓包。因爲APP和Redis都部署在容器裏面(圖3),所以一個完整請求的包是B->A->C->D,而Redis的回包是D->C->A->B。

 

圖3


上面抓包的某一條請求如下:

 

  1. B(圖1第二個)的請求時間是21:25:04.846750 #99
  2. 到達A(圖1第一個)的時間是21:25:04.846764 #96
  3. 到達C(圖2第一個)的時間是21:25:07.432436 #103
  4. 到達D(圖2第二個)的時間是21:25:07.432446 #115

 

該請求從D回覆如下:

 

  1. D的回覆時間是21:25:07.433248 #116
  2. 到達C的時間是21:25:07.433257 #104
  3. 到達A點時間是21:25:05:901108 #121
  4. 到達B的時間是21:25:05:901114 #124

 

從這一條請求的訪問鏈路我們可以發現,B在200ms超時後等不到回包。在21:25:05.055341重傳了該包#100,並且可以從C收到重傳包的時間#105看出,幾乎與#103的請求包同時到達,也就是說該第一次的請求包在網絡上被延遲傳輸了。大致的示意如下圖4所示:

圖4


從抓包分析來看,宿主機上好像並沒有什麼問題,故障在網絡上。而我們同時在兩邊宿主機,容器裏以及連接宿主機的交換機抓包,就變成了下面圖5所示,假設連接A的交換機爲E,也就是說A->E這段的網絡有問題。

 

圖5

 

陷入僵局

儘管發現A->E這段有問題,排查卻也就此陷入了僵局,因爲影響的宿主機遍佈多個IDC,這也讓我們排除了網絡設備的問題。我們懷疑是否跟宿主機的某些TCP參數有關,比如TSO/GSO,一番測試後發現開啓關閉TSO/GSO和修改內核參數對解決問題無效,但同時我們也觀察到,從相同IDC裏任選一臺宿主機Ping有問題的宿主機,百分之幾的概率看到很高的響應值,如下圖6所示:

 

圖6

同一個IDC內如此高的Ping響應延遲,很不正常。而這時DBA告訴我們,他們的某臺物理機ServerB也有類似的的問題,Ping延遲很大,SSH上去後明顯感覺到有卡頓,這無疑給我們解決問題帶來了希望,但又更加迷惑:

 

  1. 延遲好像跟內核版本沒有關係,3.10,4.10,4.14的三個版本內核看上去都有這種問題。
  2. 延遲和容器無關,因爲延遲都在宿主機上到連接宿主機的交換機上發現的。
  3. ServerB跟ServerA雖然表現一樣,但細節上看有區別,我們的宿主機在重啓後基本上都能恢復一段時間後再復現延遲,但ServerB重啓也無效。

 

由此我們判斷ServerA和ServerB的症狀並不是同一個問題,並讓ServerB先升級固件看看。在升級固件後ServerB恢復了正常,那麼我們的宿主機是不是也可以靠升級固件修復呢?答案是並沒有。升級固件後沒過幾天,延遲的問題又出現了。

 

意外發現

回過頭來看之前爲了排查Skylake時鐘漂移問題的ServerA,上面一直有個簡單的程序在運行,來統計時間漂移的值,將時間差記到文件中。當時這個程序是爲了驗證時鐘漂移問題是否修復,如圖7:

 

圖7

 

這個程序跑在宿主機上,每個機器各有差異,但正常的時間差應該是100us以內,但1個多月後,時間差異越來越大,如圖8,最大能達到幾百毫秒以上。這告訴我們可能通過這無意中的log來找到根因,而且驗證了上面3的這個猜想,宿主機是運行一段時間後逐漸出問題,表現爲第一次打點到第二次打點之間,調度會自動delay第二次打點。

圖8

TSC和Perf

Turbostat是intel開發的,用來查看CPU狀態以及睿頻的工具,同樣可以用來查看TSC的頻率。而關於TSC,之前的文章《攜程一次Redis遷移容器後Slowlog“異常”分析》中有過相關介紹,這裏就不再展開。

 

在有問題的宿主機上,TSC並不是恆定的,如圖9所示,這個跟相關資料有出入,當然我們分析更可能的原因是,turbostat兩次去取TSC的時候,被內核調度delay了,如果第一次取時被delay導致取的結果比實際TSC的值要小,而如果第二次取時被delay會導致取的結果比實際要大。

圖9

Perf是內置於Linux上的基於採樣的性能分析工具,一般隨着內核一起編譯出來,具體的用法可以搜索相關資料,這裏也不展開。用perf sched record -a sleep 60和perf sched latency -s max來查看Linux的調度延遲,發現最大能錄得超過1s的延遲,如圖10和圖11所示。用戶態的進程有時因爲CPU隔離和代碼問題導致比較大的延遲還好理解,但這些進程都是內核態的。儘管Linux的CFS調度並非實時的調度,但在負載很低的情況下超過1s的調度延遲也是匪夷所思的。

 

圖10

 

圖11

根據之前的打點信息和Turbostat以及Perf的數據,我們非常有理由懷疑是內核的調度有問題,這樣我們就用基於RDTSCP指令更精準地來獲取TSC值來檢測CPU是否卡頓。RDTSCP指令不僅可以獲得當前TSC的值,並且可以得到對應的CPU ID。如圖12所示:

圖12

上面的程序編譯後,放在宿主機上依次綁核執行,我們發現在問題的宿主機上可以打印出比較大的TSC的值。每間隔100ms去取TSC的值,將獲得的相減,在正常的宿主機上它的值應該跟CPU的TSC緊密相關,比如我們的宿主機上TSC是1.7GHZ的頻率,那麼0.1s它的累加值應該是170000000,正常獲得的值應該是比170000000多一點,圖13的第五條的值本身就說明了該宿主機的調度延遲在2s以上。

圖13

真相大白

通過上面的檢測手段,可以比較輕鬆地定位問題所在,但還是找不到根本原因。這時我們突然想起來,線上Redis大規模使用宿主機的內核4.14.67並沒有類似的延遲,因此我們懷疑這種延遲問題是在4.14.26到4.14.67之間的bugfix修復掉了。

 

查看commit記錄,先二分查找大版本,再將懷疑的點單獨拎出來patch測試,終於發現了這個4.14.36-4.14.37之間的(圖14)commit:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=v4.14.37&id=b8d4055372b58aad4a51b67e176eabdcc238fde3。

 

圖14

從該commit的內容來看,修復了無效的apic id會導致possible CPU個數不正確的情況,那麼什麼是x2apic呢?什麼又是possible CPU?怎麼就導致了調度的延遲呢?

 

說到x2apic,就必須先知道apic,翻查網上的資料就發現,apic全稱Local Advanced ProgrammableInterrupt Controller,是一種負責接收和發送中斷的芯片,集成在CPU內部,每個CPU有一個屬於自己的local apic。它們通過apic id進行區分。而x2apic是apic的增強版本,將apic id擴充到32位,理論上支持2^32-1個CPU。簡單的說,操作系統通過apic id來確定CPU的個數。

 

而possible CPU則是內核爲了支持CPU熱插拔特性,在開機時一次載入,相應的就有online,offline CPU等,通過修改/sys/devices/system/cpu/cpu9/online可以動態關閉或打開一個CPU,但所有的CPU個數就是possible CPU,後續不再修改。

 

該commit指出,因爲沒有忽略apic id爲0xffffffff的值,導致possible CPU不正確。此commit看上去跟我們的延遲問題找不到關聯,我們也曾向該issue的提交者請教調度延遲的問題,對方也不清楚,只是表示在自己環境只能復現possible CPU增加4倍,vmstat的運行時間增加16倍。

 

這時我們查看有問題的宿主機CPU信息,奇怪的事情發生了,如圖15所示,12核的機器上possbile CPU居然是235個,而其中12-235個是offline狀態,也就是說真正工作的只有12個,這麼說好像還是跟延遲沒有關係。

 

圖15

繼續深入研究possbile CPU,我們發現了一些端倪。從內核代碼來看,引用for_each_possible_cpu()這個函數的有600多處,遍佈各個內核子模塊,其中關鍵的核心模塊比如vmstat,shed,以及loadavg等都有對它的大量調用。而這個函數的邏輯也很簡單,就是遍歷所有的possible CPU,那麼對於12核的機器,它的執行時間是正常宿主機執行時間的將近20倍!該commit的作者也指出太多的CPU會浪費向量空間並導致BUG(https://lkml.org/lkml/2018/5/2/115),而BUG就是調度系統的緩慢延遲。


以下圖16,圖17是對相同機型相同廠商的兩臺空負載宿主機的kubelet的Perf數據(perf stat -p $pid sleep 60),圖16是uptime 2天的,而圖17是uptime 89天的。

 

圖16

圖17

我們一眼就看出圖16的宿主機不正常,因爲無論是CPU的消耗,上下文的切換速度,指令週期,都遠劣於圖17的宿主機,並且還在持續惡化,這就是宿主機延遲的根本原因。而圖17宿主機恰恰只是圖16的宿主機打上圖14的patch後的內核,可以看到,possible CPU恢復正常(圖18),至此超時問題的排查告一段落。

圖18

小結

我們排查發現,不是所有的宿主機,所有的內核都在此BUG的影響範圍內,具體來說4.10(之前的有可能會有影響,但我們沒有類似的內核,無法測試)-4.14.37(因爲是stable分支,所以Master分支可能更靠後)的內核,CPU爲skylake及以後型號的某些廠商的機型會觸發這個BUG。

確認是否受影響也比較簡單,查看possible CPU是否是真實CPU即可。

 

問題再現

隨着內核升級到4.14.67,看上去延遲的問題徹底解決了,然而並沒有,只是出現的更加緩慢。幾周後,超時報障又找了過來,我們用Perf來分析,發現了一些異常。

如圖19所示是一個空負載的宿主機升級內核後8天的Perf的數據,明顯可以看到kworker的max delay已經100ms+,而這次有規律的是,延遲比較大的都是最後四個核,對於12核的節點就是8-11,並且都是同一D廠的宿主機。而上篇中使用新內核後用來驗證解決問題的卻不是D廠的宿主機,也就是說除了內核,還有其他的因素導致了延遲。

 

圖19

NUMA和CPU親和性綁定

NUMA全稱Non-Uniform Memory Access,NUMA服務器一般有多個節點,每個節點由多個CPU組成,並且具有獨立的本地內存,節點內部使用共有的內存控制器,節點之間是通過內部互聯(如QPI)進行通信。

 

然而,訪問本地內存的速度遠遠高於訪問其他節點的遠地內存的速度,通常有幾十倍的性能差異,這也正是NUMA名稱的由來。因爲NUMA的這個特性,爲了更好地發揮系統性能,應用程序應該儘量減少不同節點CPU之間的信息交互。

 

無意中發現,D廠的機型與其他機型的NUMA配置不一樣。假設12核的機型,D廠的機型NUMA節點分配如下圖20所示:

 

圖20

而其他廠家的機型NUMA節點分配如下圖21所示:

圖21

爲什麼會出現delay都是最後四個核上的進程呢?

 

經過深入排查才發現,原來相關同事之前爲了讓Kubernetes的相關進程和普通的用戶的進程相隔離,設置了CPU的親和性,讓Kubernetes的相關進程綁定到宿主機的最後四個核上,用戶的進程綁定到其他的核上,但後面這一步並沒有生效。

還是拿12核的例子來說,上面的Kubernetes是綁定到8-11核,但用戶的進程還是工作在0-11核上,更重要的是,最後4個核在遇到D廠家的這種機型時,實際上是跨NUMA綁定,導致了延遲越來越高,而用戶進程運行在相關的核上就會導致超時等各種故障。

確定問題後,解決起來就簡單了。將所有宿主機的綁核去掉,延遲就消失了,以下圖4是D廠的機型去掉綁核後開機26天Perf的調度延遲,從數據上看一切都恢復正常。

 

圖22

新的問題

大約過了幾個月,又有新的超時問題找到我們。有了之前的排查經驗,我們覺得這次肯定能很輕易的定位到問題,然而現實無情地給予了我們當頭一棒,4.14.67內核的宿主機,還是有大量無規律超時。


深入分析

Perf看調度延遲,如圖23所示,調度延遲比較大但並沒有集中在最後四個核上,完全無規律,同樣Turbostat依然觀察到TSC的頻率在跳動。

圖23

在各種猜想和驗證都被一一證否後,我們開始挨個排除來做測試:

  1. 我們將某臺A宿主機實例遷移走,Perf看上去恢復了正常,而將實例遷移回來,延遲又出現了。
  2. 另外一臺B宿主機上,我們發現即使將所有的實例都清空,Perf依然能錄得比較高的延遲。
  3. 而與B相連編號同一機型的C宿主機遷移完實例後重啓,Perf恢復正常。這時候看B的TOP,只有一個kubelet在消耗CPU,將這臺宿主機上的kubelet停掉,Perf正常,開啓kubelet後,問題又依舊。

 

這樣我們基本可以確定kubelet的某些行爲導致了宿主機卡頓和實例超時,對比正常/非正常的宿主機kubelet日誌,每秒鐘都在獲取所有實例的監控信息,在非正常的宿主機上,會打印以下的日誌。如圖24所示:

 

圖24

 

而在正常的宿主機上沒有該日誌或者該時間比較短,如圖25所示:

圖25

到這裏,我們懷疑這些LOG的行爲可能指向了問題的根源。查看Kubernetes代碼,可以知道在獲取時間超過指定值longHousekeeping (100ms)後,Kubernetes會記錄這一行爲,而updateStats即獲取本地的一些監控數據,如圖26代碼所示:

圖26

在網上搜索相關issue,問題指向cAdvisor的消耗CPU過高:

  • https://github.com/kubernetes/kubernetes/issues/15644
  • https://github.com/google/cadvisor/issues/1498

 

而在某個issue中指出(https://github.com/google/cadvisor/issues/1774):echo2 > /proc/sys/vm/drop_caches

 

可以暫時解決這種問題。我們嘗試在有問題的機器上執行清除緩存的指令,超時問題就消失了,如圖27所示。而從根本上解決這個問題,還是要減少取Metrics的頻率,比較耗時的Metrics乾脆不取或者完全隔離Kubernetes的進程和用戶進程纔可以。

圖27

硬件故障

在排查cAdvisor導致的延遲的過程中,還發現一部分用戶報障的超時,並不是cAdvisor導致的,主要表現在沒有Housekeeping的日誌,並且Perf結果看上去完全正常,說明沒有調度方面的延遲,但從TSC的獲取上還是能觀察到異常。

 

由此我們懷疑這是另一種全新的故障,最重要的是我們將某臺宿主機所有業務完全遷移掉,並關閉所有可以關閉的進程,只保留內核的進程後,TSC依然不正常並伴隨肉眼可見的卡頓,而這種現象跟之前DBA那臺物理機卡頓的問題很相似,這告訴我們很有可能是硬件方面的問題。

從以往的排障經驗來看,TSC抖動程度對於我們排查宿主機是否穩定有重要的參考作用。這時我們決定將TSC的檢測程序做成一個系統服務,每100ms去取一次系統的TSC值,將TSC的差值大於指定值打印到日誌中,並採集單位時間的異常條目數和最大TSC差值,放在監控系統上,來觀察異常的規律。如圖28所示。

圖28

這樣採集有幾個好處:

  1. 程序消耗比較小,僅僅消耗幾個CPU cycles的時間,完全可以忽略不計;
  2. 對於正常的宿主機,該日誌始終爲空;
  3. 對於有異常的宿主機,因爲採集力度足夠小,可以很清晰地定位到異常的時間點,這樣對於宿主機偶爾抖動情況也能採集到。

 

恰好TSC檢測的服務上線不久,一次明顯的故障說明了它檢測宿主機是否穩定的有效性。如圖29,在某日8點多時,一臺宿主機TSC突然升高,與應用的告警郵件和用戶報障同一時刻到來。如圖30所示:

 

圖29

 

圖30

將採集的日誌這樣展示後,我們一眼就發現問題都集中在某幾批次的同一廠商的宿主機上,並且我們找到之前DBA卡頓的物理機,也是這幾批次中的一臺。我們收集了幾臺宿主機的日誌詳情,反饋給廠商後,確認是硬件故障,無規律並且隨時可能觸發,需升級BIOS,如圖31廠商技術人員答覆的郵件所示,至此問題得到最終解決。

圖31

總結

本篇文章基本上描述了我們遇到的容器偶發性超時問題分析的大部分過程,但排障過程遠比寫出來要艱難。

總的原則還是大膽假設,小心求證,設法找到無規律中的規律性,保持細緻耐心的鑽研精神,相信這些疑難雜症終會被一一解決。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章