歡迎光臨
每天分享高質量文章

JDOS 2.0:Kubernetes的工業級實踐


JDOS(Jingdong Datacenter Operating System)1.0於2014年推出,基於OpenStack進行了深度定製,併在國內率先將容器引入生產環境,經歷了2015年618/雙十一的考驗。團隊積累了大量的容器運營經驗,對Linux內核、網絡、儲存等深度定製,實現了容器秒級分配。

意識到OpenStack架構的笨重,2016年當JDOS 1.0逐漸增長到十萬、十五萬規模時,團隊已經啟動了新一代容器引擎平臺(JDOS 2.0)研發。JDOS 2.0致力於打造從原始碼到鏡像,再到上線部署的CI/CD全流程,提供從日誌、監控、排障,終端,編排等一站式的功能。JDOS 2.0迅速成為Kubernetes的典型用戶,併在Kubernetes的官方博客分享了從OpenStack切換到Kubernetes的過程。 JDOS 2.0的發展過程中,逐步完善了容器的監控、網絡、儲存,鏡像中心等容器生態建設,開發了基於BGP的Skynet網絡、ContainerLB、ContainerDNS、ContainerFS等多個專案,並將多個專案進行了開源。本次分享,我們主要分享的是在JDOS2.0的實踐過程中關於Kubernetes的一些經驗和教訓。

:Kubernetes版本我們目前主要穩定在了1.6版本。本文中的實踐也是主要基於此版本。我們做的一些feature和bug,有的在Kubernetes後續發展過程中進行了實現和修複。大家有興趣也可以同社區版進行對照。

1. Kubernetes定製開發

為什麼定製?

很多人可能會問,原生的Kubernetes不好麽,為什麼要定製。首先,我先來解釋下定製的原因。在我們使用開源專案的過程中,無論是OpenStack還是Kubernetes,都會有一種體會,就是理想和現實是存在較大的差距的。出於各種原因,開源專案需要做很多的妥協,而且很多功能是很理想化的,這就導致了Kubernetes直接應用於生產中會遇到很多的問題。因此,我們對Kubernetes進行了定製開發,秉持了兩個基本的理念,加固與裁剪:

  1. 加固主要指的是在任何時候、任何情況下,將容器的非故障遷移、服務的非故障失效的概率降到最低,最大限度的保障線上集群的安全與穩定,包括etcd故障、apiserver全部失聯、apiserver拒絕服務等等極端情況。

  2. 裁剪則體現為我們刪減了很多社區的功能,修改了若干功能的預設策略,使之更適應我們的生產環境的實際情況。

在這樣的原則下,我們對Kubernetes進行了許多的定製開發。下麵將對其中部分進行介紹。

1.1 IP保持不變

線上很多用戶都希望自己的應用下的Pod做完更新操作後,IP依舊能保持不變,於是我們設計實現了一個IP保留池來滿足用戶的這個需求。簡單的說就是當用戶更新或刪除應用中的Pod時,把將要刪除的pod的IP放到此應用的IP保留池中,當此應用又有創建新Pod的需求時,優先從其IP保留池中分配IP,只有IP保留池中無剩餘IP時,才從大池中分配IP。IP保留池是通過標簽來與Kubernetes的資源來保持一致,因此ip保持不變功能不僅支持有狀態的StatefulSet,還可以支持rc/rs/deployment。

重覆利用IP會帶來一個潛在的問題,就是當前一個Pod還未完全刪除的時候,後一個Pod的網絡就不能提早使用,否則會存在IP的二義性。為了提升Pod更新速度,我們對容器刪除的流程進行了優化,將CNI接口的呼叫提前到了stop容器之前,從而大大加快了IP釋放和新Pod創建的速度。

1.2 檢查IP連通性

Kubernetes創建Pod,調度時Pod處於Pending的狀態,調度完成後處於Creating的狀態,磁盤分配完成,IP分配完成,容器創建完成後即處於Running狀態,而此時有可能IP還未真正生效,用戶看到Running狀態但是卻不能登錄容器可能會產生困惑,為了讓用戶有更好的體驗,在Pod轉變為Running狀態之前,我們增加了檢查IP連通性的步驟,這樣可以確保狀態的一致性。

1.3 修改預設策略

Pod的restart策略其實是Rebuild,就是當Pod故障(可能是容器自身問題,也可能是因為物理機重啟等)後,kubelet會為Pod重新創建新的容器。但是在實際過程中,其實很多用戶的應用會在根目錄寫入一些資料或者配置,因此用戶會更加期望使用先前的容器。因此我們為Pod增加了一個reUseAlways的策略,併成為restart的預設策略。而將原來的Always策略,即rebuild容器的策略作為可選的策略之一。當使用reUseAlways策略時,kubelet將會首先檢查是否有對應容器,如果有,則會直接start該容器,而不會重新create一個新的容器。

對於Service,我們進行了自己的實現,可以選用HAProxy/Nginx/LVS進行導流。當節點故障時,Controller會將該節點上的Pod從對應的Service進行摘除。但是在實際生產中,其實很容易遇到另外一個問題,就是節點實際沒有完全故障,是處於一個不穩定狀態,比如網絡時通時不通,會表現為Node的狀態在ready和notready之間反覆切換,會導致Service的Endpoint會反覆修改,最終會影響到HAProxy/Nginx進行頻繁reload。其實這個可以通過給Service配置annotation使得ep不受node notready的影響。但是我們為了安全起見,將該策略設置為了預設策略,而配置額外的annotation可以使其能夠在not ready時被摘除。因為我們的LB上都預設開啟了健康檢查(預設是端口檢查,還可以進行配置路徑健康檢查)。因此不健康節點的流量切除可以通過LB自身進行。

1.4 定製Controller

在實踐過程中,我們有一個深刻的體會,就是官方的Controller其實是一個參考實現,特別是Node Controller和Taint Controller。Node的健康狀態來自於其通過apiserver的上報。而Controller僅僅依據通過apiserver中獲取的上報狀態,就進行了一系列的操作。這樣的方式是很危險的。因為Controller的信息面非常窄,沒法獲取更多的信息。這就導致在中間任何一個環節出現問題,比如Node節點網絡不穩定,apiserver繁忙,都會出現節點狀態的誤判。假設出現了交換機故障,導致大量kubelet無法上報Node狀態,Controller進行大量的Pod重建,導致許多原先的健康節點調度了許多Pod,壓力增大,甚至部分健康節點被壓垮為notready,逐漸雪崩,最終導致整個集群的癱瘓。這種災難是不可想象的,更是不可接受的。

因此,我們對於Controller進行了定製。節點的狀態不僅僅由kubelet上報,在Controller將其置為notready之前,還會進行覆核。覆核部分交由一個單獨的分佈式系統MAGI完成,其在多個物理POD上進行部署,收到請求會對節點分別進行獨立的分析和體檢,最終投票,做出節點是否notready的判斷。這樣最大限度的降低了節點誤判的概率。

1.5 資源限制

Kubernetes預設提供了CPU和Memory的資源管理,但是這對生產環境來說,這樣的隔離和資源限制是不夠的,因此我們增加了磁盤讀寫速率限制、Swap使用限制等,最大限度保證Pod之間不會互相影響。

我們對大部分的Pod,還提供了本地儲存。目前Kubernetes支持的一種容器資料本地儲存方式是emptyDir,也就是直接在物理機上創建對應的目錄並掛載給容器,但是這種方式不能限制容器資料盤的大小,容易導致物理機上的磁盤被打滿從而影響其它行程。鑒於此,我們為Kubernetes的新開發了一個儲存插件LvmPlugin,使其支持基於LVM(Logical Volume Manager)管理本地邏輯捲的生命周期,並掛載給容器使用。LvmPlugin可以執行創建刪除掛載卸載邏輯盤LV,並且還可以上報物理機上的磁盤總量及剩餘空間給kube-scheduler,使得創建新Pod時Scheduler把LVM的磁盤是否滿足也作為一個調度指標。

對於資料庫容器或者使用磁盤頻率較高的業務,用戶會有限制磁盤讀寫的需求。我們的實現方案是把這限制指標看作容器的資源,就像CPU、Memory一樣,可以在創建Pod的yaml檔案中指定,同一個Pod的不同容器可以有不同的限制值,而kubelet創建容器時可以獲取當前容器對應的磁盤限制指標的值併進行相應的設置。

1.6 gRPC升級

生產環境中,當集群規模迅速膨脹時,即使利用負載均衡的方式部署kube-apiserver,也常常會出現某個或某幾個apiserver不穩定,難以承擔訪問壓力的情況。經過了反覆的實驗排查,最終確定是grpc的問題導致了apiserver的性能瓶頸。我們對於gRPC包進行了升級,最終地使得apiserver的性能及穩定性都有了大幅度的提升。

1.7 平滑升級

我們於2016年就著手設計研發基於Kubernetes的JDOS 2.0,彼時使用的版本是Kubernetes 1.5,2017年社區發佈了Kubernetes 1.6的release版本,其中新增了很多新的特性,比如支持etcd V3,支持節點親和性(Affinity)、Pod親和性(Affinity)與反親和性(anti-affinity)以及污點(Taints)與容忍(Tolerations)等調度,支持呼叫Container Runtime的統一接口CRI,支持Pod級別的Cgroup資源限制,支持GPU等,這些新特性都是我們迫切需要的,於是我們決定由Kubernetes 1.5升級至當時1.6最新release的版本Kubernetes 1.6.3。但是此時生產環境已經基於Kubernetes 1.5上線大量容器,如何在保證這些業務容器不受任何影響的情況下平滑升級呢?

對比了兩個版本的代碼,我們討論了對於Kubernetes進行改造兼容,實現以下幾點:

  1. Kubernetes 1.6預設的Cgroup資源限制層級是Pod,而老節點上的Cgroup資源限制層級是Container,所以升級後要添加相應配置保證老節點的資源限制層級不發生改變。

  2. Kubernetes 1.6預設會清理掉leacy container也就是老的Container,通過對kuberuntime的二次開發,我們保證了升級到1.6後在Kubernetes 1.5上創建的老容器不被清理。

  3. 新老版本的containerName格式不一致導致獲取Pod狀態時獲取不到IP,從而升級後老Pod的IP不能正常顯示,通過對dockershim部分代碼的適當調整,我們將老版本的Pod的containerName統一成新版本的格式,解決了這個問題。

經過如上的改造,我們實現了線上幾千台物理機由Kubernetes 1.5到Kubernetes 1.6的平滑升級。而業務完全無感知。

1.8 bug fix和其他feature

我們還修複了諸如GPU中的NVIDIA卡重覆分配給同一容器,磁盤重覆掛載bug等。這些大部分社區在後面的版本也做了修複。還增加了一些小的功能,比如增加了Service支持set-based的selector,kubelet image gc優化,kubectl get node顯示時增加Node的版本信息等等。這裡就不詳述了。

2. 集群運營

2.1 引數調優和配置

Kubernetes的各個組件有大量的引數,這些引數需要根據集群的規模進行優化調整,併進行適當的配置,來避免問題以及定製自己的特殊需求。比如說有次我們其中一個集群個別節點出現了不停的在Ready和NotReady的狀態之間來回切換的問題,而經過檢查,集群的各個服務都處於正常的狀態。很是認真研究了一下,才發現是此Node上的容器數量太多,並且每個容器的ConfigMap也比較多,導致Node節點每秒向apiserver發送的請求數也很多,超過了kubelet的配置api-qps的預設值,才影響了Node節點向apiserver更新狀態,導致Node狀態的切換。將相關配置值調大後就解決了這個問題。另外apiserver的api-qps,api-burst等配置也需要根據集群規模以及apiserver的個數做出正確的估量並設置。

再比如說,當Node節點掛掉多久後才允許Kubernetes自動遷移上面的容器,不管是使用node-controller或taint-controller經過適當的配置都可以實現。以及kubelet重啟時,會再一次進行predicates檢查,對於不符合二次檢查要求的Pod會將它們刪除,而如果有些Pod很重要你絕對不希望它們在這種檢查中被刪掉,那麼其實給Pod設置一下對應的annotation就可以實現。關於這樣的配置涉及到很多的細節,因為文件可能沒有更新的那麼及時,最好對於原始碼有一定掌握。

2.2 組件部署

Apiserver使用域名的方式做負載均衡,可以平滑擴展,Controller-manager和Scheduler使用Leader選舉的方式做高可用。同時為了分擔壓力和安全,我們每個集群部署了兩套etcd。一套專門用於event的儲存。另一套儲存其他的資源。

2.3 Node管理

使用標簽管理Node的生命周期,從接管物理機到裝機完成再到服務部署完畢網絡部署完畢NodeReady直至最終下線,每一個步驟都會自動給Node添加對應標簽,以方便自動化運維管理。Node生命周期如下圖:

同時,Node上還添加了區域zone。可以方便一些將一些部門獨立的物理機納入統一管理,同時資源又能保證其獨享。對於一些特殊的資源,比如物理機上有GPU、SSD等特殊資源,對應會給節點打上專屬的標簽用以標識。用戶申請時可以根據需要申請對應的資源,我們在申請的Pod上配屬相應的標簽,從而將其調度至相應的節點。

節點發生故障或者需要下線維護時,首先將該節點置為disable,禁止調度,如果超過一定時間,節點尚未恢復,Controller會自動遷移其上的容器到其他正常的節點上。

2.4 上線流程管理

上線之前制定上線步驟,經相關人員review確認無誤後,嚴格按照上線步驟操作。上線操作按照先截停控制台等入口,而後Controller、Scheduler停止,再停止kubelet。上線結束後按照反向,依次做驗證,啟動。

先截停控制台以及apiserver是為了阻止用戶繼續創建刪除,而後停止Controller和Scheduler對Pod的調度和遷移等操作,最後停止kubelet對Pod生命周期的管理。這樣的順序可以最大程度保證運行pod不受上線過程的影響,否則的話容易造成Controller和Scheduler的誤判,做出錯誤的決策。

2.5 故障演練與應急恢復

為了防止一些極端情況和故障的發生,我們也進行了多次的故障演練,並準備了應急恢復的預案。在這裡我們主要介紹下etcd和apiserver的故障恢復。

2.5.1 etcd的故障恢復

使用etcd恢復的大致流程。在etcd無法恢復情況下,另外啟動一個etcd集群的方式。

etcd在整個集群中的非常重要,一旦有差錯,整個集群都會處於癱瘓狀態,更不要說資料出現丟失的情況。線上etcd集群的運行還是相對穩定的,但是顯然還是要防患於未然,為此我們特地定製了etcd備份和恢復。線上所有集群每隔1小時都會自動做一次備份,並且發郵件通知備份成功與否。恢復則分為原地恢復和利用原集群的資料另外啟動一個集群恢復兩種方式,大致的恢復流程如下:

  • 將原etcd資料目錄備份

  • 另選3台機器,搭建一個全新的etcd集群(帶證書認證)

  • 將新etcd集群的etcd停止,資料目錄下的內容全部刪除

  • 將備份資料拷貝到3台新etcd機器上,使用etcdctl snapshot restore逐個節點恢復資料,註意觀察恢復後id是否一致。資料恢復完成後查看endpoint status狀態是否正常。

2.5.2 apiserver的故障恢復

一般單台apiserver故障,將其進行維護即可。如果apiserver同時發生故障時,會導致Node節點狀態出現異常,此時則需要立刻停掉Controller和Scheduler服務,防止狀態判斷失誤造成的誤決策。在apiserver修複後進行驗證後,再啟動其他組件。

3. 運維工具

3.1 Ansible

JDOS 2.0日常管理的物理機和容器規模龐大,平時的部署和運維如果沒有好用的工具會非常繁瑣,為此我們主要選用Ansible開發了2.0專屬的部署和運維工具,極大的提高了工作效率。

集群部署使用Ansible新搭建集群或者擴容集群或者升級都及其方便,只需要事先把模板寫好,具體操作時執行簡單的命令即可,同時也不用擔心由於操作失誤引發問題。

3.2 Kubernetes Connection Plugin

為了方便操作各個容器,我們還開發了Ansible的Kubernetes插件,可以通過Ansible對容器進行批量的諸如更新密碼、分發檔案、執行命令等操作。

hosts配置:

結果樣例:

3.3 巡檢工具

日常巡檢系統對於及時發現物理機及各個服務的異常配置和狀態非常重要,尤其是大促期間,系統的角落有些許異常可能就帶來及其惡劣的影響,因此特殊時期我們還會加大巡檢的頻率。

巡檢的系統的巡檢模塊都是可插拔的,巡檢點可以根據需求靈活配置,隨時增減,其中一個系統的控制節點的巡檢結果樣例如下:

巡檢結果出現問題,會在巡檢詳報中以紅色字體標示。

3.4 其他工具

為了方便運維統計和監控,我們還開發了一些其他的工具:

  • API日誌分析工具:使用Python對日誌進行預處理,形成結構化資料。而後使用Spark進行統計分析。可以對請求的時間、來源、資源、耗時長短、傳回值等進行分析。

  • kubesql:可以將Kubernetes的如Pod、Service、Node等資源,處理成類似於關係資料庫中的表。這樣就可以使用SQL陳述句對於相關資源進行查詢。比如可以使用SQL陳述句來查詢MySQL、default namespace下的所有Pod的名字。

  • event事件通知:監聽event,並根據event事件進行分級,對於緊急事件接入告警處理,可以通過郵件或者短信通知到相關運維人員。


Q&A;

Q:請問Skynet網絡基於OpenStack Neutron嗎?
A:我們的Kubernetes的網絡是分為兩套。最開始我們使用的是Neutron,因為我們的JDOS 1.0已經穩定運行了多年。今年開始,我們很多資料中心採用的是BGP的網絡。可以參考Calico的實現。

Q:LVM的磁盤IO限制是怎麼做的?

A:這是通過改造kube-apiserver以及kubelet,把磁盤限制的指標也作為一種資源,底層最終使用Cgroup實現的。

Q:巡檢工具是只檢查不修複嗎?

A:是的,巡檢的目的就只是檢查並通知,一般有問題會找運維修複。

Q:使用的什麼Docker storage driver?

A:我們JDOS 1.0是使用的自研的Vdisk,2.0使用的是DM。

Q:為了提升Pod更新速度,我們對容器刪除的流程進行了優化,將CNI接口的呼叫提前到了stop容器,沒太明白這裡。

A:刪除容器的流程原本是stop app容器->呼叫CNI->stop sandbox容器。因為在實際中,stop app容器時間會較長。因此我們將其調整為呼叫CNI->stop app容器->stop sandbox容器。這樣可以更快釋放IP。

Q:有用PV PVC嗎?底層儲存多的什麼技術?

A:有用到PV PVC,底層儲存使用的是我們自研的ContainerFS。目前已經開源在GitHub上,歡迎使用。

Q:請問相同Service的不同Pod上的log,fm,pm怎麼做彙總的?

A:Pod的日誌是在每個節點上,啟動有daemonset的一個容器,負責收集該節點上的日誌,併發送到訊息佇列予以彙總的。

Q:能詳細描述一下“gRPC的問題導致了apiserver的性能瓶頸”的具體內容嗎?

A:在1.6我們原來使用的單個apiserver在服務大概300個節點時,就會大量拒絕請求,出現409錯誤。後來我們查閱了社區的相關資料,發現是gRPC的問題,通過升級gRPC包,可以實現600以上節點無壓力。

Q:請問多IDC的場景你們是如何管理的?

A:目前是分多個資料中心,每個資料中心再劃分多個集群。控制單個集群規模,這樣方便管理。但是鏡像、配置、調度可以在不同資料中心、不同集群間通用。這樣集群和資料中心對用戶透明。

Q:加固環節(包括etcd故障、apiserver全部失聯、apiserver拒絕服務等等極端情況)上面列舉的幾種情況發生時會造成災難性後果嗎,Kubernetes集群的行為會怎樣,有進行演練過不,這塊可以細說一下嗎?

A:當然,如果未經過加固或者不能正常恢復etcd資料,還可能導致pod大量遷移或銷毀,甚至整個集群節點壓力增大,發生雪崩效應,最終整個集群崩潰。

Q:Pod固定IP的使用場景是什麼?有什麼實際意義?

A:呃,這個實際很多業務,特別是一些老業務,是無法做到完全無狀態的。如果不能提供固定IP,那麼他們的配置上線都會很麻煩。

Q:請問系統開發完畢後,下一步有什麼計劃?進入維護優化階段,優秀的設計開發人員下一步怎麼玩?

A:容器化,自動化這才是萬里長征的第一步啊。我們已經在調度方面做了很多的工作,可以參看我們團隊關於阿基米德的一些分享。集群自治與智慧化,我們已經在路上了。歡迎大家一道來實踐。未來我們也會同大家分享這其中的經驗。

Q:應用滾動升級,有無定製?還是採用Kubernetes預設機制?

A:是我們自己定製的deployment,進行了適當的改造,可以支持暫停狀態,比如說更新時,可以指定兩個版本的Pod個數比例,中止在這個中間狀態。

Q:能否介紹一下對GPU支持這塊?

A:GPU我們的玩法其實很簡單,就是一個容器一塊卡,每個卡只分給一個容器。這樣的好處是安全,分配效率高,利用率也比較高。

基於Kubernetes的容器雲平臺實踐培訓

本次培訓包含:Kubernetes核心概念;Kubernetes集群的安裝配置、運維管理、架構規劃;Kubernetes組件、監控、網絡;針對於Kubernetes API接口的二次開發;DevOps基本理念;Docker的企業級應用與運維等,點擊識別下方二維碼加微信好友瞭解具體培訓內容

點擊閱讀原文鏈接即可報名。
赞(0)

分享創造快樂