你應當小心設定k8s中負載的CPU limit,太小的值會給你的程序帶來額外的、無意義的延遲,太大的值會帶來過大的爆炸半徑,削弱集群的整體穩定性。
1.request和limit
k8s的一大好處就是資源隔離,通過設定負載的request和limit,我們可以方便地讓不同程序共存於合適的節點上。
其中,request是給調度看的,調度會確保節點上所有負載的CPU request合計與內存request合計分別都不大於節點本身能夠提供的CPU和內存,limit是給節點(kubelet)看的,節點會保證負載在節點上只使用這么多CPU和內存。例如,下面配置意味着單個負載會調度到一個剩余CPU request大於0.1核,剩余request內存大於200MB的節點,並且負載運行時的CPU使用率不能高於0.4核(超過將被限流),內存使用不多余300MB(超過將被OOM Kill並重啟)。
resources: requests: memory: 200Mi cpu: "0.1" limits: memory: 300Mi cpu: "0.4"
2.CPU的利用率
CPU和內存不一樣,它是量子化的,只有“使用中”和“空閑”兩個狀態。

當我們說內存的使用率是60%時,我們是在說內存有60%在空間上已被使用,還有40%的空間可以放入負載。但是,當我們說CPU的某個核的使用率是60%時,我們是在說采樣時間段內,CPU的這個核在時間上有60%的時間在忙,40%的時間在睡大覺。
你設定負載的CPU limit時,這個時空區別可能會帶來一個讓你意想不到的效果——過分的降速限流, 節點CPU明明不忙,但是節點故意不讓你的負載全速使用CPU,服務延時上升。
3.CPU限流
k8s使用CFS(Completely Fair Scheduler,完全公平調度)限制負載的CPU使用率,CFS本身的機制比較復雜,但是k8s的文檔中給了一個簡明的解釋,要點如下:
- CPU使用量的計量周期為100ms;
- CPU limit決定每計量周期(100ms)內容器可以使用的CPU時間的上限;
- 本周期內若容器的CPU時間用量達到上限,CPU限流開始,容器只能在下個周期繼續執行;
- 1 CPU = 100ms CPU時間每計量周期,以此類推,0.2 CPU = 20ms CPU時間每計量周期,2.5 CPU = 250ms CPU時間每計量周期;
- 如果程序用了多個核,CPU時間會累加統計。
舉個例子,假設一個API服務在響應請求時需要使用A, B兩個線程(2個核),分別使用60ms和80ms,其中B線程晚觸發20ms,我們看到API服務在100ms后可給出響應:

如果CPU limit被設為1核,即每100ms內最多使用100ms CPU時間,API服務的線程B會受到一次限流(灰色部分),服務在140ms后響應:

如果CPU limit被設為0.6核,即每100ms內最多使用60ms CPU時間,API服務的線程A會受到一次限流(灰色部分),線程B受到兩次限流,服務在220ms后響應:

注意,即使此時CPU沒有其他的工作要做,限流一樣會執行,這是個死板不通融的機制。
這是一個比較誇張的例子,一般的API服務是IO密集型的,CPU時間使用量沒那么大(你在跑模型推理?當我沒說),但還是可以看到,限流會實打實地延伸API服務的延時。因此,對於延時敏感的服務,我們都應該盡量避免觸發k8s的限流機制。
下面這張圖是我工作中一個API服務在pod級別的CPU使用率和CPU限流比率(CPU Throttling),我們看到,CPU限流的情況在一天內的大部分時候都存在,限流比例在10%上下浮動,這意味着服務的工作沒能全速完成,在速度上打了9折。值得一提,這時pod所在節點仍然有富余的CPU資源,節點的整體CPU使用率沒有超過50%.

你可能注意到,監控圖表里的CPU使用率看上去沒有達到CPU limit(橙色橫線),這是由於CPU使用率的統計周期(1min)太長造成的信號混疊(Aliasing),如果它的統計統計周期和CFS的一樣(100ms),我們就能看到高過CPU limit的尖刺了。(這不是bug,這是feature)
不過,內核版本低於4.18的Linux還真有個bug會造成不必要的CPU限流。
4.避免CPU限流
有的開發者傾向於完全棄用CPU limit,裸奔直接跑,特別是內核版本不夠有bug的時候。
我認為這么做還是太過放飛自我了,如果程序里有耗盡CPU的bug(例如死循環,我不幸地遇到過),整個節點及其負載都會陷入不可用的狀態,爆炸半徑太大,特別是在大號的節點上(16核及以上)。
我有兩個建議:
1.監控一段時間應用的CPU利用率,基於利用率設定一個合適的CPU limit(例如,日常利用率的95分位 * 10),同時該limit不要占到節點CPU核數的太大比例(例如2/3),這樣可以達到性能和安全的一個平衡。
2.使用automaxprocs一類的工具讓程序適配CFS調度環境,各個語言應該都有類似的庫或者執行參數,根據CFS的特點調整后,程序更不容易遇到CPU限流。
5.結語
上面說到的信號混疊(采樣頻率不足)和Linux內核bug讓我困擾了一年多,現在想想,主要還是望文生義惹的禍,文檔還是應該好好讀,基礎概念還是要搞清,遂記此文章於錯而知新。
題外話,性能和資源利用率有時是相互矛盾的。對於延時不敏感的程序,CPU限流率控制在10%以內應該都是比較健康可接受的,量體裁衣,在線離線負載混合部署,可以提升硬件的資源利用率。有消息說騰訊雲研發投產了基於服務優先級的搶占式調度,這是一條更難但更有效的路,希望有朝一日在上游能看到他們的相關貢獻。
參考資料
https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu
https://stackoverflow.com/questions/68846880/azure-kubernetes-cpu-multithreading
https://cloud.tencent.com/developer/article/1736729
引用鏈接
[1]CFS 本身的機制比較復雜: https://en.wikipedia.org/wiki/Completely_Fair_Scheduler
[2]簡明的解釋: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#how-pods-with-resource-limits-are-run
[3]信號混疊(Aliasing): https://en.wikipedia.org/wiki/Aliasing
[4]內核版本低於 4.18 的 Linux 還真有個 bug 會造成不必要的 CPU 限流: https://github.com/kubernetes/kubernetes/issues/67577#issuecomment-466609030
[5]完全棄用 CPU limit: https://amixr.io/blog/what-wed-do-to-save-from-the-well-known-k8s-incident/
[6]內核版本不夠有 bug 的時候: https://medium.com/omio-engineering/cpu-limits-and-aggressive-throttling-in-kubernetes-c5b20bd8a718
[7]automaxprocs: https://github.com/uber-go/automaxprocs
[8]程序更不容易遇到 CPU 限流: https://github.com/uber-go/automaxprocs/issues/12#issuecomment-405976401
[9]錯而知新: https://nanmu.me/zh-cn/categories/錯而知新/
[10]基於服務優先級的搶占式調度: https://cloud.tencent.com/developer/article/1876817
