Skip to content

1. Pod中断原因

Kubernetes 集群中,业务通常采用 Deployment + LoadBalancer 类型 Service 的方式对外提供服务

图片

1.1 新建pod

Pod running 后被加入到 Endpoint 后端,容器服务监控到 Endpoint 变更后将 Node 加入到 SLB 后端。此时请求从 SLB 转发到 Pod 中,但是 Pod 业务代码还未初始化完毕,无法处理请求,导致服务中断

1.2 删除pod

在删除旧 pod 过程中需要对多个对象(如 Endpoint、ipvs/iptables、SLB)进行状态同步,并且这些同步操作是异步执行的

图片

Pod

  1. pod 状态变更:将 Pod 设置为 Terminating 状态,并从所有 Service 的 Endpoints 列表中删除。此时,Pod 停止获得新的流量,但在 Pod 中运行的容器不会受到影响;
  2. 执行 preStop Hook:Pod 删除时会触发 preStop Hook,preStop Hook 支持 bash 脚本、TCP 或 HTTP 请求;
  3. 发送 SIGTERM 信号:向 Pod 中的容器发送 SIGTERM 信号;
  4. 等待指定的时间:terminationGracePeriodSeconds 字段用于控制等待时间,默认值为 30 秒。该步骤与 preStop Hook 同时执行,因此 terminationGracePeriodSeconds 需要大于 preStop 的时间,否则会出现 preStop 未执行完毕,pod 就被 kill 的情况;
  5. 发送 SIGKILL 信号:等待指定时间后,向 pod 中的容器发送 SIGKILL 信号,删除 pod。

中断原因:上述 1、2、3、4 步骤同时进行,因此有可能存在 Pod 收到 SIGTERM 信号并且停止工作后,还未从 Endpoints 中移除的情况。此时,请求从 slb 转发到 pod 中,而 Pod 已经停止工作,因此会出现服务中断

1.3 iptables/ipvs

中断原因:当 pod 变为 termintaing 状态时,会从所有 service 的 endpoint 中移除该 pod。kube-proxy 会清理对应的 iptables/ipvs 条目。而容器服务 watch 到 endpoint 变化后,会调用 slb openapi 移除后端,此操作会耗费几秒。由于这两个操作是同时进行,因此有可能存在节点上的 iptables/ipvs 条目已经被清理,但是节点还未从 slb 移除的情况。此时,流量从 slb 流入,而节点上已经没有对应的 iptables/ipvs 规则导致服务中断

图片

1.4 SLB

中断原因:容器服务监控到 Endpoints 变化后,会将 Node 从 slb 后端移除。当节点从 slb 后端移除后,SLB 对于继续发往该节点的长连接会直接断开,导致服务中断。

image-20241029160617930

2. Pod配置

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: default
strategy:
  rollingUpdate:
    maxSurge: 50%
    maxUnavailable: 0% #为0,保证有新的Pod启动后才停止之前的pod
  type: RollingUpdate
spec:
  terminationGracePeriodSeconds: 60 #默认30
  containers:
  - name: nginx
    image: nginx
    # 存活检测
    livenessProbe:
      failureThreshold: 3
      initialDelaySeconds: 30
      periodSeconds: 30
      successThreshold: 1
      tcpSocket:
        port: 5084
      timeoutSeconds: 1
    # 就绪检测
    readinessProbe:
      failureThreshold: 3
      initialDelaySeconds: 30
      periodSeconds: 30
      successThreshold: 1
      tcpSocket:
        port: 5084
      timeoutSeconds: 1
    # 优雅退出,command这里根据业务进行修改
    lifecycle: 
      preStop: 
        exec: 
          command: 
          - sleep
          - 30
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: default
strategy:
  rollingUpdate:
    maxSurge: 50%
    maxUnavailable: 0% #为0,保证有新的Pod启动后才停止之前的pod
  type: RollingUpdate
spec:
  terminationGracePeriodSeconds: 60 #默认30
  containers:
  - name: nginx
    image: nginx
    # 存活检测
    livenessProbe:
      failureThreshold: 3
      initialDelaySeconds: 30
      periodSeconds: 30
      successThreshold: 1
      tcpSocket:
        port: 5084
      timeoutSeconds: 1
    # 就绪检测
    readinessProbe:
      failureThreshold: 3
      initialDelaySeconds: 30
      periodSeconds: 30
      successThreshold: 1
      tcpSocket:
        port: 5084
      timeoutSeconds: 1
    # 优雅退出,command这里根据业务进行修改
    lifecycle: 
      preStop: 
        exec: 
          command: 
          - sleep
          - 30

1.springboot案例

简单点就是:

  1. Kube-apiserver接收到pod的删除请求,在Etcd上更新pod的状态为Terminating;
  2. Kubelet 清理节点上容器相关的资源,如存储、网络;
  3. Kubelet向容器发送SIGTERM,如果容器内进程没有任何配置,则容器立即退出。
  4. 如果容器在默认的 30 秒内没有退出,Kubelet 将发送 SIGKILL 并强制其退出。

image-20241108110324209

hook,和将终止宽限期延长,具体实现如下:

  1. preStop hook做了两件事情:

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

  1. sleep 35s,nacos客户端的实例缓存为30s,30s后会重新拉取实例信息,超时为10s,一般不用10s这么长,所以我们设置为35s。
  2. springboot开启优雅停机后,最大等待时间为30s。
  3. terminationGracePeriodSeconds默认为30s,远小于preStop和springboot的时间之和,所以我们需要将其调大,我这里设置的是60s。
  4. 其实在terminationGracePeriodSeconds耗尽后,k8s还给了一个2s的额外宽限期,最后才执行SIGKILL。

image-20241108112405861

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

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

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

yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod1
spec:
  containers:
  - name: pod1
    image: image
    env:
    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP
    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sh
          - '-c'
          - >
            curl -s --connect-timeout 10 -m 20 -X POST "http://svc: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
apiVersion: v1
kind: Pod
metadata:
  name: pod1
spec:
  containers:
  - name: pod1
    image: image
    env:
    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP
    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sh
          - '-c'
          - >
            curl -s --connect-timeout 10 -m 20 -X POST "http://svc: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