K8S 容器钩子函数和优雅停机 —— 筑梦之路

背景说明

在 Kubernetes 中,每次微服务的代码发布都意味着创建新版本的 pod 并删除旧 pod,如果部署不够优雅的话,可能出现如下两个问题:

  1. 正在处理请求的pod被删除,在请求没有做幂等处理的情况下,就会出现数据重复、数据错误,亦或导致分布式系统数据不一致;

  2. Kubernetes 将流量路由到已被删除的 pod,导致处理请求失败造成用户体验不佳。

所以,为了让代码发布的部署过程不影响业务的正常运行和用户无感知,我们需要实现容器的优雅停机。

容器的生命周期

Kubernetes的容器有两种生命周期钩子(Lifecycle Hooks):

PostStart

这个钩子会在容器被创建后立即执行,但无法保证会在容器的起始点 ENTRYPOINT之前执行,如果执行时间太长,将会阻止Pod状态进入running,可用于数据初始化、容器启动回调等场景。如果需要保证在应用程序启动前就要执行完的任务,可以考虑放在初始化容器( Init Containers)中去实现。

PreStop

这个钩子会在容器被结束前执行,执行期间Pod状态为 Terminating,运行时间受终止宽限期( terminationGracePeriodSeconds)约束,超出宽限期Pod将被强制杀死,可用于容器回收前的数据清理、优雅停机等场景。
(PostStart 和 PreStop)都有四种类型,分别为:exec、httpGet、tcpSocket 和 sleep。

bash 复制代码
exec(执行shell指令,可以是指令或shell脚本, 退出状态码为 0则为成功)

# shell指令模式
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "echo 'Container is stopping'"]

# shell脚本模式
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "/data/scripts/preStop.sh"]

------------------------------------------------------------

httpGet(执行http get请求,响应状态码在[200,400)区间则为成功)

lifecycle:
  preStop:
    httpGet:
      path: /shutdown  # 请求的uri
      port: 8080       # 端口
      host: api.yilingyi.com  # 主机域名,不加该字段将请求Pod本身
      scheme: HTTP     # http协议,默认值HTTP,支持HTTP、HTTPS

------------------------------------------------------------

tcpSocket(执行tcp socket请求, TCP连接成功建立则为成功)

lifecycle:
  preStop:
    tcpSocket:
      port: 8080

------------------------------------------------------------

sleep(将容器暂停5秒,Kubernetes 1.30的新特性 PodLifecycleSleepAction,待验证)

lifecycle:
  preStop:
    sleep:
      seconds: 5

注意:如果 PostStart 或 PreStop 回调失败,容器将被杀死,所以回调处理的程序应尽量轻量级及把控好执行的时间

微服务优雅停机实现

以k8s + SpringBoot + Nacos作为案例

首先,先看看pod的默认删除过程:

  1. Kube-apiserver接收到pod的删除请求,在Etcd上更新pod的状态为Terminating;

  2. Kubelet 清理节点上容器相关的资源,如存储、网络;

  3. Kubelet向容器发送SIGTERM,如果容器内进程没有任何配置,则容器立即退出。

  4. 如果容器在默认的 30 秒内没有退出,Kubelet 将发送 SIGKILL 并强制其退出

可以看出,在没有配置优雅停机之前,pod的删除相当暴力,所以为了更加优雅,我们加入了preStop hook,和将终止宽限期延长,具体实现如下:

  1. preStop hook做了两件事情:

1)nacos反注册(也称 实例注销),确保在实例关闭期间不会再有新的请求被路由到该实例。

  1. sleep 35s,nacos客户端的实例缓存为30s,30s后会重新拉取实例信息,超时为10s,一般不用10s这么长,所以我们设置为35s。
  1. springboot开启优雅停机后,最大等待时间为30s。

  2. terminationGracePeriodSeconds默认为30s,远小于preStop和springboot的时间之和,所以我们需要将其调大,我这里设置的是60s。

  3. 其实在terminationGracePeriodSeconds耗尽后,k8s还给了一个2s的额外宽限期,最后才执行SIGKILL。

方案实现

在SpringBoot > 2.3.0的版本后支持应用程序优雅停机,需要在java微服务的配置中设置如下两个属性,这一步很重要!!!

bash 复制代码
server:
  # 默认值immediate:即立即关闭,graceful:即优雅停机
  shutdown: graceful
spring:
  lifecycle:
    # 优雅停机最大等待时间,默认30s
    timeout-per-shutdown-phase: 30s

接着是在微服务的yaml文件加上优雅停机的配置:

通过env定义POD_IP获取当前Pod的ip,传递给preStop进行nacos反注册

bash 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: sre-yilingyi
spec:
  containers:
  - name: sre-yilingyi
    image: 'sre/yilingyi:1.0.0'
    env:
      - name: POD_IP
        valueFrom:
          fieldRef:
            apiVersion: v1
            fieldPath: status.podIP
    lifecycle:
      preStop:
        exec:
          command:
            - /bin/sh
            - '-c'
            - >
              curl -s --connect-timeout 10 -m 20 -X POST "http://nacos.yilingyi.com:8848/nacos/v1/ns/instance?port=8080&healthy=true&ip=${POD_IP}&weight=1&enabled=false&serviceName=sre-yilingyi&encoding=GBK&namespaceId=production" && sleep 35
  terminationGracePeriodSeconds: 60

至此实现。

相关推荐
蒋星熠34 分钟前
全栈开发:从LAMP到云原生的技术革命
微服务·云原生·职场和发展·架构·系统架构·web·devops
Aspartame~35 分钟前
K8s的相关知识总结
java·容器·kubernetes
plusplus1683 小时前
Kubernetes“城市规划”指南:告别资源拥堵与预算超支,打造高效云原生都市
云原生·容器·kubernetes
qq_312920115 小时前
K8s存储类(StorageClass)设计与Ceph集成实战
ceph·容器·kubernetes
Nazi65 小时前
kubeadm部署k8s集群环境搭建
云原生·容器·kubernetes
Brilliantee4045 小时前
藏在 K8s 幕后的记忆中枢(etcd)
容器·kubernetes·etcd
bing.shao5 小时前
gRPC 选型 etcd 的核心优势分析
数据库·微服务·云原生·golang·etcd
焯集新人7 小时前
K8S高可用集群
云原生·容器·kubernetes
楚禾Noah7 小时前
【通用常识】YAML 中的高阶语法
运维·docker·容器
小白不想白a8 小时前
【Ansible】变量、机密、事实
运维·云原生·ansible