天天看點

Kubernetes Pod生命周期 鈎子 pod hook

Kubernetes Pod生命周期 鈎子 pod hook

Pod Hook

Kubernetes 為我們的容器提供了生命周期鈎子,就是我們說的​

​Pod Hook​

​,Pod Hook 是由 kubelet 發起的,當容器中的程序啟動前或者容器中的程序終止之前運作,這是包含在容器的生命周期之中。我們可以同時為 Pod 中的所有容器都配置 hook。

Kubernetes 為我們提供了兩種鈎子函數:

  • PostStart:這個鈎子在容器建立後立即執行。但是,并不能保證鈎子将在容器​

    ​ENTRYPOINT​

    ​​之前運作,因為沒有參數傳遞給處理程式。主要用于資源部署、環境準備等。不過需要注意的是如果鈎子花費太長時間以至于不能運作或者挂起, 容器将不能達到​

    ​running​

    ​狀态。(PostStart 可以在容器啟動之後就執行。但需要注意的是,此 hook 和容器裡的 ENTRYPOINT 指令的執行順序是不确定的。)
  • PreStop:這個鈎子在容器終止之前立即被調用。它是阻塞的,意味着它是同步的, 是以它必須在删除容器的調用發出之前完成。主要用于優雅關閉應用程式、通知其他系統等。如果鈎子在執行期間挂起, Pod階段将停留在​

    ​running​

    ​​狀态并且永不會達到​

    ​failed​

    ​狀态。(PreStop 則在容器被終止之前被執行,是一種阻塞式的方式。執行完成後,Kubelet 才真正開始銷毀容器。)

如果​

​PostStart​

​​或者​

​PreStop​

​鈎子失敗, 它會殺死容器。是以我們應該讓鈎子函數盡可能的輕量。當然有些情況下,長時間運作指令是合理的, 比如在停止容器之前預先儲存狀态。

prestop

當使用者請求删除含有 pod 的資源對象時,K8S 為了讓應用程式優雅關閉(即讓應用程式完成正在處理的請求後,再關閉軟體),K8S提供兩種資訊通知:

  • 預設:K8S 通知 node 執行​

    ​docker stop​

    ​​指令,docker 會先向容器中​

    ​PID​

    ​​為1的程序發送系統信号​

    ​SIGTERM​

    ​​,然後等待容器中的應用程式終止執行,如果等待時間達到設定的逾時時間,或者預設逾時時間(30s),會繼續發送​

    ​SIGKILL​

    ​的系統信号強行 kill 掉程序。
  • 使用 pod 生命周期(利用​

    ​PreStop​

    ​回調函數),它執行在發送終止信号之前。

預設所有的優雅退出時間都在30秒内。kubectl delete 指令支援 ​

​--grace-period=<seconds>​

​​選項,這個選項允許使用者用他們自己指定的值覆寫預設值。值’0’代表 強制删除 pod. 在 kubectl 1.5 及以上的版本裡,執行強制删除時必須同時指定 ​

​--force --grace-period=0​

​。

強制删除一個 pod 是從叢集狀态還有 etcd 裡立刻删除這個 pod。 當 Pod 被強制删除時, api 伺服器不會等待來自 Pod 所在節點上的 kubelet 的确認資訊:pod 已經被終止。在 API 裡 pod 會被立刻删除,在節點上, pods 被設定成立刻終止後,在強行殺掉前還會有一個很小的寬限期。

同 readinessProbe一樣,hook 也有類似的 Handler:

  • Exec 用來執行 Shell 指令;
  • HTTPGet 可以執行 HTTP 請求。

我們來看個例子:

[root@k8s-master ~]# mkdir -p /data/nginx/html

[root@k8s-master ~]# cat hook.yml 
apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
  namespace: default
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx:1.19
    ports:
    - containerPort: 80
    volumeMounts:
    - name: message
      mountPath: /usr/share/nginx/html
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/nginx/html/index.html"]
      preStop:
        exec:
          command: ["/bin/sh","-c","echo Hello from the preStop handler > /usr/share/nginx/html/index.html"]
  volumes:
  - name: message
    hostPath:
      path: /data/nginx/html

[root@k8s-master ~]# kubectl get pod -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
lifecycle-demo           1/1     Running   0          13s   10.244.0.18   k8s-master   <none>           <none>
[root@k8s-master ~]# curl 10.244.0.18:80
Hello from the postStart handler

[root@k8s-master html]# cat index.html 
Hello from the postStart handler      

如果将容器銷毀 ,PreStop這個鈎子在容器終止之前立即被調用,可以看到結果如下

[root@k8s-master ~]# kubectl delete -f hook.yml 
pod "lifecycle-demo" deleted

[root@k8s-master html]# cat index.html 
Hello from the preStop handler      

我們可以借助​

​preStop​

​以優雅的方式停掉 Nginx 服務,進而避免強制停止容器,造成正在處理的請求無法響應。

preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]      

總結

建立容器後,Kubernetes立即發送postStart事件。但是,不能保證在調用Container的入口點之前先調用postStart處理程式。postStart處理程式相對于Container的代碼異步運作,但是Kubernetes對容器的管理會阻塞,直到postStart處理程式完成。在postStart處理程式完成之前,容器的狀态不會設定為RUNNING。

Kubernetes會在容器終止之前立即發送preStop事件。除非Pod的寬限期到期,否則Kubernetes對Container的管理将一直保持到preStop處理程式完成為止。

注意: Kubernetes僅在Pod終止時才發送preStop事件。這意味着在Pod完成時不會調用preStop挂鈎。

Ingress Controller 的yml檔案裡面定義(使用鈎子在程式關閉之前做了一定的處理)

lifecycle:
            preStop:
              exec:
                command:
                  - /wait-shutdown      
[root@master ~]# kubectl exec -it nginx-ingress-controller-pmmzd -n ingress-nginx bash
bash-5.0$ cd /
bash-5.0$ pwd
/
bash-5.0$ ls wait-shutdown 
wait-shutdown      

Q:滾動更新期間造成流量丢失

A:滾動更新觸發,Pod在删除過程中,有些節點kube-proxy還沒來得及同步iptables規則,進而部分流量請求到Terminating的Pod上,導緻請求出錯。

解決辦法:配置preStop回調,在容器終止前優雅暫停5秒,給kube-proxy多預留一點時間。

當我們更新deployment的鏡像的時候,會觸發滾動更新,這是預設的更新政策,關掉一個處于term狀态,也就是不能處理請求,馬上關閉了

Kube-proxy是負責更新規則的,是有周期性的更新規則,如果還沒有來得及更新規則還是用的之前的IP,那麼後面來的流量再配置設定過來就會失效導緻請求的出錯

這個問題的産生是下線一個pod與kube-proxy重新整理轉發的規則這個時間差并不是保持同步的,也就是這個pod被下線掉了,馬上就如kube-proxy去更新轉發的規則。這不是同步的,因為kube-proxy是周期性的去同步。

這種情況主要适用于走service的服務,微服務沒有走service是沒有的,部署單體應用是很容易碰見這種情況

Kubernetes Pod生命周期 鈎子 pod hook

優雅停止(Gracful Shutdown)與 502/504 報錯

如果 Pod 正在處理大量請求(比如 1000 QPS+)時,因為節點故障或「競價節點」被回收等原因被重新排程, 你可能會觀察到在容器被 terminate 的一段時間内出現少量 502/504。

為了搞清楚這個問題,需要先了解清楚 terminate 一個 Pod 的流程:

  1. Pod 的狀态被設為​

    ​Terminating​

    ​,(幾乎)同時該 Pod 被從所有關聯的 Service Endpoints 中移除
  2. ​preStop​

    ​ 鈎子被執行
  1. 它的執行階段很好了解:在容器被 stop 之前執行
  2. 它可以是一個指令,或者一個對 Pod 中容器的 http 調用
  3. 如果在收到 SIGTERM 信号時,無法優雅退出,要支援優雅退出比較麻煩的話,用​

    ​preStop​

    ​ 實作優雅退出是一個非常好的方式
  4. preStop 的定義位置:https://github.com/kubernetes/api/blob/master/core/v1/types.go#L2515
  1. ​preStop​

    ​ 執行完畢後,SIGTERM 信号被發送給 Pod 中的所有容器
  2. 繼續等待,直到容器停止,或者逾時​

    ​spec.terminationGracePeriodSeconds​

    ​,這個值預設為 30s
  1. 需要注意的是,這個優雅退出的等待計時是與​

    ​preStop​

    ​ 同步開始的!而且它也不會等待 ​

    ​preStop​

    ​ 結束!
  1. 如果超過了​

    ​spec.terminationGracePeriodSeconds​

    ​ 容器仍然沒有停止,k8s 将會發送 SIGKILL 信号給容器
  2. 程序全部終止後,整個 Pod 完全被清理掉

注意:1 跟 2 兩個工作是異步發生的,是以在未設定 ​

​preStop​

​​ 時,可能會出現「Pod 還在 Service Endpoints 中,但是 ​

​SIGTERM​

​ 已經被發送給 Pod 導緻容器都挂掉」的情況,我們需要考慮到這種狀況的發生。

了解了上面的流程後,我們就能分析出兩種錯誤碼出現的原因:

  • 502:應用程式在收到 SIGTERM 信号後直接終止了運作,導緻部分還沒有被處理完的請求直接中斷,代理層傳回 502 表示這種情況
  • 504:Service Endpoints 移除不夠及時,在 Pod 已經被終止後,仍然有個别請求被路由到了該 Pod,得不到響應導緻 504

通常的解決方案是,在 Pod 的 ​

​preStop​

​​ 步驟加一個 15s 的等待時間。其原理是:在 Pod 處理 terminating 狀态的時候,就會被從 Service Endpoints 中移除,也就不會再有新的請求過來了。在 ​

​preStop​

​ 等待 15s,基本就能保證所有的請求都在容器死掉之前被處理完成(一般來說,絕大部分請求的處理時間都在 300ms 以内吧)。

一個簡單的示例如下,它使 Pod 被 Terminate 時,總是在 stop 前先等待 15s,再發送 SIGTERM 信号給容器:

containers:
    - name: my-app
      # 添加下面這部分
      lifecycle:
        preStop:
          exec:
            command:
            - /bin/sleep
            - "15"      

更好的解決辦法,是直接等待所有 tcp 連接配接都關閉(需要鏡像中有 netstat):

containers:
    - name: my-app
      # 添加下面這部分
      lifecycle:
      preStop:
          exec:
            command:
            - /bin/sh
            - -c
            - "while [ $(netstat -plunt | grep tcp | wc -l | xargs) -ne 0 ]; do sleep 1; done"      

如果我的服務還使用了 Sidecar 代理網絡請求,該怎麼處理?

以服務網格 Istio 為例,在 Envoy 代理了 Pod 流量的情況下,502/504 的問題會變得更複雜一點——還需要考慮 Sidecar 與主容器的關閉順序:

  • 如果在 Envoy 已關閉後,有新的請求再進來,将會導緻 504(沒人響應這個請求了)
  • 是以 Envoy 最好在 Terminating 至少 3s 後才能關,確定 Istio 網格配置已完全更新
  • 如果在 Envoy 還沒停止時,主容器先關閉,然後又有新的請求再進來,Envoy 将因為無法連接配接到 upstream 導緻 503
  • 是以主容器也最好在 Terminating 至少 3s 後,才能關閉。
  • 如果主容器處理還未處理完遺留請求時,Envoy 或者主容器的其中一個停止了,會因為 tcp 連接配接直接斷開連接配接導緻 502
  • 是以 Envoy 必須在主容器處理完遺留請求後(即沒有 tcp 連接配接時),才能關閉
containers:
    - name: istio-proxy
      # 添加下面這部分
      lifecycle:
      preStop:
          exec:
            command:
            - /bin/sh
            - -c
            - "while [ $(netstat -plunt | grep tcp | grep -v envoy | wc -l | xargs) -ne 0 ]; do sleep 1; done"      

參考

  • Kubernetes best practices: terminating with grace
  • Graceful shutdown in Kubernetes is not always trivial

繼續閱讀