Kubernetes源碼探疑:Pod IP泄露排查及解決

UK8S是UCloud推出的Kubernetes容器雲產品,徹底兼容原生API,爲用戶提供一站式雲上Kubernetes服務。咱們團隊自研了CNI(Container Network Interface)網絡插件,深度集成VPC,使UK8S容器應用擁有與雲主機間等同的網絡性能(目前最高可達10Gb/s, 100萬pps),並打通容器和物理雲/託管雲的網絡。過程當中,咱們解決了開源kubelet建立多餘Sandbox容器致使Pod IP莫名消失的問題,確保CNI插件正常運行,並準備將修復後的kubelet源碼提交給社區。docker

深度集成VPC的網絡方案api

按照咱們的設想,開發者能夠在UK8S上部署、管理、擴展容器化應用,無需關心Kubernetes集羣自身的搭建及維護等運維類工做。UK8S徹底兼容原生的Kubernetes API, 以UCloud 公有云資源爲基礎, 經過自研的插件整合打通了ULB、UDisk、EIP等公有云網絡和存儲產品,爲用戶提供一站式雲上Kubernetes服務。數組

其中VPC既保障網絡隔離,又提供靈活的IP地址定義等,是用戶對網絡的必備需求之一。UK8S研發團隊通過考察後認爲,UCloud基礎網絡平臺具備原生、強大的底層網絡控制能力,令咱們能拋開Overlay方案,把VPC的能力上移到容器這一層,經過VPC的能力去實現控制和轉發。 UK8S每建立一個Pod都爲其申請一個VPC IP並經過VethPair配置到Pod上,再配置策略路由。 原理以下圖所示。緩存

此方案具備如下優點:微信

無Overlay,網絡性能高。50臺Node下的測試數據代表,容器與容器之間的網絡性能,相對於雲主機與雲主機之間,只有輕微差別(小包場景下,pps 會有 3~5% 損耗),並且Pod網絡性能各項指標(吞吐量,包量,延遲等)不會隨着節點規模增大而削減。而Flannel UDP,VXLan模式和Calico IPIP的模式存在明顯的性能消耗。 Pod能直通公有云和物理雲。對於使用公有云和物理雲的用戶而言,業務上K8S少了一層障礙,多了一份便利。而Flannel的host gw模式下,容器沒法訪問公有云和物理雲主機。 而CNI的工做流程以下所示。網絡

建立Pod網絡過程:運維

刪除Pod網絡過程:函數

Pod IP 消失問題的排查與解決oop

爲了測試CNI插件的穩定性,測試同窗在UK8S上部署了一個CronJob,每分鐘運行一個Job任務,一天要運行1440個任務。該CronJob定義以下:性能

apiVersion: batch/v1beta1 kind: CronJob metadata: name: hello spec: schedule: "*/1 * * * *" jobTemplate: spec: template: spec: containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster restartPolicy: OnFailure

每運行一次Job都要建立一個Pod, 每建立一個Pod,CNI插件須要申請一次VPC IP,當Pod被銷燬時,CNI插件須要釋放該VPC IP。 所以理論上,經過該CronJob天天須要進行1440次申請VPC IP和釋放VPC IP操做。

然而,通過數天的測試統計,發現經過該CronJob,集羣天天申請IP次數高達2500以上, 而釋放的的IP次數也達到了1800。申請和釋放次數都超過了1440,並且申請次數超過了釋放次數,意味着,部分分配給Pod的VPC IP被無效佔用而消失了。

CNI:待刪除的IP去哪兒了?

仔細分析CNI插件的運行日誌,很快發現,CNI在執行拆除SandBox網絡動做(CNI_COMMAND=DEL)中,存在很多沒法找到Pod IP的狀況。因爲UK8S 自研的CNI查找Pod IP依賴正確的Pod網絡名稱空間路徑(格式:/proc/10001/net/ns),而kubelet傳給CNI的NETNS環境變量參數爲空字符串,所以,CNI沒法獲取待釋放的VPC IP,這是形成IP泄露的直接緣由,以下圖所示。

問題轉移到kubelet, 爲何kubelet會傳入一個空的CNI_NETNS環境變量參數給CNI插件?

隨後跟蹤kubelet的運行日誌,發現很多Job Pod建立和銷燬的時候,生成了一個額外的Sandbox容器。Sandbox容器是k8s pod中的Infra容器,它是Pod中第一個建立出來的容器,用於建立Pod的網絡名稱空間和初始化Pod網絡,例如調用CNI分配Pod IP,下發策略路由等。它執行一個名爲pause的進程,這個進程絕大部分時間處於Sleep狀態,對系統資源消耗極低。奇怪的是,當任務容器busybox運行結束後,kubelet爲Pod又建立了一個新的Sandbox容器,建立過程當中天然又進行了一次CNI ADD調用,再次申請了一次VPC IP。

回到UK8S CNI,咱們再次分析重現案例日誌。這一次有了更進一步的發現,全部kubelet傳遞給NETNS參數爲空字符串的情形都發生在kubelet試圖銷燬Pod中第二個Sandbox的過程當中。反之,kubelet試圖銷燬第二個Sandbox時,給CNI傳入的NETNS參數也所有爲空字符串。

到這裏,思路彷佛清晰了很多,全部泄露的VPC IP都是來自第二個Sandbox容器。所以,咱們須要查清楚兩個問題:

  1. 爲何會出現第二個Sandbox容器?

  2. 爲何kubelet在銷燬第二個Sandbox容器時,給CNI傳入了不正確的NETNS參數?

第二個Sandbox:我爲什麼而生?

在瞭解的第二個Sandbox的前世此生以前,須要先交待一下kubelet運行的基本原理和流程。

kubelet是kubernetes集羣中Node節點的工做進程。當一個Pod被kube-sheduler成功調度到Node節點上後, kubelet負責將這個Pod建立出來,並把它所定義的各個容器啓動起來。kubelet也是按照控制器模式工做的,它的工做核心是一個控制循環,源碼中稱之爲syncLoop,這個循環關注並處理如下事件:

Pod更新事件,源自API Server; Pod生命週期(PLEG)變化, 源自Pod自己容器狀態變化, 例如容器的建立,開始運行,和結束運行; kubelet自己設置的週期同步(Sync)任務; Pod存活探測(LivenessProbe)失敗事件; 定時的清理事件(HouseKeeping)。 在上文描述的CronJob任務中, 每次運行Job任務都會建立一個Pod。這個Pod的生命週期中,理想狀況下,須要經歷如下重要事件:

  1. Pod被成功調度到某個工做節點,節點上的Kubelet經過Watch APIServer感知到建立Pod事件,開始建立Pod流程;

  2. kubelet爲Pod建立Sandbox容器,用於建立Pod網絡名稱空間和調用CNI插件初始化Pod網絡,Sandbox容器啓動後,會觸發第一次kubelet PLEG(Pod Life Event Generator)事件。

  3. 主容器建立並啓動,觸發第二次PLEG事件。

  4. 主容器date命令運行結束,容器終止,觸發第三次PLEG事件。

  5. kubelet殺死Pod中殘餘的Sandbox容器。

  6. Sandbox容器被殺死,觸發第四次PLEG事件。

其中3和4因爲時間間隔短暫,可能被歸併到同一次PLEG事件(kubelet每隔1s進行一次PLEG事件更新)。

然而,在咱們觀察到的全部VPC IP泄露的狀況中,過程6以後「意外地」建立了Pod的第二個Sandbox容器,以下圖右下角所示。在咱們對Kubernetes的認知中,這不該該發生。

對kubelet源碼(1.13.1)抽絲剝繭

前文提到,syncLoop循環會監聽PLEG事件變化並處理之。而PLEG事件,則來源kubelet內部的一個pleg relist定時任務。kubelet每隔一秒鐘執行一次relist操做,及時獲取容器的建立,啓動,容器,刪除事件。

relist的主要責任是經過CRI來獲取Pod中全部容器的實時狀態,這裏的容器被區分紅兩大類:Sandbox容器和非Sandbox容器,kubelet經過給容器打不一樣的label來識別之。CRI是一個統一的容器操做gRPC接口,kubelet對容器的操做,都要經過CRI請求來完成,而Docker,Rkt等容器項目則負責實現各自的CRI實現,Docker的實現即爲dockershim,dockershim負責將收到的CRI請求提取出來,翻譯成Docker API發給Docker Daemon。

relist經過CRI請求更新到Pod中Sandbox容器和非Sandbox容器最新狀態,而後將狀態信息寫入kubelet的緩存podCache中,若是有容器狀態發生變化,則經過pleg channel通知到syncLoop循環。對於單個pod,podCache分配了兩個數組,分別用於保存Sandbox容器和非Sandbox容器的最新狀態。

syncLoop收到pleg channel傳來事件後,進入相應的sync同步處理流程。對於PLEG事件來講,對應的處理函數是HandlePodSyncs。這個函數開啓一個新的pod worker goroutine,獲取pod最新的podCache信息,而後進入真正的同步操做:syncPod函數。

syncPod將podCache中的pod最新狀態信息(podStatus)轉化成Kubernetes API PodStatus結構。這裏值得一提的是,syncPod會經過podCache裏各個容器的狀態,來計算出Pod的狀態(getPhase函數),好比Running,Failed或者Completed。而後進入Pod容器運行時同步操做:SyncPod函數,即將當前的各個容器狀態與Pod API定義的SPEC指望狀態作同步。下面源碼流程圖能夠總結上述流程。

SyncPod:我作錯了什麼?

SyncPod首先計算Pod中全部容器的當前狀態與該Pod API指望狀態作對比同步。這一對比同步分爲兩個部分:

檢查podCache中的Sandbox容器的狀態是否知足此條件:Pod中有且只有一個Sandbox容器,而且該容器處於運行狀態,擁有IP。如不知足,則認爲該Pod須要重建Sandbox容器。若是須要重建Sandbox容器,Pod內全部容器都須要銷燬並重建。 檢查podCache中非Sandbox容器的運行狀態,保證這些容器處於Pod API Spec指望狀態。例如,若是發現有容器主進程退出且返回碼不爲0,則根據Pod API Spec中的RestartPolicy來決定是否重建該容器。 回顧前面提到的關鍵線索:全部的VPC IP泄露事件,都源於一個意料以外的Sandbox容器,被泄露的IP即爲此Sandbox容器的IP。剛纔提到,SyncPod函數中會對Pod是否須要重建Sandbox容器進行斷定,這個意外的第二個Sandbox容器是否和此次斷定有關呢? 憑kubelet的運行日誌沒法證明該猜想,必須修改源碼增長日誌輸出。從新編譯kubelet後,發現第二個Sandbox容器確實來自SyncPod函數中的斷定結果。進一步確認的是,該SyncPod調用是由第一個Sandbox容器被kubelet所殺而致使的PLEG觸發的。

那爲何SyncPod在第一個Sandbox容器被銷燬後認爲Pod須要重建Sandbox容器呢?進入斷定函數podSandboxChanged仔細分析。

podSandboxChanged獲取了podCache中Sandbox容器結構體實例,發現第一個Sandbox已經被銷燬,處於NOT READY狀態,因而認爲pod中已無可用的Sandbox容器,須要重建之,源碼以下圖所示。

注意本文前面咱們定位的CronJob yaml配置, Job模板裏的restartPolicy被設置成了OnFailure。SyncPod完成Sandbox容器狀態檢查斷定後,認爲該Pod須要重建Sandbox容器,再次檢查Pod的restartPolicy爲OnFailure後,決定重建Sandbox容器,對應源碼以下。

能夠看出kubelet在第一個Sandbox容器死亡後觸發的SyncPod操做中,只是簡單地發現惟一的Sandbox容器處於NOT READY狀態,便認爲Pod須要重建Sandbox,忽視了Job的主容器已經成功結束的事實。

事實上,在前面syncPod函數中經過podCache計算API PodStatus Phase的過程當中,kubelet已經知道該Pod處於Completed狀態並存入apiPodStatus變量中做爲參數傳遞給SyncPod函數。以下圖所示。

Job已經進入Completed狀態,此時不該該重建Sandbox容器。而SyncPod函數在斷定Sandbox是否須要重建時, 並無參考調用者syncPod傳入的apiPodStatus參數,甚至這個參數是被忽視的。

第二個Sandbox容器的來源已經水落石出,解決辦法也很是簡單,即kubelet不爲已經Completed的Pod建立Sandbox,具體代碼以下所示。

從新編譯kubelet並更新後,VPC IP泄露的問題獲得解決。

下圖能夠總結上面描述的第二個Sandbox容器誕生的緣由。

事情離真相大白還有一段距離。還有一個問題須要回答:

爲何kubelet在刪除第二個Sandbox容器的時候, 調用CNI拆除容器網絡時,傳入了不正確的NETNS環境變量參數?

失去的NETNS

還記得前面介紹kubelet工做核心循環syncLoop的時候,裏面提到的按期清理事件(HouseKeeping)嗎?HouseKeeping是一個每隔2s運行一次的定時任務,負責掃描清理孤兒Pod,刪除其殘餘的Volume目錄並中止該Pod所屬的Pod worker goroutine。HouseKeeping發現Job Pod進入Completed狀態後,會查找該Pod是否還有正在運行的殘餘容器,若有則請理之。因爲第二個Sandbox容器依然在運行,所以HouseKeeping會將其清理,其中的一個步驟是清理該Pod所屬的cgroup,殺死該group中的全部進程,這樣第二個Sandbox容器裏的pause進程被殺,容器退出。

已經死亡的第二個Sandbox容器會被kubelet裏的垃圾回收循環接管,它將被完全中止銷燬。然而因爲以前的Housekeeping操做已經銷燬了該容器的cgroup, 網絡名稱空間不復存在,所以在調用CNI插件拆除Sandbox網絡時,kubelet沒法得到正確的NETNS參數傳給CNI,只能傳入空字符串。

到此,問題的緣由已經確認。

問題解決

一切水落石出後,咱們開始着手解決問題。爲了能確保找到所刪除的Pod對應的VPC IP,CNI須要在ADD操做成功後,將PodName,Sandbox容器ID,NameSpace,VPC IP等對應關聯信息進行額外存儲。這樣當進入DEL操做後,只須要經過kubelet傳入的PodName,Sandbox容器ID和NameSpace便可找到VPC IP,而後經過UCloud 公有云相關API刪除之,無需依賴NETNS操做。

考慮到問題的根因是出如今kubelet源碼中的SyncPod函數,UK8S團隊也已修復kubelet相關源碼並準備提交patch給Kubernetes社區。

寫在最後

Kubernetes依然是一個高速迭代中的開源項目,生產環境中會不可用避免碰見一些異常現象。UK8S研發團隊在學習理解Kubernetes各個組件運行原理的同時,積極根據現網異常現象深刻源碼逐步探索出問題根因,進一步保障UK8S服務的穩定性和可靠性,提高產品體驗。

2019年內UK8S還將支持節點彈性伸縮(Cluster AutoScaler)、物理機資源、GPU資源、混合雲和ServiceMesh等一系列特性,敬請期待。

歡迎掃描下方二維碼,加入UCloud K8S技術交流羣,和咱們共同探討Kubernetes前沿技術。

如顯示羣人數已加滿,可添加羣主微信zhaoqi628543,備註K8S便可邀請入羣。

相關文章
相關標籤/搜索