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

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

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