簡單整理一下編排。
【資料圖】
一個deployment 例子:
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-deploymentspec: selector: matchLabels: app: nginx replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80
然后看下結(jié)果:
出現(xiàn)了兩個pod。
這個 Deployment 定義的編排動作非常簡單,即:確保攜帶了 app=nginx 標(biāo)簽的 Pod 的個數(shù),永遠(yuǎn)等于 spec.replicas 指定的個數(shù),即 2 個。
這就意味著,如果在這個集群中,攜帶 app=nginx 標(biāo)簽的 Pod 的個數(shù)大于 2 的時候,就會有舊的 Pod 被刪除;反之,就會有新的 Pod 被創(chuàng)建。
究竟是 Kubernetes 項目中的哪個組件,在執(zhí)行這些操作呢?
我在前面介紹 Kubernetes 架構(gòu)的時候,曾經(jīng)提到過一個叫作 kube-controller-manager 的組件。
實(shí)際上,這個組件,就是一系列控制器的集合。我們可以查看一下 Kubernetes 項目的 pkg/controller 目錄:
這個目錄下面的每一個控制器,都以獨(dú)有的方式負(fù)責(zé)某種編排功能。而我們的 Deployment,正是這些控制器中的一種。
實(shí)際上,這些控制器之所以被統(tǒng)一放在 pkg/controller 目錄下,就是因?yàn)樗鼈兌甲裱?Kubernetes 項目中的一個通用編排模式,即:控制循環(huán)(control loop)。
比如,現(xiàn)在有一種待編排的對象 X,它有一個對應(yīng)的控制器。那么,我就可以用一段 Go 語言風(fēng)格的偽代碼,為你描述這個控制循環(huán):
for { 實(shí)際狀態(tài) := 獲取集群中對象 X 的實(shí)際狀態(tài)(Actual State) 期望狀態(tài) := 獲取集群中對象 X 的期望狀態(tài)(Desired State) if 實(shí)際狀態(tài) == 期望狀態(tài){ 什么都不做 } else { 執(zhí)行編排動作,將實(shí)際狀態(tài)調(diào)整為期望狀態(tài) }}
在具體實(shí)現(xiàn)中,實(shí)際狀態(tài)往往來自于 Kubernetes 集群本身。
比如,kubelet 通過心跳匯報的容器狀態(tài)和節(jié)點(diǎn)狀態(tài),或者監(jiān)控系統(tǒng)中保存的應(yīng)用監(jiān)控數(shù)據(jù),或者控制器主動收集的它自己感興趣的信息,這些都是常見的實(shí)際狀態(tài)的來源。
而期望狀態(tài),一般來自于用戶提交的 YAML 文件。
比如,Deployment 對象中 Replicas 字段的值。很明顯,這些信息往往都保存在 Etcd 中。
接下來,以 Deployment 為例,我和你簡單描述一下它對控制器模型的實(shí)現(xiàn):
Deployment 控制器從 Etcd 中獲取到所有攜帶了“app: nginx”標(biāo)簽的 Pod,然后統(tǒng)計它們的數(shù)量,這就是實(shí)際狀態(tài);
Deployment 對象的 Replicas 字段的值就是期望狀態(tài);
Deployment 控制器將兩個狀態(tài)做比較,然后根據(jù)比較結(jié)果,確定是創(chuàng)建 Pod,還是刪除
這個操作,通常被叫作調(diào)諧(Reconcile)。這個調(diào)諧的過程,則被稱作“Reconcile Loop”(調(diào)諧循環(huán))或者“Sync Loop”(同步循環(huán))。
其中,這個控制器對象本身,負(fù)責(zé)定義被管理對象的期望狀態(tài)。比如,Deployment 里的 replicas=2 這個字段。
而被控制對象的定義,則來自于一個“模板”。比如,Deployment 里的 template 字段。
可以看到,Deployment 這個 template 字段里的內(nèi)容,跟一個標(biāo)準(zhǔn)的 Pod 對象的 API 定義,絲毫不差。而所有被這個 Deployment 管理的 Pod 實(shí)例,其實(shí)都是根據(jù)這個 template 字段的內(nèi)容創(chuàng)建出來的。
像 Deployment 定義的 template 字段,在 Kubernetes 項目中有一個專有的名字,叫作 PodTemplate(Pod 模板)。
這就是為什么,在所有 API 對象的 Metadata 里,都有一個字段叫作 ownerReference,用于保存當(dāng)前這個 API 對象的擁有者(Owner)的信息。
現(xiàn)在我們知道了,Deployment 可以控制pod。
那么是不是deployment 直接控制了pod呢? 是不是pod 直接向deployment 匯報呢?
答案是不是,中間還有一個replicaset。
apiVersion: apps/v1kind: ReplicaSetmetadata: name: nginx-set labels: app: nginxspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9
創(chuàng)建一個replicaset。
創(chuàng)建一個replicaset,里面指定了個數(shù)是3個,
從這個 YAML 文件中,我們可以看到,一個 ReplicaSet 對象,其實(shí)就是由副本數(shù)目的定義和一個 Pod 模板組成的。不難發(fā)現(xiàn),它的定義其實(shí)是 Deployment 的一個子集。
更重要的是,Deployment 控制器實(shí)際操縱的,正是這樣的 ReplicaSet 對象,而不是 Pod 對象。
再來看上面這個例子:
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-deployment labels: app: nginxspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80
可以看到,這就是一個我們常用的 nginx-deployment,它定義的 Pod 副本個數(shù)是 3(spec.replicas=3)。
那么,在具體的實(shí)現(xiàn)上,這個 Deployment,與 ReplicaSet,以及 Pod 的關(guān)系是怎樣的呢?
我們可以用一張圖把它描述出來:
通過這張圖,我們就很清楚的看到,一個定義了 replicas=3 的 Deployment,與它的 ReplicaSet,以及 Pod 的關(guān)系,實(shí)際上是一種“層層控制”的關(guān)系。
其中,ReplicaSet 負(fù)責(zé)通過“控制器模式”,保證系統(tǒng)中 Pod 的個數(shù)永遠(yuǎn)等于指定的個數(shù)(比如,3 個)。這也正是 Deployment 只允許容器的 restartPolicy=Always 的主要原因:只有在容器能保證自己始終是 Running 狀態(tài)的前提下,ReplicaSet 調(diào)整 Pod 的個數(shù)才有意義。
而在此基礎(chǔ)上,Deployment 同樣通過“控制器模式”,來操作 ReplicaSet 的個數(shù)和屬性,進(jìn)而實(shí)現(xiàn)“水平擴(kuò)展 / 收縮”和“滾動更新”這兩個編排動作。
其中,“水平擴(kuò)展 / 收縮”非常容易實(shí)現(xiàn),Deployment Controller 只需要修改它所控制的 ReplicaSet 的 Pod 副本個數(shù)就可以了。
比如,把這個值從 3 改成 4,那么 Deployment 所對應(yīng)的 ReplicaSet,就會根據(jù)修改后的值自動創(chuàng)建一個新的 Pod。這就是“水平擴(kuò)展”了;“水平收縮”則反之。
而用戶想要執(zhí)行這個操作的指令也非常簡單,就是 kubectl scale,比如:
kubectl scale deployment nginx-deployment --replicas=4
DESIRED:用戶期望的 Pod 副本個數(shù)(spec.replicas 的值);
CURRENT:當(dāng)前處于 Running 狀態(tài)的 Pod 的個數(shù);
UP-TO-DATE:當(dāng)前處于最新版本的 Pod 的個數(shù),所謂最新版本指的是 Pod 的 Spec 部分與 Deployment 里 Pod 模板里定義的完全一致;
AVAILABLE:當(dāng)前已經(jīng)可用的 Pod 的個數(shù),即:既是 Running 狀態(tài),又是最新版本,并且已經(jīng)處于 Ready(健康檢查正確)狀態(tài)的 Pod 的個數(shù)。
可以看到,只有這個 AVAILABLE 字段,描述的才是用戶所期望的最終狀態(tài)。
而 Kubernetes 項目還為我們提供了一條指令,讓我們可以實(shí)時查看 Deployment 對象的狀態(tài)變化。這個指令就是 kubectl rollout status.
如上所示,在用戶提交了一個 Deployment 對象后,Deployment Controller 就會立即創(chuàng)建一個 Pod 副本個數(shù)為 3 的 ReplicaSet。這個 ReplicaSet 的名字,則是由 Deployment 的名字和一個隨機(jī)字符串共同組成。
這個隨機(jī)字符串叫作 pod-template-hash,在我們這個例子里就是:3167673210。ReplicaSet 會把這個隨機(jī)字符串加在它所控制的所有 Pod 的標(biāo)簽里,從而保證這些 Pod 不會與集群里的其他 Pod 混淆。
而 ReplicaSet 的 DESIRED、CURRENT 和 READY 字段的含義,和 Deployment 中是一致的。所以,相比之下,Deployment 只是在 ReplicaSet 的基礎(chǔ)上,添加了 UP-TO-DATE 這個跟版本有關(guān)的狀態(tài)字段。
這個時候,如果我們修改了 Deployment 的 Pod 模板,“滾動更新”就會被自動觸發(fā)。
修改 Deployment 有很多方法。比如,我可以直接使用 kubectl edit 指令編輯 Etcd 里的 API 對象。
我們可以直接修改:
kubectl edit deployment/nginx-deployment
這個 kubectl edit 指令,會幫你直接打開 nginx-deployment 的 API 對象。然后,你就可以修改這里的 Pod 模板部分了。比如,在這里,我將 nginx 鏡像的版本升級到了 1.9.1。
備注:kubectl edit 并不神秘,它不過是把 API 對象的內(nèi)容下載到了本地文件,讓你修改完成后再提交上去。
kubectl edit 指令編輯完成后,保存退出,Kubernetes 就會立刻觸發(fā)“滾動更新”的過程。你還可以通過 kubectl rollout status 指令查看 nginx-deployment 的狀態(tài)變化:
出現(xiàn)了兩個rs。
其中,舊 ReplicaSet已經(jīng)被“水平收縮”成了 0 個副本, 怎么看它是舊的呢? 看age 時間。
這時,你可以通過查看 Deployment 的 Events,看到這個“滾動更新”的流程:
這種“滾動更新”的好處是顯而易見的。
比如,在升級剛開始的時候,集群里只有 1 個新版本的 Pod。如果這時,新版本 Pod 有問題啟動不起來,那么“滾動更新”就會停止,從而允許開發(fā)和運(yùn)維人員介入。而在這個過程中,由于應(yīng)用本身還有兩個舊版本的 Pod 在線,所以服務(wù)并不會受到太大的影響。
當(dāng)然,這也就要求你一定要使用 Pod 的 Health Check 機(jī)制檢查應(yīng)用的運(yùn)行狀態(tài),而不是簡單地依賴于容器的 Running 狀態(tài)。要不然的話,雖然容器已經(jīng)變成 Running 了,但服務(wù)很有可能尚未啟動,“滾動更新”的效果也就達(dá)不到了。
而為了進(jìn)一步保證服務(wù)的連續(xù)性,Deployment Controller 還會確保,在任何時間窗口內(nèi),只有指定比例的 Pod 處于離線狀態(tài)。同時,它也會確保,在任何時間窗口內(nèi),只有指定比例的新 Pod 被創(chuàng)建出來。這兩個比例的值都是可以配置的,默認(rèn)都是 DESIRED 值的 25%。
所以,在上面這個 Deployment 的例子中,它有 3 個 Pod 副本,那么控制器在“滾動更新”的過程中永遠(yuǎn)都會確保至少有 2 個 Pod 處于可用狀態(tài),至多只有 4 個 Pod 同時存在于集群中。這個策略,是 Deployment 對象的一個字段,名叫 RollingUpdateStrategy,如下所示:
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-deployment labels: app: nginxspec:... strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 1
在上面這個 RollingUpdateStrategy 的配置中,maxSurge 指定的是除了 DESIRED 數(shù)量之外,在一次“滾動”中,Deployment 控制器還可以創(chuàng)建多少個新 Pod;而 maxUnavailable 指的是,在一次“滾動”中,Deployment 控制器可以刪除多少個舊 Pod。
同時,這兩個配置還可以用前面我們介紹的百分比形式來表示,比如:maxUnavailable=50%,指的是我們最多可以一次刪除“50%*DESIRED 數(shù)量”個 Pod。
如上所示,Deployment 的控制器,實(shí)際上控制的是 ReplicaSet 的數(shù)目,以及每個 ReplicaSet 的屬性。
而一個應(yīng)用的版本,對應(yīng)的正是一個 ReplicaSet;這個版本應(yīng)用的 Pod 數(shù)量,則由 ReplicaSet 通過它自己的控制器(ReplicaSet Controller)來保證。
通過這樣的多個 ReplicaSet 對象,Kubernetes 項目就實(shí)現(xiàn)了對多個“應(yīng)用版本”的描述。
而明白了“應(yīng)用版本和 ReplicaSet 一一對應(yīng)”的設(shè)計思想之后,我就可以為你講解一下Deployment 對應(yīng)用進(jìn)行版本控制的具體原理了。
這一次,我會使用一個叫kubectl set image的指令,直接修改 nginx-deployment 所使用的鏡像。這個命令的好處就是,你可以不用像 kubectl edit 那樣需要打開編輯器。
不過這一次,我把這個鏡像名字修改成為了一個錯誤的名字,比如:nginx:1.91。這樣,這個 Deployment 就會出現(xiàn)一個升級失敗的版本。
通過這個返回結(jié)果,我們可以看到,新版本的 ReplicaSet(hash=2156724341)的“水平擴(kuò)展”已經(jīng)停止。而且此時,它已經(jīng)創(chuàng)建了兩個 Pod,但是它們都沒有進(jìn)入 READY 狀態(tài)。這當(dāng)然是因?yàn)檫@兩個 Pod 都拉取不到有效的鏡像。
與此同時,舊版本的 ReplicaSet 的“水平收縮”,也自動停止了。此時,已經(jīng)有一個舊 Pod 被刪除,還剩下3個舊 Pod。
那么怎么回滾呢?
這樣就回到了4個。
很容易想到,在具體操作上,Deployment 的控制器,其實(shí)就是讓這個舊 ReplicaSet(hash=1764197365)再次“擴(kuò)展”成 4 個 Pod,而讓新的 ReplicaSet(hash=2156724341)重新“收縮”到 0 個 Pod。
首先,我需要使用 kubectl rollout history 命令,查看每次 Deployment 變更對應(yīng)的版本。
當(dāng)然,你還可以通過這個 kubectl rollout history 指令,看到每個版本對應(yīng)的 Deployment 的 API 對象的細(xì)節(jié),具體命令如下所示:
kubectl rollout undo deployment/nginx-deployment --to-revision=2
這樣,Deployment Controller 還會按照“滾動更新”的方式,完成對 Deployment 的降級操作。
不過,你可能已經(jīng)想到了一個問題:我們對 Deployment 進(jìn)行的每一次更新操作,都會生成一個新的 ReplicaSet 對象,是不是有些多余,甚至浪費(fèi)資源呢?
沒錯。
所以,Kubernetes 項目還提供了一個指令,使得我們對 Deployment 的多次更新操作,最后 只生成一個 ReplicaSet。
具體的做法是,在更新 Deployment 前,你要先執(zhí)行一條 kubectl rollout pause 指令。它的用法如下所示:
kubectl rollout pause deployment/nginx-deployment
這個 kubectl rollout pause 的作用,是讓這個 Deployment 進(jìn)入了一個“暫?!睜顟B(tài)。
所以接下來,你就可以隨意使用 kubectl edit 或者 kubectl set image 指令,修改這個 Deployment 的內(nèi)容了。
由于此時 Deployment 正處于“暫?!睜顟B(tài),所以我們對 Deployment 的所有修改,都不會觸發(fā)新的“滾動更新”,也不會創(chuàng)建新的 ReplicaSet。
而等到我們對 Deployment 修改操作都完成之后,只需要再執(zhí)行一條 kubectl rollout resume 指令,就可以把這個 Deployment“恢復(fù)”回來,如下所示:
kubectl rollout resume deploy/nginx-deployment
而在這個 kubectl rollout resume 指令執(zhí)行之前,在 kubectl rollout pause 指令之后的這段時間里,我們對 Deployment 進(jìn)行的所有修改,最后只會觸發(fā)一次“滾動更新”。
當(dāng)然,我們可以通過檢查 ReplicaSet 狀態(tài)的變化,來驗(yàn)證一下 kubectl rollout pause 和 kubectl rollout resume 指令的執(zhí)行效果,如下所示:
不過,即使你像上面這樣小心翼翼地控制了 ReplicaSet 的生成數(shù)量,隨著應(yīng)用版本的不斷增加,Kubernetes 中還是會為同一個 Deployment 保存很多很多不同的 ReplicaSet。
那么,我們又該如何控制這些“歷史”ReplicaSet 的數(shù)量呢?
很簡單,Deployment 對象有一個字段,叫作 spec.revisionHistoryLimit,就是 Kubernetes 為 Deployment 保留的“歷史版本”個數(shù)。所以,如果把它設(shè)置為 0,你就再也不能做回滾操作了。
下一節(jié)容器化守護(hù)進(jìn)程 DaemonSet.
關(guān)鍵詞: