引言
Kubernetes從創(chuàng)建之初的核心模塊之一就是資源調(diào)度,想要在生產(chǎn)環(huán)境使用好Kubernetes,就必須對它的資源模型以及資源管理非常了解,同時,為了避免不必要的資源浪費,Kubernetes也提供了基于kubelet的垃圾回收機制,最大限度地保證整個容器內(nèi)的資源可以被有效利用,并及時清理回收失效的對象資源。
一、Kubernetes的資源和管理
在 Kubernetes 中,有兩個基礎(chǔ)但是非常重要的概念:Node和Pod。Node通常翻譯成節(jié)點,是對集群資源的抽象;Pod 是對容器的封裝,是應用運行的實體。Node 提供資源,而 Pod 使用資源,這里的資源分為計算資源(CPU、Memory、GPU)、存儲資源(Disk、SSD)和網(wǎng)絡(luò)資源(Network Bandwidth、IP、Ports)等。這些資源提供了應用運行的基礎(chǔ)環(huán)境,正確理解這些資源以及集群調(diào)度如何使用這些資源,對于大規(guī)模的 Kubernetes 集群來說至關(guān)重要,不僅能保證應用的穩(wěn)定性,也可以提高資源的利用率。
通常我們在關(guān)注一個容器化系統(tǒng)的運行性能時,首先關(guān)注的就是它可以獲得的計算資源(CPU、內(nèi)存)的資源情況,一般來說,獲得的資源越多、越充分,那么這個系統(tǒng)的工作就會越順暢,其中CPU 分配的是可使用時間,也就是操作系統(tǒng)管理的時間片,每個進程在一定的時間片里運行自己的任務(wù),而對于內(nèi)存,系統(tǒng)提供的是可供調(diào)配的內(nèi)存大小。
CPU 的使用時間是可壓縮的,換句話說它本身并無狀態(tài),資源申請很快,也能快速正?;厥?;而內(nèi)存大小是不可壓縮的,因為它是有狀態(tài)的(內(nèi)存里面保存的數(shù)據(jù)),申請資源很慢(需要計算和分配內(nèi)存塊的空間),并且回收可能失敗(被占用的內(nèi)存一般不可回收)。
這里把資源分成可壓縮和不可壓縮,是因為在資源不足的時候,它們的表現(xiàn)很不一樣。對于不可壓縮資源,如果資源不足,也就無法繼續(xù)申請資源(內(nèi)存用完就是用完了),并且會導致 Pod 的運行產(chǎn)生無法預測的錯誤(應用申請內(nèi)存失敗會導致一系列問題);而對于可壓縮資源,比如 CPU 時間片,即使 Pod 使用的 CPU 資源很多,CPU 使用也可以按照權(quán)重分配給所有 Pod 使用,雖然每個Pod使用的時間片減少,但不會影響程序的邏輯。
在 Kubernetes 集群管理中,有一個非常核心的功能,就是為 Pod 選擇一個主機運行。調(diào)度必須滿足一定的條件,其中最基本的是主機上要有足夠的資源給 Pod 使用。
圖1 集群管理示意圖
用戶在 Pod 中可以配置要使用的資源總量,Kubernetes 根據(jù)配置的資源數(shù)進行調(diào)度和運行。目前主要可以配置的資源是 CPU 和 Memory,對應的配置字段是:
spec.containers[].resource.limits/request.cpu/memory
需要注意的是,用戶是對每個容器配置Request值,所有容器的資源請求之和就是 Pod 的資源請求總量。
CPU 一般用核數(shù)來標識,一核CPU 對應物理服務(wù)器的一個超線程核,也就是操作系統(tǒng)/proc/cpuinfo中列出來的核數(shù)。因為對資源進行了池化和虛擬化,所以Kubernetes允許配置非整數(shù)個的核數(shù),比如0.5也是合法的,它標識應用可以使用半個CPU核的計算量。CPU的請求有兩種方式,一種是剛提到的 0.5、1 這種直接用數(shù)字標識CPU核心數(shù);另外一種表示是 500m,它等價于 0.5,也就是說 1Core = 1000m。
內(nèi)存比較容易理解,是通過字節(jié)大小指定的。如果直接一個數(shù)字,后面沒有任何單位,表示這么多字節(jié)的內(nèi)存。數(shù)字后面還可以跟著單位,支持的單位有 E、P、T、G、M、K,前者分別是后者的1000倍大小的關(guān)系,此外還支持 Ei、Pi、Ti、Gi、Mi、Ki,其對應的倍數(shù)關(guān)系是2^10 = 1024。比如要使用100M 內(nèi)存的話,直接寫成100Mi即可。
但是,節(jié)點上的的資源,真的可以予取予求嗎?
理想情況下,我們希望節(jié)點上所有的資源都可以分配給Pod 使用,但實際上節(jié)點上除了運行Pods之外,還會運行其他的很多進程:系統(tǒng)相關(guān)的進程(比如SSHD、Udev等),以及Kubernetes集群的組件(Kubelet、Docker等)。我們在分配資源的時候,需要給這些進程預留一些資源,剩下的才能給Pod 使用,Kubelet會保證節(jié)點上的資源使用率不會真正到100%,因此Pod的實際可使用資源會稍微再少一點。主機上的資源邏輯分配圖如圖2所示。
圖2 主機上的資源邏輯分配圖
用戶在創(chuàng)建Pod 的時候,可以指定每個容器的Requests和Limits 兩個字段,下面是一個實例:
Requests是容器請求要使用的資源,Kubernetes會保證Pod能使用到這么多的資源。請求的資源是調(diào)度的依據(jù),只有當節(jié)點上的可用資源大于Pod請求的各種資源時,調(diào)度器才會把Pod調(diào)度到該節(jié)點上(如果CPU資源足夠,內(nèi)存資源不足,調(diào)度器也不會選擇該節(jié)點)。
需要注意的是,調(diào)度器只關(guān)心節(jié)點上可分配的資源,以及節(jié)點上所有Pods請求的資源,而不關(guān)心節(jié)點資源的實際使用情況,換句話說,如果節(jié)點上的Pods申請的資源已經(jīng)把節(jié)點上的資源用滿,即使它們的使用率非常低,比如說CPU和內(nèi)存使用率都低于10%,調(diào)度器也不會繼續(xù)調(diào)度Pod上去。
Limits是Pod能使用的資源上限,是實際配置到內(nèi)核cgroups里面的配置數(shù)據(jù)。對于內(nèi)存來說,會直接轉(zhuǎn)換成dockerrun 命令行的--memory大小,最終會配置到cgroups對應任務(wù)的文件中,文件目錄如下:
/sys/fs/cgroup/memory/…/memory.limit_in_bytes
如果Limit沒有配置,則表明沒有資源的上限,只要節(jié)點上有對應的資源,Pod就可以使用。
使用Requests和Limits概念,我們能分配更多的Pod,提升整體的資源使用率。但是這個體系有個非常重要的問題需要考慮,那就是怎么去準確地評估Pod的資源Requests?如果評估過低,會導致應用不穩(wěn)定;如果過高,則會導致使用率降低。這個就需要開發(fā)者和管理員共同討論和定義,要結(jié)合生產(chǎn)實踐進行調(diào)節(jié)。
舉個容器化改造后問題排查及優(yōu)化的例子:某行的移動辦公平臺進行從傳統(tǒng)服務(wù)遷移到“微服務(wù)+容器化”的改造,開發(fā)測試驗證未出現(xiàn)問題,轉(zhuǎn)入非功能性能試期間后,隨著壓力的不斷增加,并發(fā)量上升,系統(tǒng)開始出現(xiàn)登錄請求緩慢、超時等情況,與之前基于開發(fā)測試的預估有明顯差距,經(jīng)日志排查后,定位直接原因為接入“移動網(wǎng)關(guān)”服務(wù)內(nèi)存使用率過高,檢查非功能性能試驗環(huán)境的移動網(wǎng)關(guān)服務(wù)資源配置,配置如下:
開發(fā)人員與運維人員對非功能性能試驗環(huán)境及應用程序的日志進行分析后,決定解決問題從兩方面進行,一方面開發(fā)對應用程序進行優(yōu)化,減少開源軟件產(chǎn)生堆外內(nèi)存,另外一方面,在非功能性能試驗環(huán)境對進行資源配置緊急擴容,擴容后配置如下:
經(jīng)過資源擴容,系統(tǒng)穩(wěn)定性得到提升,問題出現(xiàn)的概率明顯下降,后經(jīng)應用程序調(diào)整后,問題徹底解決。由于此移動網(wǎng)關(guān)是通用的消息轉(zhuǎn)發(fā)網(wǎng)關(guān)服務(wù),根據(jù)此次問題的解決結(jié)果,后續(xù)此服務(wù)的默認配置即以此次優(yōu)化后結(jié)果為準。
二、Kubernetes垃圾對象的回收機制
Kubernetes系統(tǒng)在長時間運行后,Kubernetes Node會下載非常多的鏡像,其中可能存在很多過期的鏡像。同時因為運行大量的容器,容器推出后就變成死亡容器,將數(shù)據(jù)殘留在宿主機上,這樣一來,過期鏡像和死亡容器都會占用大量的硬盤空間。如果磁盤空間被用光,可能會發(fā)生非常糟糕的情況,甚至會導致磁盤的損壞。為此kubelete會進行垃圾清理工作,即定期清理過期鏡像和死亡容器。因為kubelet需要通過容器來判斷pod的運行狀態(tài),如果使用其它方式清除容器有可能影響kubelet的正常工作,所以不推薦使用其它管理工具或手工進行容器和鏡像的清理。
那么實現(xiàn)GC的機制是怎么樣的呢?各項資源對象(Resource Object)是以怎樣的一種方式進行清理的呢?
Kubernetes通過 Garbage Collector組件 和 ownerReference 一起配合實現(xiàn)了“垃圾回收GC”的功能。
通常在K8s 中,有兩大類GC:
第一類
級聯(lián)(Cascading Deletionstrategy)
Kubernetes在不同的 Resource Objects 中維護了一定的“從屬關(guān)系”,一般會默認在一個Resource Object和它的創(chuàng)建者之間建立一個“從屬關(guān)系”。Kubernetes利用已經(jīng)建立的“從屬關(guān)系”進行資源對象的進行“級聯(lián)”清理工作。例如,當一個dependent 資源的owner已經(jīng)被刪除或者不存在的時候,從某種角度可以判定,這個dependent的對象已經(jīng)異常(無人管轄),則需要進行清理。而“Cascading Deletion”則是被通過Garbage Collector(GC)組件實現(xiàn)。
在級聯(lián)刪除中,又有兩種模式:前臺(foreground)和后臺(background)。
前臺級聯(lián)刪除(Foreground Cascading Deletion):在這種刪除策略中,所有者對象的刪除將會持續(xù)到其所有從屬對象都被刪除為止。當所有者被刪除時,會進入“正在刪除”(deletionin progress)狀態(tài),此時:
對象仍然可以通過REST API 查詢到(可通過kubectl 或kuboard 查詢到)
對象的deletion Timestamp 字段被設(shè)置
對象的metadata.finalizers 包含值foreground Deletion
一旦對象被設(shè)置為“正在刪除”的狀態(tài),垃圾回收器將刪除其從屬對象。當垃圾回收器已經(jīng)刪除了所有的“blocking”從屬對象之后(ownerReference.blockOwnerDeletion=true的對象),將刪除所有者對象。
后臺級聯(lián)刪除(Background Cascading Deletion):這種刪除策略會簡單很多,它會立即刪除所有者的對象,并由垃圾回收器在后臺刪除其從屬對象。這種方式比前臺級聯(lián)刪除快的多,因為不用等待時間來刪除從屬對象。
第二類
孤兒(Orphan)
這種情況下,對所有者的進行刪除只會將其從集群中刪除,并使所有對象處于“孤兒”狀態(tài)。在孤兒刪除策略(orphandeletion strategy)中,會直接刪除所有者對象,并將從屬對象中的ownerReference元數(shù)據(jù)設(shè)置為默認值。之后垃圾回收器會確定孤兒對象并將其刪除。
Garbage Collector 組件的工作通常由三部分實現(xiàn),具體如圖3所示。
圖3 GarbageCollector 組件工作的實現(xiàn)
1
Scanner:它負責收集目前系統(tǒng)中已存在的Resource,并且周期性的將這些資源對象放入一個隊列中,等待處理(檢測是否要對某一個Resource Object 進行 GC操作)。
2
GarbageProcessor:Garbage Processor 由兩部分組成。
a、Dirty Queue:Scanner會將周期性掃描到的Resource Object 放入這個隊列中等待處理
b、Worker:worker負責從Dirty Queue隊列中取出元素進行處理
檢查Object 的metaData部分,查看ownerReference字段是否為空。如果為空,則本次處理結(jié)束,如果不為空,繼續(xù)檢測ownerReference字段內(nèi)標識的Owner Resource Object是否存在,如果存在:則本次處理結(jié)束;如果不存在:刪除這個Object。
3
Propagator:Propagator 由三個部分構(gòu)成。
a、Event Queue:負責存儲Kubernetes中資源對象的事件(Eg:ADD,UPDATE,DELETE)
b、DAG(有向無環(huán)圖):負責存儲Kubernetes中所有資源對象的“owner-dependent”關(guān)系
c、Worker:從EventQueue中,取出資源對象的事件,根據(jù)事件的類型會采取以下兩種操作:
1)ADD/UPDATE:將該事件對應的資源對象加入DAG,且如果該對象有owner 且owner 不在DAG 中,將它同時加入Garbage Processor 的Dirty Queue 中;
2)DELETE:將該事件對應的資源對象從DAG中刪除,并且將其“管轄”的對象(只向下尋找一級,如刪除Deployment,那么只操作ReplicaSet )加入Garbage Processor 的Dirty Queue 中。
其實,在有了Scanner 和Garbage Processor 之后,Garbage Collector 就已經(jīng)能夠?qū)崿F(xiàn)“垃圾回收”的功能了。但是有一個明顯的問題:Scanner的掃描頻率設(shè)置多少好呢?時間間隔太長了,Kubernetes內(nèi)部就會積累過多的“廢棄資源”;間隔太短了,尤其是在集群內(nèi)部資源對象較多的時候,頻繁地拉取信息對API-Server 也是一個不小的壓力。
Kubernetes作為一個分布式的服務(wù)編排系統(tǒng),其內(nèi)部執(zhí)行任何一項邏輯或者行為,都依賴一種機制:“事件驅(qū)動”。說的簡單點,Kubernetes中一些看起來“自動”的行為,其實都是由我們所說的“Event”驅(qū)動的。任意一個Resource Object發(fā)生變動的時候(新建、更新、刪除),都會觸發(fā)一個Kubernetes的事件(Event),而這個事件在Kubernetes的內(nèi)部是公開的,也就是說,我們可以在任意一個地方監(jiān)聽這些事件。
總的來說,無論是“事件的監(jiān)聽機制”還是“周期性訪問API-Server 批量獲取Resource Object信息”,其目的都是為了能夠掌握Resource Object的最新信息。兩者是各有優(yōu)勢的:
(1)批量拉?。阂淮涡岳∷械腞esource Object,獲取信息全面;
(2)實時監(jiān)聽Resource 的Event:實時性強,且對 API—SERVER不會造成太大的壓力。
綜上所述,在實現(xiàn)Garbage Collector的過程中,Kubernetes向其添加了一個“增強型”的組件:Propagator。在有了Propagator 的加入之后,我們完全可以僅在GC 開始運行的時候,讓Scanner 掃描一下系統(tǒng)中所有的Object,然后將這些信息傳遞給Propagator 和Dirty Queue。只要DAG 一建立起來之后,那么Scanner其實就沒有再工作的必要了。“事件驅(qū)動”的機制提供了一種增量的方式讓GC 來監(jiān)控k8s 集群內(nèi)部的資源對象變化情況。
總結(jié)
docker+Kubernetes確實是有效緩解分布式架構(gòu)升級改造后虛擬機設(shè)備激增代理的運維壓力的一劑良方。充分熟悉Kubernetes的各種使用技巧,優(yōu)化對容器資源的有效管理和配置,是進一步提升系統(tǒng)運行性能,支撐系統(tǒng)穩(wěn)定運行的基礎(chǔ),隨著容器化改造的不斷深入,早期的那種套用傳統(tǒng)虛擬機架構(gòu)下的運維管理方式必然將被更有彈性更靈活的方式取代,請大家開始主動學習和掌握Kubernetes,以應對未來的要求和挑戰(zhàn)吧。