背景
容器雲出現大量業務接口訪問失敗告警,觀察到批量業務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監控,觀察相關的各種性能指標