kubernetes 在 pod 的生命週期中引入探針機制來判斷服務的健康狀態。
Liveness 探針顧名思義是用來探測服務的生存狀態,若是 Liveness 探針連續失敗次數超過設定的閾值,則 kubelet 會 kill 掉該 pod。 Readiness 探針用來判斷服務是否準備好接收流量和負載,按照官方文檔說明,Readiness 探針連續失敗後,將從 service 中摘除該 endpoint,再也不承載流量。但實際上它還有另一個做用,就在更新 deployment 或 replica set 時,Liveness 探針決定了 controller kill 掉舊 pod 的時機,這一點在後面的例子中咱們再細講。java
在寫這篇博客以前,官方文檔看了一遍又一遍,也翻了好多第三方的博客,但仍是沒用深刻的理解這兩個探針的區別。在生產環境中使用的時候,咱們到底該不應使用這兩個探針呢,又該怎麼搭配使用這個探針呢?golang
本文基於 kubernetes 1.14 編寫,對於 kubernetes 1.16 引入的 startup 探針的功能請查看官方文檔。web
少囉嗦,直接上例子。docker
首先咱們構建一個 golang webserver,而後咱們對此服務驗證 Liveness Probe 和 Readiness Probe 對此服務的影響。
Dockerfile 以下:api
FROM golang:1.10-alpine3.8 AS build-env WORKDIR /go/src/app COPY . . RUN go build -o /app . FROM alpine:3.8 COPY --from=build-env /app /app COPY --from=build-env /go/src/app/docker/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["/app"]
在 entrypoint 中咱們加入幾秒鐘的 sleep 來模擬一個啓動較慢的應用(或者你能夠直接起一個 java 服務),來方便咱們觀察 Liveness Probe 和 Readiness Probe 的做用。
entrypoint 以下:bash
#!/bin/bash echo "in entrypoint.sh" echo "sleep start" sleep 10 echo "sleep over" exec "$@"
依據此 Dockerfile 構建鏡像 example.com/server:latest, 而後建立一個最基本的 deploy網絡
和 service ,咱們的試驗就開始啦。併發
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: server labels: app: server spec: replicas: 1 selector: matchLabels: app: server template: metadata: labels: app: server spec: containers: - name: app image: example.com/server:latest ports: - name: app-tcp containerPort: 8000 protocol: TCP --- apiVersion: v1 kind: Service metadata: name: server labels: app: server spec: selector: app: server ports: - port: 80 protocol: TCP targetPort: 8000
至此,咱們就在集羣中運行起來一個服務,它具備以下特徵:app
說幹就幹。
接下來分別配置幾種不一樣的探針組合方式來觀察其對平常發版(更新 deployment/rs)以及擴容,縮容操做:tcp
而後在發版的同時,用壓測工具模擬必定併發請求去訪問服務,而後統計不一樣場景下的請求成功率。
壓測工具選用 hey
,須要注意的是,這裏使用壓測工具並非爲了測試系統的負載能力,而單純是爲了模擬
必定併發量,觀察系統行爲。
# qps 10, 併發 10 ,持續 20s hey -disable-keepalive -t 1 -z 20s -q 10 -c 10 http://server/ping
不配置探針時,kubelet 和 endpoint controller 都默認認爲 pod 只要主進程存在,容器即存活,
不會嘗試重啓 pod, 也不會主動將 pod 的 ip 從 endpoint controller 摘除。
所以,此種場景下 pod 可以正常啓動,但啓動後流量即打入到新啓動的容器。在咱們的場景下,主進程啓動 10 秒後服務才能夠處理網絡請求,所以每次發版會有 10 秒左右的服務不可用時間。
livenessProbe: httpGet: path: /ping port: 8000 scheme: HTTP initialDelaySeconds: 2 timeoutSeconds: 3 periodSeconds: 5 successThreshold: 1 failureThreshold: 3
咱們配置了初始延遲爲 2 秒,超時時間爲 3 秒,請求間隔爲 5 秒,失敗閾值爲 3 的 liveness 探針,這意味着什麼呢?
從 pod 啓動2秒鐘開始進行探測,每5秒進行一次,連續失敗三次後即斷定容器不存活,則會殺掉該容器。
2 + 5*3=17, 17 後若是容器還沒啓動完成,則會被重啓!由於咱們的服務啓動 10 秒後才能正確的處理 ping
請求,因此咱們的容器將會不斷重啓,永遠沒法正常提供服務!
livenessProbe: httpGet: path: /ping port: 8000 scheme: HTTP initialDelaySeconds: 10 timeoutSeconds: 3 periodSeconds: 5 successThreshold: 1 failureThreshold: 3
什麼叫適當呢。其實只要你的服務正常啓動時間小於 initialDelaySeconds + failureThreshold*periodSeconds 就好。可是這樣算有點麻煩,爲了簡便起見,這裏直接讓服務啓動時間小於 initialDelaySeconds 便可。
這樣配置之後,服務是起來了,但咱們測試後,發現仍是有 10 秒左右的不可用時間!
由於 liveness 只是決定了 kubelet 重啓 Pod 的時間,可是對 endpoint controller 什麼時候將 pod 的 IP
添加與刪除並沒有直接影響,所以流量仍是能打到未啓動徹底的容器上。不過只是咱們的 Pod 能正常啓動了。
readinessProbe: httpGet: path: /ping port: 8000 scheme: HTTP initialDelaySeconds: 5 timeoutSeconds: 3 periodSeconds: 5 successThreshold: 1 failureThreshold: 3
readiness 探針會的功能包含兩個:
若是隻想實現 1 功能,僅服務啓動時檢測可用性的話, 可以使用 startupProbe (前提是你的集羣版本大於 1.16) 。
配置 readiness 探針後,手動調用時沒有發現服務不可用,而後咱們再使用 hey 進行併發測試。
測試後發現,成功率能達到 90% 以上,有了質的飛越。
readinessProbe: httpGet: path: /ping port: 8000 scheme: HTTP initialDelaySeconds: 10 timeoutSeconds: 3 periodSeconds: 5 successThreshold: 1 failureThreshold: 3
只要 Readiness 的 initialDelay 小於服務的正常啓動時間(在咱們的例子裏也就是 10 秒),該值的大小對
服務自己是沒有影響的。
可是若是 initialDelay 大於你的服務啓動時間,雖然對服務自己也沒有影響,可是將會延長你的發佈時間。由於在新啓動的 pod ready 以前,舊的 pod 是不會被 kill 掉的,流量也不會切到新起的容器。
有了 readiness 探針後,咱們發版時服務的調用成功率已經達 90% 以上了,但對於一些併發較高的服務,這剩餘的 10% 的失敗,影響也是很嚴重的。還有沒有辦法再進一步提高成功率呢?
分析一下,請求失敗,無非是兩種可能:
第一種狀況已經由 readiness 解了,只能是第二種了。來看一張 《Kubernetes in action》 一書中的圖:
如下節選自《Kubernetes in action》 :
當APIserver收到一箇中止Pod的請求時,它首先修改了etcd裏Pod的狀態,並通知關注這個刪除事件全部的watcher。這些watcher裏包括Kubelet和Endpoint Controller。這兩個序列的事件是並行發生的(標記爲A和B)在A系列事件裏,你會看到在Kubelet收到該Pod要中止的通知之後會盡快開始中止Pod的一系列操做(執行prestop鉤子,發送SIGTERM信號,等待一段時間而後若是這個容器沒有自動退出的話就強行殺掉這個容器)。若是應用響應了SIGTERM並迅速中止接收請求,那麼任未嘗試鏈接它的客戶端都會收到一個Connection Refusd的錯誤。由於APIserver是直接向Kubelet發送的請求,那麼從Pod被刪除開始計算,Pod用於執行這段邏輯的時間至關短。如今,讓咱們一塊兒看看另一系列事件都發生了什麼——移除這個Pod相關的iptables規則(圖中所示事件系列B)。當Endpoints Controller(運行在在Kubernetes控制平面裏的Controller Manager裏)收到這個Pod被刪除的通知,而後它會把這個Pod從全部關聯到這個Pod的Service裏剔除。它經過向APIserver發送一個REST請求對Endpoint對象進行修改來實現。APIserver而後通知每一個監視Endpoint對象的組件。在這些組件裏包含了每一個工做節點上運行的Kubeproxy。這些代理負責更新它所在節點上的iptables規則,這些規則能夠用來阻止外面的請求被轉發到要中止的Pod上。這裏有個很是重要的細節,移除這些iptables規則對於已有的鏈接不會有任何影響——鏈接到這個Pod的客戶端仍然能夠經過已有鏈接向它發送請求。
這些請求都是並行發生的。更確切地,關停應用所須要的時間要比iptables更新花費所需的時間稍短一些。這是由於iptables修改的事件鏈看起來稍微長一些(見圖2),由於這些事件須要先到達Endpoints Controller,而後它向APIServer發送一個新請求,接着在Proxy最終修改iptables規則以前,APIserver必須通知到每一個KubeProxy。這意味着SIGTERM可能會在全部節點iptables規則更新前發送。
簡單來講,就是 ip 回收要比 kubelet 停掉容器慢一點。解決辦法就很簡單了,咱們另容器的回收晚於 ip 回收,就不會出現上面的問題了。
lifecycle: preStop: exec: command: - sh - -c - "sleep 5"
具體延遲時間需根據集羣狀況調整。
添加了上面的延遲退出操做後,再來進行併發測試,能夠看到在咱們給定的壓力下,請求成功率打到了 100%
經過配置 readiness 探針和程序延遲退出的方式,咱們實現了必定併發下 kubernetes 無感發布,發版時服務請求成功率 100%