一、灰度釋出
灰度釋出是一種釋出方式,也叫金絲雀釋出,起源是礦工在下井之前會先放一隻金絲雀到井裡,如果金絲雀不叫了,就代表瓦斯濃度高。原因是金絲雀對瓦斯氣體很敏感。灰度釋出的做法是:會在現存舊應用的基礎上,啟動一個新版應用,但是新版應用並不會直接讓使用者訪問。而是先讓測試同學去進行測試。如果沒有問題,則可以將真正的使用者流量慢慢匯入到新版,在這中間,持續對新版本執行狀態做觀察,直到慢慢切換過去,這就是所謂的A/B測試。當然,你也可以招募一些灰度使用者,給他們設定獨有的灰度標示(Cookie,Header),來讓他們可以訪問到新版應用,當然,如果中間切換出現問題,也應該將流量迅速地切換到老應用上。
1)準備新版本的service
拷貝一份deployment檔案:
cp deployment-user-v1.yaml deployment-user-v2.yaml
修改之前寫過的內容:
apiVersion: apps/v1 #API 配置版本 kind: Deployment #資源型別 metadata: + name: user-v2 #資源名稱 spec: selector: matchLabels: + app: user-v2 #告訴deployment根據規則匹配相應的Pod進行控制和管理,matchLabels欄位匹配Pod的label值 replicas: 3 #宣告一個 Pod,副本的數量 template: metadata: labels: + app: user-v2 #Pod的名稱 spec: #組內建立的 Pod 資訊 containers: - name: nginx #容器的名稱 + image: registry.cn-beijing.aliyuncs.com/zhangrenyang/nginx:user-v2 ports: - containerPort: 80 #容器內對映的埠
然後service的檔案內容是這樣的:
apiVersion: v1 kind: Service metadata: + name: service-user-v2 spec: selector: + app: user-v2 ports: - protocol: TCP port: 80 targetPort: 80 type: NodePort
啟動:
kubectl apply -f deployment-user-v2.yaml service-user-v2.yaml
2)根據cookie切分流量
基於 Cookie 切分流量。這種實現原理主要根據使用者請求中的 Cookie 是否存在灰度標示 Cookie去判斷是否為灰度使用者,再決定是否返回灰度版本服務
nginx.ingress.kubernetes.io/canary
:可選值為 true / false 。代表是否開啟灰度功能nginx.ingress.kubernetes.io/canary-by-cookie
:灰度釋出 cookie 的 key。當 key 值等於 always 時,灰度觸發生效。等於其他值時,則不會走灰度環境 ingress-gray.yaml
我們建立一個ingress-gray.yaml檔案:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: user-canary annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-by-cookie: "vip_user" spec: rules: - http: paths: - backend: serviceName: service-user-v2 servicePort: 80 backend: serviceName: service-user-v2 servicePort: 80
使檔案生效:
kubectl apply -f ./ingress-gray.yaml
獲取外部介面:
kubectl -n ingress-nginx get svc
測試:
curl http://172.31.178.169:31234/user curl http://118.190.156.138:31234/user curl --cookie "vip_user=always" http://172.31.178.169:31234/user
3)基於header切分流量
基於 Header 切分流量,這種實現原理主要根據使用者請求中的 header 是否存在灰度標示 header去判斷是否為灰度使用者,再決定是否返回灰度版本服務。
修改下上面的ingress-gray.yml檔案即可:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: user-canary annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/canary: "true" + nginx.ingress.kubernetes.io/canary-by-header: "name" + nginx.ingress.kubernetes.io/canary-by-header-value: "vip" spec: rules: - http: paths: - backend: serviceName: service-user-v2 servicePort: 80 backend: serviceName: service-user-v2 servicePort: 80
同樣的:
kubectl apply -f ingress-gray.yaml curl --header "name:vip" http://172.31.178.169:31234/user
4)基於權重切分流量
這種實現原理主要是根據使用者請求,通過根據灰度百分比決定是否轉發到灰度服務環境中
nginx.ingress.kubernetes.io/canary-weight
:值是字串,為 0-100 的數字,代表灰度環境命中概率。如果值為 0,則表示不會走灰度。值越大命中概率越大。當值 = 100 時,代表全走灰度。
一樣一樣的,修改下配置引數罷了:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: user-canary annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/canary: "true" + nginx.ingress.kubernetes.io/canary-weight: "50" spec: rules: - http: paths: - backend: serviceName: service-user-v2 servicePort: 80 backend: serviceName: service-user-v2 servicePort: 80
測試下:
kubectl apply -f ingress-gray.yaml for ((i=1; i<=10; i++)); do curl http://172.31.178.169:31234/user; done
k8s 會優先去匹配 header ,如果未匹配則去匹配 cookie ,最後是 weight。
二、滾動釋出
滾動釋出,則是我們一般所說的無當機發布。其釋出方式如同名稱一樣,一次取出一臺/多臺伺服器(看策略配置)進行新版本更新。當取出的伺服器新版確保無問題後,接著採用同等方式更新後面的伺服器。k8s建立副本應用程式的最佳方法就是部署(Deployment),部署自動建立副本集(ReplicaSet),副本集可以精確地控制每次替換的Pod數量,從而可以很好的實現滾動更新。k8s每次使用一個新的副本控制器(replication controller)來替換已存在的副本控制器,從而始終使用一個新的Pod模板來替換舊的pod模板
- 建立一個新的replication controller
- 增加或減少pod副本數量,直到滿足當前批次期望的數量
- 刪除舊的replication controller
滾動釋出的優缺點如下:
- 優點
- 不需要停機更新,無感知平滑更新。
- 版本更新成本小,不需要新舊版本共存
- 缺點
- 更新時間長:每次只更新一個/多個映象,需要頻繁連續等待服務啟動緩衝
- 舊版本環境無法得到備份:始終只有一個環境存在
- 回滾版本異常痛苦:如果滾動釋出到一半出了問題,回滾時需要使用同樣的滾動策略回滾舊版本
我們下面來嘗試下,先擴容為10個副本:
kubectl get deploy kubectl scale deployment user-v1 --replicas=10
修改deployment-user-v1.yaml檔案:
apiVersion: apps/v1 #API 配置版本 kind: Deployment #資源型別 metadata: name: user-v1 #資源名稱 spec: minReadySeconds: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: user-v1 #告訴deployment根據規則匹配相應的Pod進行控制和管理,matchLabels欄位匹配Pod的label值 replicas: 10 #宣告一個 Pod,副本的數量 template: metadata: labels: app: user-v1 #Pod的名稱 spec: #組內建立的 Pod 資訊 containers: - name: nginx #容器的名稱 + image: registry.cn-beijing.aliyuncs.com/zhangrenyang/nginx:user-v3 #使用哪個映象 ports: - containerPort: 80 #容器內對映的埠
引數 | 含義 |
---|---|
minReadySeconds | 容器接受流量延緩時間:單位為秒,預設為0。如果沒有設定的話,k8s會認為容器啟動成功後就可以用了。設定該值可以延緩容器流量切分 |
strategy.type = RollingUpdate | ReplicaSet 釋出型別,宣告為滾動釋出,預設也為滾動釋出 |
strategy.rollingUpdate.maxSurge | 最多Pod數量:為數字型別/百分比。如果 maxSurge 設定為1,replicas 設定為10,則在釋出過程中pod數量最多為10 + 1個(多出來的為舊版本pod,平滑期不可用狀態)。maxUnavailable 為 0 時,該值也不能設定為0 |
strategy.rollingUpdate.maxUnavailable | 升級中最多不可用pod的數量:為數字型別/百分比。當 maxSurge 為 0 時,該值也不能設定為0 |
啟動:
kubectl apply -f ./deployment-user-v1.yaml
deployment.apps/user-v1 configured
然後檢視狀態:
kubectl rollout status deployment/user-v1
三、服務可用性探針
當 Pod 的狀態為 Running 時,該 Pod 就可以被分配流量(可以訪問到)了。一個後端容器啟動成功,不一定不代表服務啟動成功。
3.2.1 存活探針 LivenessProbe
第一種是存活探針。存活探針是對執行中的容器檢測的。如果想檢測你的服務在執行中有沒有發生崩潰,服務有沒有中途退出或無響應,可以使用這個探針。如果探針探測到錯誤, Kubernetes 就會殺掉這個 Pod;否則就不會進行處理。如果預設沒有配置這個探針, Pod 不會被殺死。
3.2.2 可用探針 ReadinessProbe
第二種是可用探針。作用是用來檢測 Pod 是否允許被訪問到(是否準備好接受流量)。如果你的服務載入很多資料,或者有其他需求要求在特定情況下不被分配到流量,那麼可以用這個探針。如果探針檢測失敗,流量就不會分配給該 Pod。在沒有配置該探針的情況下,會一直將流量分配給 Pod。當然,探針檢測失敗,Pod 不會被殺死。
3.2.3 啟動探針 StartupProbe
第三種是啟動探針。作用是用來檢測 Pod 是否已經啟動成功。如果你的服務啟動需要一些載入時長(例如初始化日誌,等待其他呼叫的服務啟動成功)才代表服務啟動成功,則可以用這個探針。如果探針檢測失敗,該 Pod 就會被殺死重啟。在沒有配置該探針的情況下,預設不會殺死 Pod 。在啟動探針執行時,其他所有的探針檢測都會失效。
探針名稱 | 在哪個環節觸發 | 作用 | 檢測失敗對Pod的反應 |
---|---|---|---|
啟動探針 | Pod 執行時 | 檢測服務是否啟動成功 | 殺死 Pod 並重啟 |
存活探針 | Pod 執行時 | 檢測服務是否崩潰,是否需要重啟服務 | 殺死 Pod 並重啟 |
可用探針 | Pod 執行時 | 檢測服務是不是允許被訪問到 | 停止Pod的訪問排程,不會被殺死重啟 |
檢測方式
1、ExecAction
通過在 Pod 的容器內執行預定的 Shell 指令碼命令。如果執行的命令沒有報錯退出(返回值為0),代表容器狀態健康。否則就是有問題的
我們來新建一個檔案,vi shell-probe.yaml,內容如下:
apiVersion: v1 kind: Pod metadata: labels: test: shell-probe name: shell-probe spec: containers: - name: shell-probe image: registry.aliyuncs.com/google_containers/busybox args: - /bin/sh - -c - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600 livenessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5
然後執行下面的命令,檢視情況:
kubectl apply -f liveness.yaml kubectl get pods | grep liveness-exec kubectl describe pods liveness-exec
2、TCPSocketAction
這種方式是使用 TCP 套接字檢測。 Kubernetes 會嘗試在 Pod 內與指定的埠進行連線。如果能建立連線(Pod的埠開啟了),這個容器就代表是健康的,如果不能,則代表這個 Pod 就是有問題的。
建立檔案如下,tcp-probe.yaml:
apiVersion: v1 kind: Pod metadata: name: tcp-probe labels: app: tcp-probe spec: containers: - name: tcp-probe image: nginx ports: - containerPort: 80 readinessProbe: tcpSocket: port: 80 initialDelaySeconds: 5 periodSeconds: 10
類似的:
kubectl apply -f tcp-probe.yaml kubectl get pods | grep tcp-probe kubectl describe pods tcp-probe
進入到容器內部:
kubectl exec -it tcp-probe -- /bin/sh
更新apt-get並安裝vim:
apt-get update apt-get install vim -y vi /etc/nginx/conf.d/default.conf
修改nginx檔案配置,把80埠修改為8080,然後過載一下nginx:
nginx -s reload
看一下狀態:
kubectl describe pod tcp-probe
3、HTTPGetAction
這種方式是使用 HTTP GET 請求。Kubernetes 會嘗試訪問 Pod 內指定的API路徑。如果返回200,代表容器就是健康的。如果不能,代表這個 Pod 是有問題的。
新增http-probe.yaml檔案:
apiVersion: v1 kind: Pod metadata: labels: test: http-probe name: http-probe spec: containers: - name: http-probe image: registry.cn-beijing.aliyuncs.com/zhangrenyang/http-probe:1.0.0 livenessProbe: httpGet: path: /liveness port: 80 httpHeaders: - name: source value: probe initialDelaySeconds: 3 periodSeconds: 3
然後,執行並檢視狀態:
kubectl apply -f ./http-probe.yaml kubectl describe pods http-probe kubectl replace --force -f http-probe.yaml
Dockerfile內容如下:
FROM node COPY ./app /app WORKDIR /app EXPOSE 3000 CMD node index.js
node服務檔案如下:
let http = require('http'); let start = Date.now(); http.createServer(function(req,res){ if(req.url === '/liveness'){ let value = req.headers['source']; if(value === 'probe'){ let duration = Date.now()-start; if(duration>10*1000){ res.statusCode=500; res.end('error'); }else{ res.statusCode=200; res.end('success'); } }else{ res.statusCode=200; res.end('liveness'); } }else{ res.statusCode=200; res.end('liveness'); } }).listen(3000,function(){console.log("http server started on 3000")});
好了今天的內容就到這裡了。