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

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)

分享創造快樂