天天看点

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

继续阅读