Kubernetes生態體系落地過程中的選型和踩坑
開源節流,是企業提升利潤的兩大方向;中臺戰略或基礎結構體系常常肩負了節流的重任。無論大小企業,容器化都被認為可以大幅度地提升效率,增加運維標準化和資源利用率。但是此類事情一旦做不好很容易造成花了大量成本而效果得不到認可的尷尬結果。本次分享從團隊的實際經驗出發,聊一下容器化生態體系落地中的一些事情。
監控
容器環境一般是提供一整套解決方案的,監控可以分為三種:指標監控、業務監控、調用鏈監控。
業務監控和調用鏈監控更多的取決于業務開發部門的選型,如skywalking等。
容器環境下,指標監控非Prometheus莫屬,通過Service Discovery機制中的Kubernetes plugin獲得scrape路徑,之后的鏈路就比較通暢了。
使用Prometheus過程中一個繞不開的問題是持久化存儲,WAL中保存的數據不宜過多,否則內存和加載速度都會產生很大問題,官方支持的remote read/write列表中,我們考查了InfluxDB和TiDB這兩個,實踐中兩者占用的內存都非常大,建議在集群外的物理機中進行部署,如果使用InfluxDB,如果集群中Pod創建頻繁(例如使用了cronjob)可能會觸發key數量限制。
日志
日志分為兩種:std系列日志和文件日志,它們的區別主要在于收集方式不同,一般來說,收集上來的日志都會并進入ELK體系,后面的處理就都差不多了。
std系列日志因其屬于Linux模型,可以統一從Docker數據目錄中予以收集,一種部署方式是使用DaemonSet部署Fluentd并掛載hostPath。
文件形態的日志略顯復雜,NFS/CephFS等分布式存儲肯定不適合存放日志,我們通過emptyDir形式實現目錄共享,然后新增filebeat sidecar對共享目錄中的日志文件進行收集,入ELK體系。
如何與持續交付對接
這里我們關注持續交付部署部分的方案,Kubernetes的部署本質上就是不同類型的資源對象以yaml格式應用,在自研與使用開源方案之間,我們選用了Helm作為部署階段中,持續交付與Kubernetes的溝通橋梁。通過Helm我們可以把部署配置變成一個JSON對象,輔以標準化的部署模版,實現部署的標準化,同時自帶了資源狀態監測,應用管理等功能。
作為一個toB性質的服務,我們不應該只關注服務本身的可用性和性能,更應該從最終用戶體驗維度進行自查改進。例如Kubernetes官方的Benchmark工具中提到Pod平均啟動時間,但是對項目來說更加關注的是Pod平均ready時間,而探針的結果是受到項目依賴,數據庫等因素的影響的。對于特定項目,很多數值是穩定的,我們可以在報警系統中進行一些統計學方面的處理。
如何正確地添加Sidecar
剛剛的日志章節,提到了使用Filebeat Sidecar來收集日志,持續交付對接過程中提到了使用模版來生成項目的yaml文件。這就意味著,日志Sidecar容器必須在項目部署配置中予以體現,與項目進行耦合。這帶來了很大的復雜度,也令日志系統的配置變更流程非常復雜。畢竟穩定的項目一般不會去更新部署配置,日志系統要一直兼容老版本的規則文件。因而需要一種手段,把日志配置和項目配置進行隔離。
我們找到的辦法是Kubernetes的動態準入控制(Mutating Admission Webhook)來實現sidecar injection。通過這一機制,所有的資源在操作(增刪改)同步到etcd前,都會請求Webhook,Webhook可以通過或否決(allow/reject),也可以響應一個JSON Patch,修改對象的部分資源。
事實上,常常會發現我們定義的Pod中會被默認注入default service account,就是Kubernetes中內置Admission的作用產物,現在非常火的Istio,其劫持流量的原理為修改每個Pod的網絡規則,也是通過這種機制注入init-container,從而在Pod中修改iptables來實現。
通過這一機制,還可以針對諸如hostPort,hostPath,探針規范作出安全審計,可以說提供了相當豐富的想象空間。風險點是Webhook必須穩定可靠,延時較長不是問題,1.14+提供了timeoutSeconds,但如果返回一個不能被apply的patch,會導致資源創建失敗。
在日志應用場合,我們注冊了Pod對象的Create動作,項目只需要通過annotation傳入幾個簡單配置,就可以自動生成一個自定義的Filebeat Sidecar,非常干凈和方便。
如何實現自定義PodIP
Kubernetes中每次Pod的創建都會分配一個新的IP,社區的目的是希望用戶使用Service+DNS的機制實現通信,但實際上,在一些基礎組件的容器化過程中,由于軟件兼容性,我們會希望某些業務容器的IP固化,不因重啟而變更。
這里以Redis舉例要用到穩定的IP的場景:
在Redis集群模式中,“cluster meet”命令只支持IP格式,不支持域名解析配置,社區中有人提出過這個issue結果被拒了。雖說Redis集群中任意一個節點的IP變更都可以在Redis集群內自動識別(因為Instance ID不變),但是如果因為意外情況導致所有Redis集群節點同時發生重啟,集群內節點兩兩無法發現彼此,那就只能由運維人工介入,重新讓節點發現彼此,此外IP的變更也會導致有緩存的Redis客戶端產生錯誤。
在Kubernetes中,Service相關資源由kube-proxy負責,主要體現在iptables或IPVS規則中,而PodIP是由CNI負責分配,具體體現在eth-pair和路由表中。我們選用了Calico作為CNI插件,通過cni.projectcalico.org/ipAddrs這個annotation將預期的IP傳遞給Calico。
相對于對CNI進行二次開發自行實現IPAM來說,這種方法的開發成本較小。
在具體實現上:由于Pod是通過上級對象資源的模版創建,無法在模版中為每個Pod自定義annotation,所以我們同樣通過動態準入機制實現,例如在sts資源中自定義一個annotation并傳遞一組IP,隨后劫持Pod的創建,根據序號依次為Pod新增annotation,以激活Calico的指定PodIP功能。
這里注意的一點是,我們在實現IP固化功能后,一些微服務團隊也希望使用這個功能。他們想要解決的痛點是容器發版之后,注冊中心仍然保有舊的PodIP的問題。這里不適合去做IP固化:
- 原因一:Web項目大都使用deployment發布,在rs和Pod階段,podName會添加隨機字符串,無法甄別排序;事實上,我們只對sts資源開放了固化IP的方案;
- 原因二:微服務應用應當實現對SIGINT,SIGTERM等信號的監聽,在pod terminationGracePeriodSeconds中自行實現注冊中心的反注冊。
任務調度
我們有一些祖傳的業務員仍然使用PHP,PHP在進程管理上比較欠缺,物理機環境下很多調度工作要借助于cronjob來完成。我們一些PHP項目一開始上容器的時候,采用的就是Kubernetes提供的cronjob機制,使用下來有這么幾個問題:
- Pod執行日志通過ELK體系收集后展示不直觀;
- 更新代碼后Pod在節點的首次啟動會因為pull代碼而不準時;
- 無法手動執行啟動;
- 間隔時間較短的cron大幅度提高了集群Pod總數,增加管理節點的壓力。
最后我們選擇使用開源的goCron方案,為項目單獨部署任務專用deployment,通過gRPC的方式進行任務的啟停和日志傳輸。
值得注意的是,在開源goCron方案中,由Server角色向Node角色發起請求,但是我們不可能為每一個Node容器都配備Ingress或者NodePort暴露。
在有關二次開發中,我們為gRPC proto參數中新增了target字段。即Server角色中心化部署,每個容器編排集群部署一個Agent角色作為中轉,最終通過SVC達到Node角色。
集群事件監控
我們排查問題的時候第一件事一般都是describe一下相關資源,然后查看event,但是事實上,event默認只能存在1小時;kube-apiserver中有一個參數定義了事件在etcd中的保留時間:event-ttl Amount of time to retain events. (default 1h0m0s)。
這個1h主要是考慮到大規模集群中etcd的性能瓶頸;但即使是小集群,這個值也不建議調整到24h以上。這意味著,如果半夜中集群中發生事件,到了白天上班只能看到restart計數器+1或者對象存活時間清零,而找不到任何相關信息。
所以我們經過二次開發,在所有集群內部署了一個事件收集中間件,監聽所有ns中的ev,發送至ES,并進行一些簡單的聚合,以metrics的形式暴露給prom。這一工具深受運維團隊好評,并且逐漸成為了集群健康的重要晴雨表。
容器內時間模擬及系統參數模擬
容器化和虛擬化相比,最大的區別在于容器和物理機共享了內核,內核實現了進程調度、網絡、io,等等功能,并通過Namespace和CGroup實現隔離。但是在這些隔離中,時間、CPU、內存等信息不在隔離范圍內,從而帶來了問題。
首先我們看一下CPU和內存,在容器中,如果我們打印/proc/cpuinfo或是/proc/meminfo,取到的是物理機的核數和內存大小,但實際上容器必然是會有資源限制的,這會誤導容器環境中的進程,使得一些預期中的優化變成了負優化。如線程數、GC的默認設置。
針對此問題的解決方案有三個:
- Java/Golang/Node啟動時手動參數傳入資源最大限制
- Java 8u131+和Java 9+添加-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap;Java 8u191+和Java 10+默認開啟UseContainerSupport,無需操作;但是這些手段無法修正進程內直接讀取/proc下或者調用top、free -m、uptime等命令輸出的內容
- 改寫相關內核參數,對任意程序都有效果
前兩種方案,侵入性較高,我們選擇使用第三種方案,改寫相關內核參數,使用LXCFS實現,yaml中使用hostPath裝載。
關于LXCFS,這里只提供一個關鍵詞,大家可以去搜索相關信息。
與CPU/內存相類似的還有Uptime、diskStats、Swaps等信息,改寫后容器內top、free -m、uptime等命令都會顯示正確。
值得注意的是CPU的限制,容器中所謂的CPU限制,并不是綁定獨占核,而是限制使用時間。舉個例子:一臺4核的物理機,能并行4個線程;而一臺32核的宿主機上起一個限制為4核的容器,它仍然能并行32個線程,只不過每個核只能占用1/8的時間片。
關于容器內時間的模擬,我們使用了libfaketime,進程啟動時添加LD_PRELOAD和FAKETIME環境變量。
最后聊一下Kubernetes的基礎,etcd。當api-server不可用的時候,直接讀取etcd中的數據將成為最后的救命稻草。然而etcd中存放的數據在某個版本之后已經變成了Protobuf編譯過的二進制數據。get出來之后肉眼無法識別。
我平時會使用Auger這個開源項目,通過管道的形式將etcd中的內容還原成yaml文本。
我認知中的Kubernetes,它是一個容器編排體系,是一套云原生的微服務架構。
Q&A
Q:落地過程必然涉及到之前開發、測試和運維流程的變更,組織和相關人員都會面臨調整,這部分工作貴公司是如何推進的,踩了哪些坑,如何解決的?A:這個一言難盡啊,人的問題是最難解決的,能用技術解決的都不是問題,要是說回答的話,初期打通公司各個關節,讓大boss認可這件事,行政命令強推,很重要。不然做出來也沒人用,就是白忙活,在用戶中找小白鼠迭代,而不是自己弄個自以為完美的推出去。
Q:Java容器瞬間拉起的過程,整個集群都會被CPU用盡,如何解決Java CPU啟動時候CPU資源互爭的情況?A:這個問題我們也遇到過,后來把內核升級到4.19后就不再發生了,很多內存耗盡,CPU爆炸的問題我們都通過內核升級解決了。
Q:日志平臺怎么解決沒法像grep -C查找上下文,日志平臺怎么標準化日志格式?A:這個得看日志平臺具體開發是怎么實現的了,一般來說這不是問題日志格式的標準化,得和業務合作。事實上日志平臺一般是中臺部門的單獨的系統,它要單獨開發。
Q:容器化落地怎么協調開發的需求?比如開發學習成本,比如本地調試和現場保留復現問題,排查問題的方法方式對開發友好。A:這還是人的問題,很多業務開發不愿意學習,不接受新事物,一葉障目否定容器,這真的沒辦法。還是從人身上尋求妥協吧。每個人的精力都是有限的,這種事情陷進去很難拔出來;公開培訓,講座,駐場支持,培養業務部門懂的人。
Q:線上Kubernetes集群采用什么方式部署,二進制還是kubeadm等,部署架構是怎么樣的?A:如果了解證書制作和Kubernetes各個組件的作用,建議從二進制文件入手,企業環境可以自己寫Ansible等腳本。kubeadm維護一般不適用于線上環境。
Q:我是一名Java工程師,有7年經驗,想轉行到容器相關領域,請問成為容器開發工程師需要哪些條件?A:對Linux要非常了解,脫離JVM看一些系統方面的知識。此外容器的語言基本上都是Go,微服務那套和Java沒啥區別,熟悉Protobuf。
Q:如何保證日志Sidecar的存活與否不會影響到業務容器?A:Sidecar和業務容器本來就是互相隔離的,現在1.10+的Kubernetes在Pod內只會共享網絡,不會默認共享pid了,應該不會有啥影響。
Q:Sidecar方式收集日志會出現延時,特別是丟失問題,這個如何解決?A:減少Filebeat的采集時間,這個我感覺無解。或者在gracefultime上做文章,讓Filebeat多活一會。