使用prometheus來避免Kubernetes CPU Limits造成的事故

charlieroro發表於2023-01-30

使用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秒。

image

由於1000毫秒等同於1秒CPU執行時間,這就可以讓該應用每秒不受限地執行一個完整的CPU秒,實際的工作方式更加微妙。我們將一個CPU秒稱為一個週期(period),用來衡量時間塊。

Linux Accounting system

Limits是一個記賬系統(Accounting system),用於跟蹤和限制一個容器在固定時間週期內使用的總vCPU數,該值作為可用執行時的全域性池進行跟蹤,一個容器可以在該週期內使用該池。上面陳述中有很多內容,下面對此進行分析。

回到週期或記賬系統翻頁頻率的概念。我們需要跨多個 vCPU申請執行時間,這意味著需要將賬簿的每頁分為多個段,稱為切片。Linux核心預設會將一個週期分為20個切片。image

假設我們需要執行半個週期,此時只需要將配額配置為一半數目的切片即可,在一個週期之後,記賬系統會重置切片,並重啟程式。

image

類似於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。

image

Linux有一個配置,稱為cpu.cfs_period_us,設定了賬簿翻到下一頁前的時間,該值表示下一個週期建立前的微秒時間。這些Linux指標會透過cAdvisor轉換為prometheus指標。

撇開一些特殊場景不談,在賬簿翻頁之前經過的時間並不像被限制的 CPU時間切片那樣重要。

下面看下使用cpu.cfs_quota_us指標設定的容器配額,這裡配置為50毫秒,即100ms的一半:

image

多執行緒容器

容器通常具有多個處理執行緒,根據語言的不同,可能有數百個執行緒。

image

當這些執行緒/程式執行時,它們會排程不同的(可用)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的時間片,如果全域性配額中有足夠的時間片,則會啟動執行緒,否則,該執行緒會被抑制並等待下一個週期。

image

真實場景

下面假設一個實驗,假如有4個執行緒,每個執行緒需要100ms的CPU時間來完成一個任務,將所有所需的vCPU時間加起來,總計需要400ms或4000m,因此可以以此為程式配置limit來避免被抑制。

image

不幸的是,實際的負載並不是這樣的。這些函式的執行緒可能執行重的或輕的API呼叫。應用所需的CPU時間是變化的,因此不能將其認為是一個固定的值。再進一步,4個執行緒可能並不會同時各需要一個vCPU,有可能某些執行緒需要等待資料庫鎖或其他條件就緒。

正因為如此,負載往往會突然爆發,因此延遲並不總是能夠成為設定limits的候選因素。最新的一個特性--cpu.cfs_burst_us允許將部分未使用的配額由一個週期轉至下一個週期。

有趣的是,這並不是讓大多數客戶陷入麻煩的地方。假設我們只是猜測了應用程式和測試需求,並且1個 CPU 秒聽起來差不多是正確的。該容器的應用程式執行緒將分佈到4個 vCPU 上。這樣做的結果是將每個執行緒的全域性配額分為100ms/4或25ms 的執行時。

image

而實際的總配額為(100ms 的配額) * (4個執行緒)或400ms 的配額。在100毫秒的現實時間裡,所有執行緒有300毫秒處於空閒狀態。因此,這些執行緒總共被抑制了300毫秒。

Latency

下面從應用的角度看下這些影響。單執行緒應用需要100ms來完成一個任務,當設定的配額為100ms或1000 m/1000 m = 100%,此時設定了一個合理的limits,且沒有抑制。

image

在第二個例子中,我們猜測錯誤,並將limits設定為400m或400 m/1000 m = 40%,此時的配額為100ms週期中的40ms。下圖展示該配置了對該應用的延遲:

image

此時處理相同請求的時間翻倍(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:

image

下面對該容器新增limits限制:

          resources:
            limits:
              cpu: 2000m
              memory: 128Mi

可以看到總的 CPU 使用在100ms 的現實時間中減少了一半,這正是我們所期望的。

image

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不會對應用的響應造成影響,但會造成資源浪費。

相關文章