問題描述
今天下午運維反饋說我們這一個pod一天重啟了8次,需要排查下原因。一看Kiban日誌,jvm沒有丟擲過任何錯誤,服務就直接重啟了。顯然是程式被直接殺了,初步判斷是pod達到記憶體上限被K8s oomkill了。
因為我們xmx和xsx設定的都是3G,而pod的記憶體上限設定的是6G,所以出現這種情況還挺詭異的。
排查過程
初步定位
先找運維拉了一下pod的描述,關鍵資訊在這裡
Containers:
container-prod--:
Container ID: --
Image: --
Image ID: docker-pullable://--
Port: 8080/TCP
Host Port: 0/TCP
State: Running
Started: Fri, 05 Jan 2024 11:40:01 +0800
Last State: Terminated
Reason: Error
Exit Code: 137
Started: Fri, 05 Jan 2024 11:27:38 +0800
Finished: Fri, 05 Jan 2024 11:39:58 +0800
Ready: True
Restart Count: 8
Limits:
cpu: 8
memory: 6Gi
Requests:
cpu: 100m
memory: 512Mi
- 可以看到Last State:Terminated,Exit Code: 137。這個錯誤碼錶示的是pod程式被SIGKILL給殺掉了。一般情況下是因為pod達到記憶體上限被k8s殺了。
因此得出結論是生產環境暫時先擴大下pod的記憶體限制,讓服務穩住。然後再排查為啥pod裡會有這麼多的堆外記憶體佔用。
進一步分析
但是運維反饋說無法再擴大pod的記憶體限制,因為宿主機的記憶體已經佔到了99%了。
然後結合pod的記憶體監控,發現pod被殺前的記憶體佔用只到4G左右,沒有達到上限的6G,pod就被kill掉了。
於是問題就來了,為啥pod沒有達到記憶體上限就被kill了呢。
帶著疑問,我開始在google裡尋找答案,也發現了一些端倪:
- 如果是pod記憶體達到上限被kill,pod的描述裡會寫Exit Code: 137,但是Reason不是Error,而是OOMKilled
- 宿主機記憶體已經吃滿,會觸發k8s的保護機制,開始evict一些pod來釋放資源
- 但是為什麼整個叢集裡,只有這個pod被反覆evict,其他服務沒有影響?
謎題解開
最終還是google給出了答案:
Why my pod gets OOMKill (exit code 137) without reaching threshold of requested memory
連結裡的作者遇到了和我一樣的情況,pod還沒吃到記憶體上限就被殺了,而且也是:
Last State: Terminated
Reason: Error
Exit Code: 137
作者最終定位的原因是因為k8s的QoS機制,在宿主機資源耗盡的時候,會按照QoS機制的優先順序,去殺掉pod來釋放資源。
什麼是k8s的QoS?
QoS,指的是Quality of Service,也就是k8s用來標記各個pod對於資源使用情況的質量,QoS會直接影響當節點資源耗盡的時候k8s對pod進行evict的決策。官方的描述在這裡.
k8s會以pod的描述檔案裡的資源限制,對pod進行分級:
QoS | 條件 |
---|---|
Guaranteed | 1. pod裡所有的容器都必須設定cpu和記憶體的request和limit,2. pod裡所有容器設定的cpu和記憶體的request和容器設定的limit必須相等(容器自身相等,不同容器可以不等) |
Burstable | 1. pod並不滿足Guaranteed的條件,2. 至少有一個容器設定了cpu或者記憶體的request或者limit |
BestEffort | pod裡的所有容器,都沒有設定任何資源的request和limit |
當節點資源耗盡的時候,k8s會按照BestEffort->Burstable->Guaranteed這樣的優先順序去選擇殺死pod去釋放資源。
從上面運維給我們的pod描述可以看到,這個pod的資源限制是這樣的:
Limits:
cpu: 8
memory: 6Gi
Requests:
cpu: 100m
memory: 512Mi
顯然符合的是Burstable的標準,所以宿主機記憶體耗盡的情況下,如果其他服務都是Guaranteed,那自然會一直殺死這個pod來釋放資源,哪怕pod本身並沒有達到6G的記憶體上限。
QoS相同的情況下,按照什麼優先順序去Evict?
但是和運維溝通了一下,我們叢集內所有pod的配置,limit和request都是不一樣的,也就是說,大家都是Burstable。所以為什麼其他pod沒有被evict,只有這個pod被反覆evict呢?
QoS相同的情況,肯定還是會有evict的優先順序的,只是需要我們再去尋找下官方文件。
關於Node資源耗盡時候的Evict機制,官方文件有很詳細的描述。
其中最關鍵的一段是這個:
If the kubelet can't reclaim memory before a node experiences OOM, the
oom_killer
calculates anoom_score
based on the percentage of memory it's using on the node, and then adds theoom_score_adj
to get an effectiveoom_score
for each container. It then kills the container with the highest score.This means that containers in low QoS pods that consume a large amount of memory relative to their scheduling requests are killed first.
簡單來說就是pod evict的標準來自oom_score,每個pod都會被計算出來一個oom_score,而oom_score的計算方式是:pod使用的記憶體佔總記憶體的比例加上pod的oom_score_adj值。
oom_score_adj的值是k8s基於QoS計算出來的一個偏移值,計算方法:
QoS | oom_score_adj |
---|---|
Guaranteed | -997 |
BestEffort | 1000 |
Burstable | min(max(2, 1000 - (1000 × memoryRequestBytes) / machineMemoryCapacityBytes), 999) |
從這個表格可以看出:
- 首先是BestEffort->Burstable->Guaranteed這樣的一個整體的優先順序
- 然後都是Burstable的時候,pod實際佔用記憶體/pod的request記憶體比例最高的,會被優先Evict
總結
至此已經可以基本上定位出Pod被反覆重啟的原因了:
- k8s節點宿主機記憶體佔用滿了,觸發了Node-pressure Eviction
- 按照Node-pressure Eviction的優先順序,k8s選擇了oom_score最高的pod去evict
- 由於所有pod都是Burstable,並且設定的request memery都是一樣的512M,因此記憶體佔用最多的pod計算出來的oom_score就是最高的
- 所有pod中,這個服務的記憶體佔用一直都是最高的,所以每次計算出來,最後都是殺死這個pod
那麼如何解決呢?
- 宿主機記憶體擴容,不然殺死pod這樣的事情無法避免,無非就是殺哪個的問題
- 對於關鍵服務的pod,要把request和limit設定為完全一致,讓pod的QoS置為Guaranteed,儘可能降低pod被殺的機率