Cloudpods容器化經驗分享
Cloudpods是一個開源的多云混合云管理平臺。Cloudpods首先是一個私有云云平臺,具備將計算節點使用開源QEMU/KVM虛擬化技術虛擬出虛擬機,實現私有云的功能。其次,Cloudpods能夠納管其他的云平臺,包括主流私有云和公有云,實現云管的功能。Cloudpods的目標是幫助用戶基于本地基礎設置以及已有云基礎設置,構建一個統一融合的云上之云,達到降低復雜度,提高管理效率的效果。Cloudpods從3.0開始全面擁抱Kubernetes,基于Kubernetes部署運行云平臺的服務組件,采用Kubernetes Operator,基于Kubernetes集群自動化部署服務,實現了云平臺的服務的容器化分布式部署。本文總結了Cloudpods在過去3年云平臺底層容器化改造的經驗。目前,將Kubernetes作為IAAS平臺的底層服務管理平臺是一個趨勢,例如OpenStack的Kolla項目,VMware的Tanzu,以及基于Kubernetes的虛擬化方案KubeVirt。Cloudpods順應此趨勢,早在2019年下半年開始基于Kubernetes構建Cloudpods的服務組件基礎設施。理論上,Cloudpods站在了巨人的肩膀上。有了Kubernetes的加持,我們基于Operator管理CRD(Custom Resource Definition)機制做到了更優雅的服務自動化部署,符合IaC(Infrastructure as code)實踐的服務升級和回滾,服務的自動高可用部署等等。但在實際效果上,我們基于Kubernetes,獲得了一些便利,但也遇到了不少未曾預料到的問題。本文介紹自從2019年3.0容器化改造以來,因為引入Kubernetes遇到的問題,我們的一些解決方案,以及將來的規劃。
1、容器化帶來了哪些好處
1)方便對分布于多個節點上的服務的管理
管理員可以在控制節點統一地查看運行在各個節點的服務狀態,查看日志,啟停和發布回滾服務,甚至exec進入服務容器排查問題。同時我們引入Loki收集所有容器的日志,可以統一地查看各個服務的日志。對分布式集群的運維和排障都變得相對簡單。采用Kubernetes之后,直接登錄各個節點排障的機會大大降低了。
2)集群配置變更更方便及可控
整個集群的狀態可以保存為一個OnecloudCluster yaml文件。可以方便地變更集群的配置,包括集群的版本,實現版本的升級和回退,以及集群服務的開啟和關閉,鏡像版本等關鍵參數的變更等。更進一步地,可以通過git進行配置yaml的版本控制,做到變更的歷史記錄審計,并且可以隨時恢復到任意指定的配置。
3)便于適配不同的CPU架構和操作系統
Kubernetes作為一層中間層,從一定程度上屏蔽了底層的差異。采用Kubernetes后,對CPU和操作系統的適配大概分為三部分工作:
1. Kubernetes對CPU和操作系統的適配;
2. 不同CPU架構下服務容器鏡像的構建;
3. Kubernetes之外的組件的適配,例如平臺依賴的rpm包等?;贙ubernetes自身強大的生態,基本都有現成的解決方案,只需要做相應的集成工作。只需通過docker buildx工具生成異構CPU架構的鏡像。因此,整個適配工作復雜度大大降低了。
4)部署的便利性增加
引入Kubernetes之后,整個部署流程分為幾個階段:
5)可復用Kubernetes本身自帶的強大功能
如coredns可以自定義域名,甚至可以做泛域名解析。Ingress自帶反向代理的功能。service+deployment提供的多副本冗余機制。daemonset提供的在新添加節點自動拉起服務的能力。對服務的資源限制(CPU,內存,進程號等)。這些都使得云平臺服務功能特性的實現變得更加容易。
2、容器化遇到了哪些問題,如何解決
下面總結一些遇到的問題。這些問題是我們在采用Kubernetes管理和運行云平臺組件中陸續發現的。有些已經徹底解決,但很大一部分還只是部分解決,徹底解決的方案還在持續摸索中。
1)容器內運行系統級服務
Cloudpods在計算節點運行的服務都是系統級的服務,如計算節點的核心服務hostagent,需具備幾個特權:
在容器化之前,這些服務由systemd管理,以root身份運行。這些特權都自然具備。容器化后,服務需運行在容器內。雖然可以通過配置給與容器系統級的root權限,但是一些特權操作在容器內依然無法執行。
首先,容器內無法啟動系統級daemon服務進程。如果通過容器內的程序啟動進程,則該進程只能運行在容器內的PID空間(pid namespace),只能跟隨容器的生命周期啟停。為了解決這個問題,我們將系統服務的二進制程序安裝在計算節點的底層操作系統,并且開發了一個命令執行代理executor-server。該代理安裝在底層操作系統,并作為一個系統服務運行。容器內的hostagent通過該代理執行系統級命令,例如啟動這些daemon服務,設置內核參數等,從而獲得了執行系統級命令的特權。
其次,每個容器具有自己獨立的文件系統命名空間(mount namespace)。為了允許容器內服務訪問計算節點底層系統的特定路徑文件,需要將該路徑顯式地掛載到容器的文件系統命名空間。例如,虛擬機的配置文件和本地磁盤文件都存儲在/opt/cloud/workspace目錄下。容器內的hostagent在虛擬機準備和配置階段需要能夠訪問這個目錄的文件,同時,啟動虛擬機后,在底層操作系統運行的虛擬機qemu進程也需要能夠訪問對應的文件。并且,由于上述命令執行代理的機制,為了簡化和保持向后兼容的目的,需要確保盡量以一致的路徑在容器內和容器外訪問這些文件。為此,我們將一些特定的系統目錄以同樣的路徑掛載到hostagent的容器內,例如系統設備文件路徑/dev,云平臺的配置文件路徑/etc/yunion,虛擬機系統文件路徑/opt/cloud/workspace等。然而,這個機制還無法解決容器內服務訪問底層系統任意路徑的問題。例如,用戶可以將底層系統的任意目錄設置為虛擬機磁盤的存儲目錄,但是該目錄其實并未通過容器的spec掛載到hostagent容器內,從而導致hostagent在容器內無法訪問該目錄。為了解決這個問題,我們對hostagent進行了改造。當hostagent檢測到用戶添加了新的本地目錄作為虛擬磁盤文件的存儲路徑,會自動地執行底層系統命令,將該路徑掛載到底層操作系統的/opt/cloud/workspace目錄下。因該目錄已經掛載到hostagent容器內,這樣hostagent就可以在容器內訪問這個目錄下的文件。
總之,相比將一個普通應用程序容器化,將系統級的服務程序從systemd托管變為在Kubernetes容器中運行,不是僅僅簡單地打一個容器鏡像,其實還需要做一系列比較復雜和繁雜的改造工作。
2)日志持久化
容器化之前,服務日志會記錄到journald中,并被持久化到/var/log/messages。按照CentOS的默認策略,保留最近一段時間的日志。遇到問題的時候,可以到對應服務器查找到對應的日志,排查錯誤原因。但是,不方便的地方是需要登錄到服務運行的節點查看日志。在一個事故涉及多個節點的時候,就需要同時登錄多個節點進行日志排查。
容器化之后,可以方便地在一個地方,通過kubectl log命令查看指定容器的日志,不需要登錄到服務運行的節點。
然而,如果沒有做特殊設置,K8s里的容器的日志都是不持久保存的,并且只保留當前正在運行的容器的最近一段時間的日志。而容器往往非常動態,很容易刪除。這就導致遇到問題需要排查已經被刪除的容器時候,容易遇到找不到對應的日志。這就使得追溯問題變得比較困難。
我們的解決方案是從3.7開始,會默認在k8s集群里部署Loki套件來收集容器的日志,日志最后存在minio的S3 Bucket 里面。這樣做能夠持久化容器的日志。解決上述問題。但是,保存Loki日志有一定的系統負載,并且需要較大容量的存儲空間。在集群容量緊張的情況下成為平臺的額外負擔,可能造成平臺的不穩定。
3)節點Eviction機制
Kubernetes有驅逐機制(Evict)。當節點的資源余量不足時,例如磁盤剩余空間低于閾值或剩余內存低于閾值(默認根分區磁盤空間低于85%,空閑內存低于500M)等,會觸發Kubernetes的節點驅逐機制,將該節點設置為不可調度,上面的所有容器都設置為Evict狀態,停止運行。
該機制對于無狀態應用可以動態地規避有問題的節點,是一個好的特性。然而,在云平臺的場景中,甚至對于普遍的有狀態服務場景中,Eviction機制導致節點可用性變得非常動態,進而降低了整體的穩定性。例如,由于用戶上傳一個大的鏡像,導致控制節點根分區利用率超過Eviction的閾值85%,云平臺的所有控制服務就會被立即驅除,導致云平臺控制平面完全不可用。用戶在虛擬機磁盤寫入大量數據導致宿主機磁盤空間利用率超過閾值,也會引起計算節點上所有服務被驅逐,進而導致這臺計算節點上所有的虛擬機失聯,無法控制??梢钥吹剑m然觸發Eviction機制的問題存在造成服務問題的可能,但是這些問題對服務的影響是延后的,逐步生效的。Eviction機制則使得這些潛在風險對服務的影響提前了,并立即發生,起到了放大的作用。
為了避免Eviction機制生效,云平臺在計算節點的agent啟動的時候,會自動檢測該節點的Eviction閾值,并設置為計算節點的資源申請上限。云平臺在調度主機的時候,會考慮到Eviction的閾值,避免資源分配觸發Eviction。這個機制能從一定程度規避Eviction的出現,但云平臺只能管理由云平臺分配的資源,還是存不在云平臺管理范圍內的存儲和內存分配導致Eviction的情況。因此需要計算節點一定程度的內存和存儲的over-provisioning。
目前,Eviction的存在也有一定的積極作用,那就是讓節點資源的不足以云平臺罷工的方式提出警示。由于云平臺的冗余設計,云平臺的暫時罷工并不會影響虛擬機的運行,因此影響程度還比較可控。無論如何,以云平臺可用性的犧牲來達到資源不足的警示,代價還是有點大。這樣的警示可以其他更柔和的方式來實現。隨著云平臺自身管理資源容量能力的完善,Eviction機制應該可以去除。
4)容器內進程泄露
Cloudpods服務主要為go開發的應用程序,容器鏡像采用alpine基礎鏡像最小化構建,僅包含服務的二進制和alpine基礎鏡像,服務進程作為容器的啟動進程(1號進程)運行。我們的服務程序沒有為作為1號進程做專門的優化,因此不具備systemd/init等正常操作系統1號進程具備的進程管理能力,例如處理孤兒進程,回收zombie進程等。然而,一些服務存在fork子進程的場景,例如kubeserver調用服務的時候會fork ssh執行遠程命令,cloudmon則會執行采集監控數據的子進程。當這些子進程遇到異常退出時,由于我們的服務進程不具備主動回收子進程的功能,導致系統里積壓了了大量退出異常未回收的子進程,導致進程泄露。這些子進程占用操作系統進程號,當達到系統最大進程數時,會出現系統CPU和內存非??臻e,但是無法進一步fork新的進程的情況,導致系統服務異常。
為了避免容器內進程泄露問題,我們在Cloudpods服務框架里加入了回收子進程的邏輯,并且添加到每個服務進程中,這樣在子進程異常退出后,我們的服務進程會回收子進程資源,從而避免了這個問題。同時也配置了kubelet的 最大進程數的限制參數,限制一個pod里面最多能有1024個進程。
5)高可用不一定高可用
我們基于Kubernetes實現了控制節點的3節點高可用,基本思路是使用3個節點部署高可用的Kubernetes的控制服務,包括apiserver, scheduler, controller, etcd等。Kubernetes服務通過VIP訪問。采用keepalived實現VIP在三個控制節點上的自動漂移。這此高可用Kubernetes集群之上,部署云平臺控制服務,實現云平臺控制平面的高可用。預期效果是將3個控制節點中的任意節點宕機后,主要服務不受影響,如果有影響,需能夠在短時間內自動恢復。
然而,初期測試發現采用默認參數部署的 Kubernetes 高可用自動恢復的時間高達15分鐘,不符合預期。經過調研發現,可以通過給各個組件設置相關的參數來減少恢復時間(https://github.com/yunionio/ocadm/pull/39/files)。經過參數調整,可以讓Kubernetes集群高可用切換時間縮短到1分鐘以內。
6)服務的啟動順序
Kubernetes無法指定pod啟動的順序,同時也要求部署在K8s里的服務不要對其他服務的啟動先后順序有依賴。云平臺服務在采用Kubernetes部署管理之前是采用systemd管理,systemd可以明確定義服務之間的啟動順序。這導致服務之間有比較明顯的先后次序依賴。比如,keystone服務就要求最先啟動,其他所有服務都依賴keystone服務提供初始化服務賬號的認證。容器化改造后,由于這個依賴,導致在keystone容器啟動之前的服務無法正常運行。定位到該問題后,我們將服務因為啟動順序導致的錯誤升級為致命錯誤。這樣,該服務程序遇到依賴服務未啟動導致的問題就異常退出。進而,通過Kubernetes自動重啟拉起服務進程。通過這樣的改造消除了其他服務對keystone的啟動順序依賴。然而,我們無法找到有效手段識別出所有依賴啟動順序而出現的錯誤,因此這樣的服務無順序改造還在持續。
7) 證書失效問題
Kubernetes集群節點之間的相互認證和通信依賴PKI秘鑰體系。如果節點的PKI證書過期,則該節點kubelet無法正常和ApiServer通信,進而導致節點狀態被設置為NotReady,進而出現前述的容器驅逐導致節點不可用的嚴重問題。剛開始,我們部署的k8s集群還是采用kubeadm默認的1年有效期的證書,當時還未顧及到證書到期的問題。到2020年底,開始陸續出現多個集群莫名服務不可用的情況,才注意到證書過期的問題。針對這個問題,我們剛開始采用cronjob安裝自動更新證書腳本的方案,并且在客戶巡檢中,專門檢查證書過期問題,以提前發現問題。后來到了2021年3.8版本,采用了更糙快猛的方法,直接修改了kubeadm的證書簽發代碼,一次性簽發99年證書,從而徹底解決了Kubernetes的證書過期問題。
8) iptables修改
Kubernetes部署后,kubelet、kube-proxy以及我們采用的calico等都依賴iptables,會接管節點的iptables規則,在kubelet啟動之后,對iptables規則的修改會被重置,并且會刷新iptables規則。如何持久化對iptables規則的修改成為問題。目前,針對節點的防火墻規則可以采用calico的網絡策略來實現,可參考文章https://www.cloudpods.org/zh/blog/2021/09/25/calico-customized-node-firewall/。但更復雜iptables規則,還沒找到有效辦法。
3、未來規劃
1)升級Kubernetes版本
目前,云平臺底座kubernetes的版本是1.15.12,該版本已經不再被Kubernetes官方支持。目前存在的比較明顯的問題是和較新的采用cgroup v2的操作系統不兼容,導致無法設置服務的資源limit。后續考慮升級底層Kubernetes到更新版本,以期獲得更新的功能特性支持。
2)采用K3S等更輕量版本
目前云平臺依賴底層Kubernetes的功能特性不多,同時Kubernetes本身也要消耗一定的節點資源,后面也計劃考慮采用k3s等更輕量的Kubernetes版本,進一步降低Kubernetes的使用成本。
3)移除計算節點對iptables的依賴
計算節點網絡主要依賴openvswitch實現虛擬機的通信,iptables主要是給kubelet,kube-proxy和cailico-node等k8s服務組件使用,而計算節點上的服務組件主要是用來管理QEMU/KVM虛擬機的host-agent等服務,這些服務本身具備基于ovs的網絡管理能,不依賴k8s的網絡,完全可以只依賴host網絡即可正常工作。因此,其實可以去掉計算節點的kubeproxy,calico等組件,去除對iptables的修改,這樣簡化組件依賴,進一步提高系統的可靠性。
4)完全禁用Eviction機制
Eviction機制在虛擬化云平臺或有狀態服務場景中,會起到故障放大的作用。在充分掌控對節點資源耗盡預警的前提下,應考慮徹底禁用Eviction機制。
5) 多數據中心架構的支持
目前云平臺所有節點都運行在一個Kubernetes集群內。而云平臺本身是可以支持多數據中心部署的。但是跨數據中心部署單個kubernetes集群不是最佳實踐。比較理想的架構是單個kubernetes集群部署在一個數據中心內。因此,應該允許云平臺跨多個K8s集群部署。例如每個數據中心一個Kubernetes集群,其中一個集群部署完整的云平臺,其他Kubernetes集群以從可用區的角色加入主集群。每個Kubernetes集群之上只運行管理一個數據中心所需的云平臺組件。進而構成一個多數據中心的云平臺架構。

