使用prometheus來避免Kubernetes CPU Limits造成的事故
譯自:Using Prometheus to Avoid Disasters with Kubernetes CPU Limits
本文將介紹Kubernetes的resource limits是如何工作的、使用哪些metrics來設定正確的limits值、以及使用哪些指標來定位CPU抑制的問題。
將limits中的CPU解釋為時間概念,可以方便地理解容器中的多執行緒是如何使用CPU時間的。
理解Limits
在配置limits時,我們會告訴Linux節點在一個特定的週期內一個容器應用的執行時長。這樣做是為了保護節點上的其餘負載不受任意一組程式佔用過多 CPU 週期的影響。
limits的核並不是主機板上的物理核,而是配置了單個容器內的一組程式或執行緒在容器短暫暫停(避免影響到其他應用)前的執行時長。這句話有點違反直覺,特別是在 Kubernetes 排程器級別上很容易出錯,Kubernetes 排程器使用了物理核的概念。
kubernetes 排程器在執行排程的時候用的是節點上物理核的概念,但容器執行的時候,應該將limits配置的CPU 轉換為CPU時間的概念。
Limits其實是時間
下面使用一個虛構的例子來解釋這個概念。假設有一個單執行緒應用,該應用需要1秒CPU執行時間來完成一個事務,此時將limits配置為1 core或1000 millicores:
Resources:
limits:
cpu: 1000m
如果該應用需要完整的1秒CPU執行時間來服務一個API呼叫,中間不能被停止或抑制,即在容器被抑制前需要允許該應用執行1000毫秒(ms)或1 CPU秒。
由於1000毫秒等同於1秒CPU執行時間,這就可以讓該應用每秒不受限地執行一個完整的CPU秒,實際的工作方式更加微妙。我們將一個CPU秒稱為一個週期(period),用來衡量時間塊。
Linux Accounting system
Limits是一個記賬系統(Accounting system),用於跟蹤和限制一個容器在固定時間週期內使用的總vCPU數,該值作為可用執行時的全域性池進行跟蹤,一個容器可以在該週期內使用該池。上面陳述中有很多內容,下面對此進行分析。
回到週期或記賬系統翻頁頻率的概念。我們需要跨多個 vCPU申請執行時間,這意味著需要將賬簿的每頁分為多個段,稱為切片。Linux核心預設會將一個週期分為20個切片。
假設我們需要執行半個週期,此時只需要將配額配置為一半數目的切片即可,在一個週期之後,記賬系統會重置切片,並重啟程式。
類似於requests或shares可以轉換為表示 CPU 分配百分比的比率,也可以將limits轉換為一個百分比。例如,容器的配額設定為半個週期,則配置為:
resources:
limits:
cpu: 500m
開始時,使用1000 milliCPU作為一個完整的share。當配置500 milliCPU時,使用了半個週期,或500m/1000m = 50%。如果設定了200m/1000m,則表示使用的CPU比率為20%,以此類推。我們需要這些轉換數字來理解一些prometheus的指標輸出。
上面提到的記賬系統是按容器計算的,下面看下指標container_spec_cpu_period
,與我們假設的實驗不同,實際與容器相關的週期為100ms。
Linux有一個配置,稱為cpu.cfs_period_us
,設定了賬簿翻到下一頁前的時間,該值表示下一個週期建立前的微秒時間。這些Linux指標會透過cAdvisor轉換為prometheus指標。
撇開一些特殊場景不談,在賬簿翻頁之前經過的時間並不像被限制的 CPU時間切片那樣重要。
下面看下使用cpu.cfs_quota_us
指標設定的容器配額,這裡配置為50毫秒,即100ms的一半:
多執行緒容器
容器通常具有多個處理執行緒,根據語言的不同,可能有數百個執行緒。
當這些執行緒/程式執行時,它們會排程不同的(可用)vCPU,Linux的記賬系統需要全域性跟蹤誰在使用這些vCPU,以及需要將哪些內容新增到賬簿中。
先不談週期的概念,下面我們使用container_cpu_usage_seconds_total
來跟蹤一個應用的執行緒在1秒內使用的vCPU數。假設執行緒在4個 vCPU 上均執行了整整一秒鐘,則說明其使用了4個vCPU秒。
如果總的vCPU時間小於1個vCPU秒會發生什麼呢?此時會在該時間幀內抑制節點上該應用的其他執行緒的執行。
Global accounting
上面討論瞭如何將一個vCPU秒切分為多個片,然後就可以全域性地在多個vCPU上申請時間片。讓我們回到上述例子(4個執行緒執行在4個vCPU上),進一步理解它們如何執行的。
當一個CPU需要執行其佇列中的一個執行緒或程式時,它首先會確認容器的全域性配額中是否有5ms的時間片,如果全域性配額中有足夠的時間片,則會啟動執行緒,否則,該執行緒會被抑制並等待下一個週期。
真實場景
下面假設一個實驗,假如有4個執行緒,每個執行緒需要100ms的CPU時間來完成一個任務,將所有所需的vCPU時間加起來,總計需要400ms或4000m,因此可以以此為程式配置limit來避免被抑制。
不幸的是,實際的負載並不是這樣的。這些函式的執行緒可能執行重的或輕的API呼叫。應用所需的CPU時間是變化的,因此不能將其認為是一個固定的值。再進一步,4個執行緒可能並不會同時各需要一個vCPU,有可能某些執行緒需要等待資料庫鎖或其他條件就緒。
正因為如此,負載往往會突然爆發,因此延遲並不總是能夠成為設定limits的候選因素。最新的一個特性--cpu.cfs_burst_us允許將部分未使用的配額由一個週期轉至下一個週期。
有趣的是,這並不是讓大多數客戶陷入麻煩的地方。假設我們只是猜測了應用程式和測試需求,並且1個 CPU 秒聽起來差不多是正確的。該容器的應用程式執行緒將分佈到4個 vCPU 上。這樣做的結果是將每個執行緒的全域性配額分為100ms/4或25ms 的執行時。
而實際的總配額為(100ms 的配額) * (4個執行緒)或400ms 的配額。在100毫秒的現實時間裡,所有執行緒有300毫秒處於空閒狀態。因此,這些執行緒總共被抑制了300毫秒。
Latency
下面從應用的角度看下這些影響。單執行緒應用需要100ms來完成一個任務,當設定的配額為100ms或1000 m/1000 m = 100%,此時設定了一個合理的limits,且沒有抑制。
在第二個例子中,我們猜測錯誤,並將limits設定為400m或400 m/1000 m = 40%,此時的配額為100ms週期中的40ms。下圖展示該配置了對該應用的延遲:
此時處理相同請求的時間翻倍(220ms)。該應用在三個統計週期中的兩個週期內受到了抑制。在這兩個週期中,應用被抑制了60ms。更重要的是,如果沒有其他需要處理的執行緒,vCPU將會被浪費,這不僅僅會降低應用的處理速度,也會降低CPU的利用率。
與limits相關的最常見的指標container_cpu_cfs_throttled_periods_total
展示了被抑制的週期,container_cpu_cfs_periods_total
則給出了總的可用週期。上例中,三分之二(66%)的週期被抑制了。
那麼,如何知道limits應該增加多少呢?
Throttled seconds
幸運的是,cAdvisor提供了一個指標container_cpu_cfs_throttled_seconds_total
,它會累加所有被抑制的5ms時間片,並讓我們知道該程式超出配額的數量。指標的單位是秒,因此可以透過將該值除以10來獲得100ms(即我們設定的週期)。
透過如下表示式可以找出CPU使用超過100ms的前三個pods。
topk(3, max by (pod, container)(rate(container_cpu_usage_seconds_total{image!="", instance="$instance"}[$__rate_interval]))) / 10
下面做一個實驗:使用sysbench
啟動一個現實時間100ms中需要400ms CPU時間的的4執行緒應用。
command:
- sysbench
- cpu
- --threads=4
- --time=0
- run
可以觀測到使用了400ms的vCPU:
下面對該容器新增limits限制:
resources:
limits:
cpu: 2000m
memory: 128Mi
可以看到總的 CPU 使用在100ms 的現實時間中減少了一半,這正是我們所期望的。
PromQL 給出了每秒的抑制情況,每秒有10個週期(每個週期預設100ms)。為了得到每個週期的抑制情況,需要除以10。如果需要知道應該增加多少limits,則可以乘以10(如200ms * 10 = 2000m)。
topk(3, max by (pod, container)(rate(container_cpu_cfs_throttled_seconds_total{image!="", instance="$instance"}[$__rate_interval]))) / 10
總結
本文介紹了limits是如何工作的,以及可以使用哪些指標來設定正確的值,使用哪些指標來進行抑制型別的問題定位。本文的實驗提出了一個觀點,即過多地配置limits的vCPU數可能會導致vCPU處於idle狀態而造成應用響應延遲,但在現實的服務中,一般會包含語言自身runtime的執行緒(如go和java)以及開發者自己啟動的執行緒,一般設定較多的vCPU不會對應用的響應造成影響,但會造成資源浪費。