記錄一次K8s pod被殺的排查過程

su_xtf2009發表於2024-01-05

問題描述

今天下午運維反饋說我們這一個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 an oom_score based on the percentage of memory it's using on the node, and then adds the oom_score_adj to get an effective oom_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被殺的機率

相關文章