Openshift Master資源不足引發的業務Pod漂移重啟排查


背景

容器雲出現大量業務接口訪問失敗告警,觀察到批量業務Pod狀態變成MatchNodeSelector狀態,同時調度生成新的Pod,由於目前未完全推廣使用Pod優雅退出方案,在舊pod中的容器被刪除,新pod創建起來的過錯中就必然會導致交易丟失了。這次事件中我們觀察到的現象是:

0、監控發現三個Master節點cpu和內存高使用率告警

1、多個Master節點負載高,一段時間內apiserver出現無法處理請求的現象

2、監控發現大量Sync pod重啟

3、大量計算節點Node(kubelet)服務重啟

4、監控發現大量業務pod狀態變為MatchNodeSelector

問題環境:Openshift 3.11.43(Kubernetes 1.11.0)

 

問題點

出現上述事件之后第一反應是apiserver接收到大量外部請求,因為確實有部分系統會通過sa token來調用我們容器雲apiserver的接口(比如獲取已部署的服務等),由於我們容器雲的管理域名和應用域名都會經過一層haproxy進行轉發(其中8443端口的管理流量轉發到三個master,80/443端口的業務流量轉發到openshift router,使用keepalived做高可用),在這之前已經在prometheus上配置部署好了haproxy的監控,查看grafana監控面板可以看到在故障時間段master具有明顯的流量峰值,並且三個master出現了逐漸重啟的現象,這里遺憾的是生產環境並沒有部署apiServer監控(可以更加清楚的看到是哪些客戶端請求apiServer)。

但是這里卻帶來了很多無法解釋的問題:

1、從各種監控信息來看,只有master節點出現了資源緊張的問題,所有計算節點負載都是很正常的,那計算節點上面的業務pod為什么發生MatchNodeSelector現象。

2、為什么會有大量Sync Pod中的container發生restart,前期只知道Sync會同步openshift-node下面的configmap到節點上面,還有什么其他作用嗎?

3、master節點在事件期間為什么可以看到apiserver有突發的流量?

4、Node服務在這里發生重啟了,重啟會影響節點上面的業務Pod嗎?

 

排查

我們的排查的目標是把上述各個現象串起來形成一個完整的鏈路,這樣給出對應的優化解決方案也就簡單明了了

 

Openshift中openshift-node ns下sync pod的作用描述

該pod直接運行一段腳本,可直接在github openshift-tools sync 查看,改腳本邏輯比較簡單,可以簡單描述為如下過程:

1、檢查/etc/origin/node/node-config.yam即node服務配置文件是否存在,是則計算md5值寫到/tmp/.old
2、檢查/etc/sysconfig/atomic-openshift-node文件中是否配置BOOTSTRAP_CONFIG_NAME參數,該參數表示當前節點在openshift-node ns下的configmap名稱(ansible安裝集群時指定)
3、執行一段后台腳本不斷檢測2中的參數是否發生變化,是則exit 0退出自己以讓pod中container重啟
4、訪問apiserver取出2中指定的conigmap內容,寫入/etc/origin/node/tmp/node-config.yaml,並計算md5值寫入/tmp/.new
5、如果/tmp/.new中的內容和/tmp/.old中的內容不一致,則把/etc/origin/node/tmp/node-config.yaml文件覆蓋到/etc/origin/node/node-config.yaml,並執行6
6、提取/etc/origin/node/node-config.yaml中的node-labels參數並其重新強制刷新到etcd(oc label),如果成功則kill kubelet進程以Node服務重啟
7、使用oc annotate為node對象添加一個annotations為新配置文件的md5值
8、使用cp命令覆蓋/tmp/.new到/tmp/.old
9、重復執行4、5、6、7、8

我們發現上面腳本在configmap中的內容與節點本地配置不一致時,確實是會重啟kubelet進程,但是生產環境不應該存在configmap和節點本地配置被修改的可能。再次分析腳本,發現腳本中存在兩個潛在的問題:

1、md5sum命令對文件求md5值結果是帶文件名稱的,根據上述過程,/tmp/.old和/tmp/.new在首次比較時,內容分別如下

[root@k8s-master ~]# md5sum /etc/origin/node/node-config.yaml 
6de9439834c9147569741d3c9c9fc010  /etc/origin/node/node-config.yaml
[root@k8s-master ~]# md5sum /etc/origin/node/tmp/node-config.yaml 
6de9439834c9147569741d3c9c9fc010  /etc/origin/node/tmp/node-config.yaml

if [[ "$( cat /tmp/.old )" != "$( cat /tmp/.new )" ]]; then
  mv /etc/origin/node/tmp/node-config.yaml /etc/origin/node/node-config.yaml
  echo "info: Configuration changed, restarting kubelet" 2>&1
  if ! pkill -U 0 -f '(^|/)hyperkube kubelet '; then
    echo "error: Unable to restart Kubelet" 2>&1
    sleep 10 &
    wait $!
    continue
  fi
fi

這樣直接比較,即使配置內容是一致的,也會重啟kubelet服務了,由於每次都會把/tmp/.new覆蓋到/tmp/.old,之后的比較則不會判斷為配置不同,但這里帶來的問題就是sync pod在啟動之后,必然會重啟Node服務即kubelet服務一次,這樣就能把我們的現象2和現象3聯系起來了。

2、腳本在開頭位置設置了set -euo pipefail屬性,其中

-e表示在腳本中某個獨立的命令出現錯誤(exit)時馬上退出,后續命令不再執行(默認繼續執行)

-u表示所有未定義的變量被認為是錯誤(默認是視為空值)

-o pipefail表示多個命令通過管道連接時,所有命令都正常(exit 0)才認為最后結果是正常(默認是最后一個命令的退出碼作為整體退出碼)

上述sync pod執行過程描述7中使用如下命令請求apiserver為node資源對象打上注解annotation,這個命令是獨立的一個shell語句,如果這個時候apiserver不可用,那么這個命令將會以非零狀態碼退出,由於set -e的存在,腳本作為容器主進程將退出,也即容器會發生容器,這樣就能把我們的現象1和現象2聯系起來了。

#If this command failed, sync pod will restart.           
oc annotate --config=/etc/origin/node/node.kubeconfig "node/${NODE_NAME}" \
              node.openshift.io/md5sum="$( cat /tmp/.new | cut -d' ' -f1 )" --overwrite

 

上述關於sync pod腳本存在的兩個潛在問題相結合會導致apiserver出問題時kubelet服務重啟(我這邊認為是可以避免且沒有什么益處的,於是提了redhat的問題case並等待其回復確認是否為bug),而大量kubelet服務重啟之后會向apiserver做List Pod的操作,似乎能解釋為什么故障期間apiserver有流量峰值的情況。

 

20200907:查詢到openshift v3.11.154中的sync pod腳本是已經修復了啟動會重啟Node服務的問題

function md5()
{
// 將命令執行結果用()括號括起來表示一個數組,下面echo數組第一個元素 local md5result
=($(md5sum $1)) echo md5result } md5 {file}

 

 

 

Kubelet的Admit機制

關於kubelet的admit機制,可以參考另一文檔:https://www.cnblogs.com/orchidzjl/p/14801278.html

在kubernetes項目的issue中Podstatus becomes MatchNodeSelector after restart kubelet關於MatchNodeSelector的討論跟我們的場景基本是一致的,大概過程為kubelet服務的重啟之后將會請求apiServer節點上面所有的Pod列表(新調度到節點上和正在節點上運行的pod),對列表中的每一個Pod進行Admit操作,這個Admit過程將會執行一系列類似scheduler中的預選策略,來判斷這些pod是否真正適合跑在我這個節點,其中有一個策略就是從apiserver獲取node資源對象,並判斷從node中的標簽是否滿足pod親和性,如果不滿足則該Pod會變成MatchNodeSelector,如果這個Pod是之前已經運行在當前節點上,那么這個Pod會被停止並重新生成調度。那為什么node標簽會不滿足pod的親和性呢,因為在kubelet向apiServer獲取最新的node對象時,如果apiServer不可用導致獲取失敗時,那么kubelet會通過本地配置文件直接生成一個initNode對象,這個node對象的標簽只有kubelet的--node-labels參數指定的標簽,那些通過api添加的標簽都不會出現在這個initNode對象上(我們的生產環境都是通過api額外添加的標簽作為pod的親和性配置),這樣就能把我們的現象3和現象4聯系起來了。

# 從Apiserver中獲取Node信息,如果失敗,則構造一個initNode
# pkg/kubelet/kubelet_getters.go
// getNodeAnyWay() must return a *v1.Node which is required by RunGeneralPredicates().
// The *v1.Node is obtained as follows:
// Return kubelet's nodeInfo for this node, except on error or if in standalone mode,
// in which case return a manufactured nodeInfo representing a node with no pods,
// zero capacity, and the default labels.
func (kl *Kubelet) getNodeAnyWay() (*v1.Node, error) {
    if kl.kubeClient != nil {
        if n, err := kl.nodeInfo.GetNodeInfo(string(kl.nodeName)); err == nil {
            return n, nil
        }
    }
    return kl.initialNode()
}

 

 

模擬MatchNodeSelector Pod的生成:通過restAPI(kc、oc等命令)給節點打上自定義標簽,給Pod所屬的deploy加上spec.nodeAffinity通過前面的標簽親和到節點,創建該deploy,pod成功部署到節點上。再把節點上那個標簽刪除,pod正常running在節點上,重啟節點的kubelet服務,pod變成MatchNodeSelector狀態並重新調度新pod。

 

Master節點資源配置

 參考openshift官方文檔關於master節點的資源配置需求以及相關的基准測試數據 scaling-performance-capacity-host-practices-master,擴容三個master的虛擬機配置,部署容器雲apiserver、etcd、controller-manager prometheus監控,根據監控信息優化各組件配置。

 

結論

這是一個由於master節點資源使用率高導致的apiserver不可用,進而導致openshift sync pod重啟,進而導致節點上的kubelet服務重啟,進而導致節點上面的業務pod發生MatchNodeSelector的現象。

 

優化

修改sync pod中shell的邏輯,讓sync pod只有在真正檢查到配置文件修改時才重啟節點上的kubelet服務

提高三個master節點的cpu和memory資源配置,調整apisever所能接受請求的並發數限制,觀察一段時間內apiserver的資源使用情況

部署容器雲apiserver、etcd、controller-manager prometheus監控,觀察相關的各種性能指標


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM