來源 / 譯者 | 偽架構(gòu)師 原文作者 | Jér?me Petazzoni
要把容器化的應(yīng)用部署起來?在 Kubernetes 中部署容器化應(yīng)用,總要涉及到 Deployment,這里有這個(gè)對(duì)象的所有內(nèi)容。 我們最早學(xué)會(huì)的 Kubernetes 命令之一就是 kubectl run。具備 Docker 經(jīng)驗(yàn)的用戶,不免會(huì)用 docker run 命令和這個(gè)命令進(jìn)行對(duì)比,結(jié)論可能是:運(yùn)行容器就是這么簡單。 我們來看看,在運(yùn)行一個(gè)基本的 kubectl run 命令的時(shí)候,都發(fā)生了些什么: $ kubectl run web --image=nginx deployment.apps/web created
集群中創(chuàng)建了什么? $ kubectl get all NAME READY STATUS RESTARTS AGE pod/web-65899c769f-dhtdx 1/1 Running 0 11s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 46s
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE deployment.apps/web 1 1 1 1 11s
NAME DESIRED CURRENT READY AGE replicaset.apps/web-65899c769f 1 1 1 11s
我們并沒有看到容器,而是一組未知對(duì)象: - ReplicaSet:web-65899c769f
此處的 kubernetes 服務(wù)可以忽略,它在我們運(yùn)行命令之前就已經(jīng)存在了。 我只想要個(gè)容器!為什么看到了三個(gè)不同的對(duì)象?簡單說來,這些 Kubernetes 對(duì)象能在不停服務(wù)的情況下,為應(yīng)用提供漸進(jìn)式部署、回滾以及伸縮的支持。初次見面難免會(huì)好奇:究竟是怎么回事?在了解這些問題之后,就會(huì)理解每個(gè)對(duì)象的角色和存在價(jià)值了。持續(xù)集成提升了對(duì)代碼的信心。要把這種信心擴(kuò)展到發(fā)布流程之中,部署操作就需要更多保障。在 Kubernetes 中,一個(gè) Deployment 的最小單元不是容器,而是 Pod。Pod 是一組容器(當(dāng)然這一組也可以只有一個(gè)),它們運(yùn)行在同一臺(tái)服務(wù)器中,并共享一些資源。例如 Pod 中的容器能夠通過 localhost 互相通信。在網(wǎng)絡(luò)視角中,這些容器中的所有進(jìn)程都是本地的。但是我們永遠(yuǎn)無法創(chuàng)建獨(dú)立的容器:最相近的操作也只能是創(chuàng)建一個(gè)僅包含單一容器的一個(gè) Pod。我們想讓 Kubernetes 創(chuàng)建 NGINX,完整的臺(tái)詞是:“我要一個(gè) Pod,其中只包含一個(gè)容器,這個(gè)容器運(yùn)行的是 nginx 鏡像”。# pod-nginx.yml # Create it with: # kubectl apply -f pod-nginx.yml apiVersion: v1 kind: Pod metadata: name: web spec: containers: - image: nginx name: nginx ports: - containerPort: 80 name: http
這就只有一個(gè) Pod,那 ReplicaSet 和 Deployment 是怎么回事?Kubernetes 是一個(gè)聲明式系統(tǒng)(和指令式系統(tǒng)相對(duì)),這就意味著我們無法給它發(fā)出命令。我們不能說:“運(yùn)行這個(gè)容器”。我們能做的只能是——描述我們需要的東西,然后等 Kubernetes 根據(jù)現(xiàn)有內(nèi)容,同步為預(yù)期內(nèi)容。打個(gè)比方,我們可以說:“我要一個(gè) 40 英尺高的有黃色門的藍(lán)色容器”,Kubernetes 會(huì)為我們查找這種容器,如果找不到,就會(huì)創(chuàng)建一個(gè);如果已經(jīng)有了,但它是綠色紅門的,Kubernetes 就會(huì)幫我們上色;如果已經(jīng)有了完全符合要求的容器,因?yàn)楝F(xiàn)有內(nèi)容和預(yù)期內(nèi)容一致,所以 Kubernetes 什么都不會(huì)做。回到軟件容器的話題,我們可以說:“我想要一個(gè)名字叫 web 的 Pod,其中應(yīng)該有單獨(dú)的容器,運(yùn)行的是 nginx 鏡像”。如果這個(gè) Pod 不存在,Kubernetes 會(huì)創(chuàng)建出來。如果符合我們要求的 Pod 已經(jīng)存在,Kubernetes 無需進(jìn)行任何動(dòng)作。基于這種思路,怎樣對(duì) web 應(yīng)用進(jìn)行伸縮,來滿足多容器或 Pod 的運(yùn)行需要呢?如果我們只有一個(gè) Pod,我們想要更多的同樣的 Pod,我們可能會(huì)給 Kubernetes 提出這樣的要求:“我們需要一個(gè)叫做 web2 的 Pod,具體要求是:…”,然后重復(fù)之前的 Pod 規(guī)范。想要多少 Pod,就重復(fù)執(zhí)行多少次。這明顯很不方便,我們要自己跟蹤所有的 Pod,確保它們都同步了正確的狀態(tài),并符合特定的規(guī)范。Kubernetes 提供了高級(jí)一些的抽象來簡化這個(gè)過程:ReplicaSet。ReplicaSet 的對(duì)象結(jié)構(gòu)和 Pod 很相似,只不過它還有個(gè)副本數(shù)量的字段,用于描述我們所需要的符合規(guī)范的 Pod 數(shù)量。有了 ReplicaSet,我們就可以告訴 Kubernetes:“我需要一個(gè)叫做 web 的 ReplicaSet,其中包含 3 個(gè) Pod,這些 Pod 符合如下規(guī)范:……”,Kubernetes 會(huì)根據(jù)這個(gè)指令來確認(rèn),是不是剛好有三個(gè)符合規(guī)范的 Pod。如果我們從頭開始,就會(huì)創(chuàng)建這 3 個(gè) Pod。如果已經(jīng)有了 3 個(gè) Pod,什么事都不會(huì)發(fā)生——我們的要求和現(xiàn)狀一致。# pod-replicas.yml apiVersion: apps/v1 kind: ReplicaSet metadata: name: web-replicas labels: app: web tier: frontend spec: replicas: 3 selector: matchLabels: tier: frontend template: metadata: labels: app: web tier: frontend spec: containers: - name: nginx image: nginx ports: - containerPort: 80
我們可以修改現(xiàn)存 ReplicaSet 的副本數(shù)量,以此來完成伸縮。Kubernetes 會(huì)根據(jù)伸縮指令來創(chuàng)建或刪除 Pod,讓 Pod 數(shù)量符合要求。高可用方面,因?yàn)?Kubernetes 會(huì)持續(xù)的對(duì)集群進(jìn)行監(jiān)控,確保無論什么情況下都保有指定數(shù)量的運(yùn)行實(shí)例。如果節(jié)點(diǎn)當(dāng)機(jī),恰好其中有一個(gè) web 所屬的 Pod,Kubernetes 會(huì)另外創(chuàng)建一個(gè) Pod 來替換它。如果節(jié)點(diǎn)沒有當(dāng)機(jī),不過是有一段時(shí)間無法聯(lián)系或者沒有響應(yīng),那么它再次恢復(fù)可用之后,就會(huì)多出一個(gè) Pod,Kubernetes 會(huì)中止一個(gè) Pod 來保證數(shù)量符合要求。修改 Pod 定義并不罕見。比如我們經(jīng)常會(huì)希望把容器鏡像替換為新版本。記住:ReplicaSet 的使命是,“確保有 N 個(gè)符合規(guī)范的 Pod?!比绻覀冃薷牧硕x,會(huì)發(fā)生什么呢——突然就沒有符合新規(guī)范的 Pod 了。寫到這里,我們已經(jīng)知道了聲明式系統(tǒng)的工作方式:Kubernetes 會(huì)立刻創(chuàng)建 N 個(gè)符合新規(guī)范的 Pod。舊的 Pod 會(huì)一致存在,直到我們手工清理。如果能用 CI/CD 對(duì)這些過期 Pod 做一個(gè)自動(dòng)清理可能不錯(cuò);如果新 Pod 的創(chuàng)建能用更優(yōu)雅的方式也會(huì)更好。Deployment 驅(qū)動(dòng)的 ReplicaSet前面說的需要就是 Deployment 的職責(zé)。粗看上去,Deployment 的規(guī)范和 ReplicaSet 很像:其中包含了 Pod 規(guī)范,以及副本數(shù)量。(還有一些后面會(huì)討論的參數(shù))# deployment-nginx.yml apiVersion: apps/v1 kind: Deployment metadata: name: web spec: selector: matchLabels: app: nginx replicas: 3 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80
Deployment 并不會(huì)直接負(fù)責(zé) Pod 的創(chuàng)建和刪除。它會(huì)把這些工作委托給一個(gè)或多個(gè) ReplicaSet。在我們創(chuàng)建 Deployment 的時(shí)候,它會(huì)用自己的 Pod 規(guī)范創(chuàng)建一個(gè) ReplicaSet。當(dāng)更新一個(gè) Deployment 并修改副本數(shù)量時(shí),它會(huì)把更新內(nèi)容傳遞給下游的 ReplicaSet。需要更新 Pod 規(guī)范的時(shí)候,事情就有意思了。例如我們可能需要使用新版本的鏡像(因?yàn)槲覀儼l(fā)布了新的版本),或者修改應(yīng)用的參數(shù)(通過命令行參數(shù)、環(huán)境變量或者配置文件)。在我們更新 Pod 規(guī)范時(shí),Deployment 會(huì)用新的 Pod 規(guī)范創(chuàng)建新的 ReplicaSet。新的 ReplicaSet 的初始實(shí)例數(shù)量是 0。接下來 ReplicaSet 的實(shí)例數(shù)量會(huì)逐步提升,同時(shí)逐漸減少另一個(gè) ReplicaSet 的尺寸。可以想象一下,面前有個(gè)混音臺(tái),我們要讓新的 ReplicaSet 淡入,同時(shí)把舊的那個(gè)淡出。整個(gè)過程之中,請(qǐng)求被發(fā)送給新舊兩個(gè) ReplicaSet,用戶不會(huì)感覺服務(wù)中斷。全景大致如此,其中還有很多小細(xì)節(jié),讓整個(gè)過程更加健壯。損壞的 Deployment 以及就緒檢測(cè)如果我們推出了一個(gè)故障版本,因?yàn)?Kubernetes 會(huì)持續(xù)把舊 Pod 替換成新的(故障)版本,它可能會(huì)讓整個(gè)應(yīng)用壞掉(逐個(gè) Pod)。就緒檢測(cè)是在容器規(guī)范中加入的一個(gè)測(cè)試過程。他是一個(gè)二進(jìn)制測(cè)試,結(jié)果只有兩個(gè)“能行”或者“不行”,這個(gè)測(cè)試會(huì)以指定的間隔被執(zhí)行(缺省情況下是每 10 秒)。Kubernetes 支持三種方式的就緒檢測(cè):- 在容器內(nèi)運(yùn)行一個(gè)命令;
- 向容器發(fā)出一個(gè) HTTP(S) 請(qǐng)求;
Kubernetes 會(huì)通過測(cè)試結(jié)果來了解容器及其所處 Pod 是否準(zhǔn)備就緒可以接受流量。在我們推出新版本時(shí),Kubernetes 會(huì)等到新 Pod 測(cè)試得到“就緒”結(jié)果之后,才會(huì)進(jìn)入下一步。如果一個(gè) Pod 因?yàn)榫途w檢測(cè)持續(xù)失敗,永遠(yuǎn)無法進(jìn)入就緒狀態(tài),Kubernetes 也不會(huì)進(jìn)入下一步。部署過程會(huì)停止,應(yīng)用會(huì)繼續(xù)使用老版本運(yùn)行,直到我們解決了問題。如果沒有就緒檢測(cè),那么這個(gè)容器成功啟動(dòng)后就會(huì)被當(dāng)成是就緒狀態(tài)。所以最好能使用就緒檢測(cè)來保障業(yè)務(wù)。使用 Rollback 來從故障版本中快速恢復(fù)在滾動(dòng)更新過程中或之后的任何時(shí)間,我們都可以告訴 Kubernetes:“我改主意了,請(qǐng)回到這個(gè) Deployment 的前一個(gè)版本?!?,這個(gè)操作會(huì)切換新舊 ReplicaSet 的地位。在這個(gè)點(diǎn)開始,會(huì)提高舊版 ReplicaSet 的實(shí)例數(shù)量到指定數(shù)值,同時(shí)降低新版的的實(shí)例數(shù)量。一般來說,并不限于新舊兩個(gè) ReplicaSet。歸根結(jié)底,有一個(gè) ReplicaSet 被視為“最新”版本,我們可以將這個(gè)版本作為目標(biāo) ReplicaSet,所謂目標(biāo),就是我們希望運(yùn)行的,也是 Kubernetes 會(huì)逐步拉起的一個(gè)版本。同時(shí)也可以有任意多個(gè)其它版本的 ReplicaSet,對(duì)應(yīng)舊版本。例如我們?cè)谶\(yùn)行 10 個(gè)副本的版本 1 應(yīng)用,然后開始推出版本 2。在某個(gè)時(shí)間點(diǎn),我們可能有了 7 個(gè)版本 1、3 個(gè)版本 2 的 Pod 正在運(yùn)行。如果我們不想等版本 2 完全推出,決定推出版本 3。在版本 3 部署的時(shí)候,我們又想回到版本 1。整個(gè)過程,Kubernetes 都會(huì)根據(jù)需要對(duì)各個(gè)版本的 ReplicaSet 中的副本數(shù)量進(jìn)行調(diào)整。MaxSurge 和 MaxUnavailableKubernetes 不一定是一次更新一個(gè) Pod 的。之前我們提到 Deployment 還有一些額外的參數(shù),這些參數(shù)中包括了 MaxSurge 和 MaxUnavailable,這兩個(gè)參數(shù)決定了更新過程的速度。- 我們可能對(duì)應(yīng)用的可用性非常謹(jǐn)慎,因此決定在關(guān)閉舊版本 Pod 之前,首先要啟動(dòng)新 Pod。只有新 Pod 啟動(dòng)、運(yùn)行并就緒之后,才終結(jié)舊 Pod。
- 上這個(gè)假設(shè)中有個(gè)隱含條件就是我們的集群中是有剩余資源的。然而如果我們的集群已經(jīng)滿載,無法負(fù)擔(dān)多余 Pod 的消耗,那么我們自然是希望首先關(guān)掉舊的,然后才啟動(dòng)新的。
MaxSurge 指出了我們?cè)跐L動(dòng)更新時(shí),可以有多少個(gè)額外的 Pod;而 MaxUnavailable 則代表在滾動(dòng)更新時(shí),我們可以忍受多少個(gè) Pod 無法提供服務(wù)。這兩個(gè)參數(shù)可以是 Pod 數(shù)量,也可以是 Deployment 的實(shí)例數(shù)量百分比;兩個(gè)參數(shù)都可以設(shè)置為 0(但是不能同時(shí)為 0)。接下來看看這兩個(gè)參數(shù)的常見取值,以及背后的意圖。MaxUnavailable 設(shè)置為 0 意味著:“在新 Pod 啟動(dòng)并就緒之前,不要關(guān)閉任何舊 Pod”。MaxSurge 設(shè)置為 100% 的意思是:“立即啟動(dòng)所有新 Pod”,也就是說我們有足夠的資源,我們希望盡快完成更新。這兩個(gè)參數(shù)的卻升值都是 25%,如果我們更新一個(gè) 100 Pod 的 Deployment,會(huì)立刻創(chuàng)建 25 個(gè)新 Old,同時(shí)會(huì)關(guān)閉 25 個(gè)舊 Pod。每次有 Pod 啟動(dòng)就緒,就可以關(guān)閉舊 Pod。每次有舊 Pod 完成關(guān)閉過程(釋放資源),就可以創(chuàng)建另一個(gè)新 Pod 了。可以很方便的觀察這些參數(shù)的作用。我們不需要編寫自己的 YAML、定義就緒檢測(cè)等東西。我們需要做的事情只是,使用一個(gè)無效的鏡像,例如一個(gè)不存在的鏡像。這個(gè)容器永遠(yuǎn)無法啟動(dòng),Kubernetes 也永遠(yuǎn)無法把它標(biāo)記為就緒。如果你有個(gè) Kubernewtes 集群(Minikube 或者 Docker 桌面版的單結(jié)點(diǎn)集群都可以),可以在不同終端運(yùn)行下面的命令,來看看發(fā)生了什么:kubectl get pods -w kubectl get replicasets -w kubectl get deployments -w kubectl get events -w 然后用下面的命令來創(chuàng)建、伸縮以及更新一個(gè) Deployment:kubectl run deployment web --image=nginx kubectl scale deployment web --replicas=10 kubectl set image deployment web nginx=that-image-does-not-exist
會(huì)看到部署過程停頓了,但是還有 80% 的應(yīng)用容量是可用的。如果我們運(yùn)行 kubectl rollout undo deployment web,Kubernetes 就會(huì)回滾到使用 nginx 鏡像的舊版本。前面我們說過,ReplicaSet 的任務(wù)是確保有 N 個(gè)符合規(guī)范的 Pod。這其實(shí)并不完全。實(shí)際上 ReplicaSet 并不關(guān)心 Pod 的規(guī)范,它關(guān)心的只是標(biāo)簽。換句話說,不論 Pod 運(yùn)行的是 nginx 還是 redis 還是什么別的什么東西;所有的關(guān)注點(diǎn)都是,它們要有正確的標(biāo)簽。前面的例子中,標(biāo)簽大概是 run=web 以及 pod-template-hash=xxxyyyzzz 的形式。ReplicaSet 包含了一個(gè) selector 成員,內(nèi)容是一個(gè)邏輯表達(dá)式,功能和 SQL 中的 SELECT 類似,用來選擇符合要求的 Pod。ReplicaSet 保證 Pod 的數(shù)量正確,如有必要,就會(huì)新建或者刪除 Pod,但是不會(huì)修改已經(jīng)存在的 Pod。這樣會(huì)有個(gè)設(shè)想:可能可以手工創(chuàng)建帶有這些標(biāo)簽的 Pod ,但是卻用的不同鏡像(或者不同配置),就能騙過 ReplicaSet 了。粗看上去,這可能是個(gè)很大的潛在問題。但實(shí)際上,我們很難恰巧選擇了正確的標(biāo)簽,這是因?yàn)闃?biāo)簽中包含了根據(jù) Pod 規(guī)范運(yùn)算得出的哈希值。選擇器還用在 Service 上,這個(gè)對(duì)象負(fù)責(zé) Kubernetes 的內(nèi)外部的負(fù)載均衡。我們可以給 web 創(chuàng)建一個(gè) Service:kubectl expose deployment web --port=80
這個(gè)服務(wù)會(huì)有它自己的內(nèi)部 IP 地址(ClusterIP),連接到這個(gè)地址的 80 端口會(huì)被負(fù)載均衡到這個(gè) Deployment 所有 Pod 之中。事實(shí)上這個(gè)連接的負(fù)載均衡范圍是所有符合 Service 標(biāo)簽選擇器的 Pod 中,例如這里對(duì)應(yīng)的是 run=web。在我們編輯 Deployment 并觸發(fā)滾動(dòng)時(shí),就會(huì)創(chuàng)建新的 ReplicaSet。這個(gè) ReplicaSet 會(huì)創(chuàng)建 Pod,新 Pod 標(biāo)簽會(huì)包含 run=web,所以這些 Pod 就會(huì)自動(dòng)的接到流量。這表明在滾動(dòng)更新時(shí),Deployment 不需要因?yàn)?Pod 的的啟動(dòng)停止,而去重新配置或者通知負(fù)載均衡器。負(fù)載均衡器通過 selector 自動(dòng)的完成任務(wù)。如果你好奇就緒檢測(cè)的內(nèi)幕:Pod 只有在所有成員容器都通過就緒檢測(cè)之后才會(huì)作為有效的 Endpoint被加入服務(wù)。換句話說,Pod 只有準(zhǔn)備就緒之后才會(huì)開始接收流量。有些事后我們希望在推出新版本時(shí)候還有更多的控制。兩個(gè)知名流行技術(shù)是藍(lán)綠部署以及金絲雀部署。Kubernetes 中的藍(lán)綠部署在藍(lán)綠部署中,我們希望立即把所有流量從舊版本切換到新版本,而不是象之前說的漸進(jìn)切換。提出這種要求可能有幾個(gè)原因:- 我們不想混合新舊請(qǐng)求,希望能夠盡可能清晰的從舊版本切換到新版本;
- 我們正在更新多個(gè)組件(例如 Web 前端和 API 后端),不想新版本前端和舊版后端發(fā)生聯(lián)系;
- 如果出現(xiàn)問題,我們希望有能力盡快回滾,無需等舊版本容器重啟。
在 Kubernetes 中,可以用創(chuàng)建多個(gè) Deployment 的方式來完成藍(lán)綠部署,通過對(duì) Service 的 Selector 字段的控制來進(jìn)行切換。下面的命令會(huì)創(chuàng)建兩個(gè) Deployment:blue 和 green,分別使用 nginx 和 httpd 鏡像:kubectl create deployment blue --image=nginx kubectl create deployment green --image=httpd
接下來我們創(chuàng)建一個(gè) Service,起初不會(huì)發(fā)送任何流量:kubectl create service clusterip web --tcp=80
然后我們更新 web 服務(wù)的選擇器:kubectl edit service web。這個(gè)命令會(huì)從 Kunernetes API 中抓取服務(wù)對(duì)象的定義,在文本編輯器中打開。在其中查找:把其中的 web 替換成 blue 或者 green 或者別的什么。保存并退出。kubectl 會(huì)把更新的定義推送給 Kubernetes API,然后 web 服務(wù)現(xiàn)在就會(huì)向特定的 Deployment 發(fā)送流量了。可以用 kubectl get svc web 命令獲取服務(wù)的地址,并使用 curl 進(jìn)行訪問。我們用文本編輯器作出的變更,也可以完全使用命令行來完成,例如 kubectl patch 命令:kubectl patch service web -p '{'spec': {'selector': {'app': 'green'}}}'
藍(lán)綠部署的好處是,流量切換幾乎是立刻完成的,推出和回滾都可以很方便的通過更新 Serevice 定義來完成。用 Kubernetes 完成金絲雀部署有時(shí)我們不想讓測(cè)試版本影響所有用戶,即使是短時(shí)間也不行。所以我們可以部分推出新版本。例如我們部署新舊兩組實(shí)例,1% 的流量發(fā)送給新版本。接下來我們?cè)谛屡f版本的監(jiān)控?cái)?shù)據(jù)中進(jìn)行觀察。如果情況允許,就可以向前推進(jìn);如果延遲、錯(cuò)誤率或者其它什么東西看起來有問題,就回滾到舊版本。由于 Kubernetes 的標(biāo)簽和選擇器的機(jī)制,可以很簡單的實(shí)現(xiàn)這種策略。前面的例子中,我們修改了服務(wù)的選擇器,接下來我們修改一下 Pod 標(biāo)簽。例如設(shè)置服務(wù)的選擇器,讓它選擇帶有 status=enabled 的 Pod,然后給特定的 Pod 打上標(biāo)簽:kubectl label pod fronted-aabbccdd-xyz status=enabled
kubectl label pods -l app=blue,version=v1.5 status=enabled
kubectl label pods -l app=blue,version=v1.4 status-
我們看到了一些用于安全部署的技術(shù),其中的一些能夠很方便的降低因部署造成的停機(jī)時(shí)間,這讓我們可以在不擔(dān)心影響用戶的情況下提高部署頻度。有些技術(shù)給我們系上安全帶,阻止問題版本影響服務(wù)。還有些別的服務(wù)讓我們感覺安心。有點(diǎn)像主機(jī)游戲中的保存按鈕——在嘗試?yán)щy操作之前,我們知道如果出了問題,我們還可以回到從前。Kubernetes 讓開發(fā)和運(yùn)維團(tuán)隊(duì)能夠使用這些技術(shù)來提高部署的安全性。如果部署的危險(xiǎn)系數(shù)降低,那么就可以更頻繁地、漸進(jìn)地進(jìn)行部署,并可以更方便的觀察變更的后果。這一切都會(huì)讓我們的新特性和修復(fù)特性能夠更快面世,讓我們的應(yīng)用有更好的可用性。這也是實(shí)現(xiàn)容器化和持續(xù)交付的重要基礎(chǔ)。
|