侧边栏壁纸
博主头像
船长博主等级

专注于云原生运维,致敬每个爱学习的你

  • 累计撰写 35 篇文章
  • 累计创建 10 个标签
  • 累计收到 9 条评论

Kubernetes 零宕机滚动更新

船长
2022-01-29 / 0 评论 / 0 点赞 / 511 阅读 / 6,005 字
温馨提示:
本文最后更新于 2022-01-29,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

背景

从旧版本到新版本进行滚动更新,只是简单的通过输出显示来判断那些Pod是存活并准备就绪的,那么这个滚动更新的行为看上去肯定就是有效的,但是往往实际情况就是从旧版本到新版本的切换的过程并不总是十分顺畅,应用程序很有可能会丢弃某些客户端的请求,那接下来一起解决下流量丢失的问题吧?

Pod 探针

  • livenessProbe: 指示容器是否正在运行。如果存活态探测失败,则 kubelet 会杀死容器, 并且容器将根据其重启策略决定未来。如果容器不提供存活探针, 则默认状态为 Success
  • readinessProbe: 指示容器是否准备好为请求提供服务。如果就绪态探测失败, 端点控制器将从与 Pod 匹配的所有服务的端点列表中删除该 Pod 的 IP 地址。 初始延迟之前的就绪态的状态值默认为 Failure。 如果容器不提供就绪态探针,则默认状态为 Success
  • startupProbe: 指示容器中的应用是否已经启动。如果提供了启动探针,则所有其他探针都会被 禁用,直到此探针成功为止。如果启动探测失败,kubelet 将杀死容器,而容器依其 重启策略进行重启。 如果容器没有提供启动探测,则默认状态为 Success

探测方式

  • ExecAction:在容器内执行一个命令,如果返回值为0,则认为容器健康。

  • TCPSocketAction:通过TCP连接检查容器内的端口是否是通的,如果是通的就认为容器健康。

  • HTTPGetAction:通过应用程序暴露的API地址来检查程序是否是正常的,如果状态码为200~400之间,则认为容器健康。

探测结果

  • Success(成功):容器通过了诊断。
  • Failure(失败):容器未通过诊断。
  • Unknown(未知):诊断失败,因此不会采取任何行动。

探针使用场景

何时该使用存活探针?

如果容器中的进程能够在遇到问题或不健康的情况下自行奔溃,则不一定需要存活探针,kubelet 将根据 Pod 的restartPolicy 自动执行修复操作。

如果你希望容器在探测失败时被杀死并重新启动,那么请指定一个存活态探针, 并指定restartPolicy 为 "Always" 或 "OnFailure"。

何时该使用就绪探针?

如果要仅在探测成功时才开始向Pod发送流量请求,请指定就绪探针。在这种情况下,就绪探针可能与存活探针相同,但是规约中的就绪态探针的存在意味着 Pod 将在启动阶段不接收任何数据,并且只有在探针探测成功后才开始接收数据。

如果你希望容器能够自行进入维护状态,也可以指定一个就绪态探针,检查某个特定于 就绪态的因此不同于存活态探测的端点。

如果你的应用程序对后端服务有严格的依赖性,你可以同时实现存活态和就绪态探针。 当应用程序本身是健康的,存活态探针检测通过后,就绪态探针会额外检查每个所需的后端服务是否可用。 这可以帮助你避免将流量导向只能返回错误信息的 Pod。

如果你只是想在 Pod 被删除时能够排空请求,则不一定需要使用就绪态探针; 在删除 Pod 时,Pod 会自动将自身置于未就绪状态,无论就绪态探针是否存在。 等待 Pod 中的容器停止期间,Pod 会一直处于未就绪状态。

何时该使用启动探针?

对于所包含的容器需要较长时间才能启动就绪的 Pod 而言,启动探针是有用的。 你不再需要配置一个较长的存活态探测时间间隔,只需要设置另一个独立的配置选定, 对启动期间的容器执行探测,从而允许使用远远超出存活态时间间隔所允许的时长。

如果你的容器启动时间通常超出 initialDelaySeconds + failureThreshold × periodSeconds 总值,你应该设置一个启动探测,对存活态探针所使用的同一端点执行检查。 periodSeconds 的默认值是 10 秒。你应该将其 failureThreshold 设置得足够高, 以便容器有充足的时间完成启动,并且避免更改存活态探针所使用的默认值。 这一设置有助于减少死锁状况的发生。

探针参数说明

  • initialDelaySeconds: 60 # 初始化时间

  • timeoutSeconds: 5 # 超时时间

  • periodSeconds: 10 # 检测间隔

  • successThreshold: 1 # 检查成功为1次表示就绪

  • failureThreshold: 5 # 检测失败2次表示未就绪


检查时间计算:

每次检查的间隔是10秒,最长超时时间是5秒,也就是单次检查应该是10 + 5 = 15秒(periodSeconds + timeoutSeconds),并不是10 * 5

所以最长的重启时间为(10 + 5)* 5

(periodSeconds + timeoutSeconds) * failureThreshold

此时又分为了两种情况:

1. 首次启动时:最长重启时间需要加上initialDelaySeconds,因为需要等待initialDelaySeconds秒后才会执行健康检查。最长重启时间:(periodSeconds + timeoutSeconds) * failureThreshold + initialDelaySeconds

2. 程序启动完成后:此时不需要计入initialDelaySeconds,最长重启时间:(periodSeconds + timeoutSeconds) * failureThreshold

滚动更新

apiVersion: v1 # 必选,API的版本号
kind: Pod       # 必选,类型Pod
metadata:       # 必选,元数据
  name: nginx   # 必选,符合RFC 1035规范的Pod名称
  namespace: default # 可选,Pod所在的命名空间,不指定默认为default,可以使用-n 指定namespace 
  labels:       # 可选,标签选择器,一般用于过滤和区分Pod
    app: nginx
    role: frontend # 可以写多个
  annotations:  # 可选,注释列表,可以写多个
    app: nginx
spec:   # 必选,用于定义容器的详细信息
  initContainers: # 初始化容器,在容器启动之前执行的一些初始化操作
  - command:
    - sh
    - -c
    - echo "I am InitContainer for init some configuration"
    image: busybox
    imagePullPolicy: IfNotPresent
    name: init-container
  containers:   # 必选,容器列表
  - name: nginx # 必选,符合RFC 1035规范的容器名称
    image: nginx:latest    # 必选,容器所用的镜像的地址
    imagePullPolicy: Always     # 可选,镜像拉取策略
    command: # 可选,容器启动执行的命令
    - nginx 
    - -g
    - "daemon off;"
    workingDir: /usr/share/nginx/html       # 可选,容器的工作目录
    volumeMounts:   # 可选,存储卷配置,可以配置多个
    - name: webroot # 存储卷名称
      mountPath: /usr/share/nginx/html # 挂载目录
      readOnly: true        # 只读
    ports:  # 可选,容器需要暴露的端口号列表
    - name: http    # 端口名称
      containerPort: 80     # 端口号
      protocol: TCP # 端口协议,默认TCP
    env:    # 可选,环境变量配置列表
    - name: TZ      # 变量名
      value: Asia/Shanghai # 变量的值
    - name: LANG
      value: en_US.utf8
    resources:      # 可选,资源限制和资源请求限制
      limits:       # 最大限制设置
        cpu: 1000m
        memory: 1024Mi
      requests:     # 启动所需的资源
        cpu: 100m
        memory: 512Mi
    startupProbe: # 可选,检测容器内进程是否完成启动。注意三种检查方式同时只能使用一种。
      httpGet:      # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序
        path: /api/successStart # 检查路径
        port: 80
    readinessProbe: # 可选,健康检查。注意三种检查方式同时只能使用一种。
      httpGet:      # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
         path: / # 检查路径
         port: 80        # 监控端口
    livenessProbe:  # 可选,健康检查
      tcpSocket:    # 端口检测方式
            port: 80
      initialDelaySeconds: 60       # 初始化时间
      timeoutSeconds: 2     # 超时时间
      periodSeconds: 5      # 检测间隔
      successThreshold: 1 # 检查成功为2次表示就绪
      failureThreshold: 2 # 检测失败1次表示未就绪
  restartPolicy: Always   # 可选,默认为Always
  imagePullSecrets:     # 可选,拉取镜像使用的secret,可以配置多个
  - name: default-dockercfg-86258
  hostNetwork: false    # 可选,是否为主机模式,如是,会占用主机端口
  volumes:      # 共享存储卷列表
  - name: webroot # 名称,与上述对应
    emptyDir: {}    # 挂载目录

从上面得输出可以看出有部分请求处理失败了,要弄清楚失败的原因就需要弄明白当应用在滚动更新期间重新路由流量时,从旧的 Pod 实例到新的实例究竟会发生什么,首先让我们先看看 Kubernetes 是如何管理工作负载连接的。

$ fortio load -a -c 8 -qps 1000 -t 60s http://192.168.1.103:30413
# target 50% 0.0629646
# target 75% 0.0782498
# target 90% 0.0940893
# target 99% 0.156753
# target 99.9% 0.38783
Sockets used: 86 (for perfect keepalive, would be 8)
Jitter: false
Code  -1 : 7 (0.1 %)
Code 200 : 7210 (99.9 %)
Response Header Sizes : count 7217 avg 275.91659 +/- 8.797 min 0 max 295 sum 1991290
Response Body/Total Sizes : count 7217 avg 27802.192 +/- 866.3 min 0 max 27848 sum 200648420
All done 7217 calls (plus 8 warmup) 66.533 ms avg, 120.2 qps
Successfully wrote 4343 bytes of Json data to 2021-10-14-170249_192_168_1_103_30413_es.json

失败原因分析

kubernetes kube-proxy

Kubernetes 会根据 Pods 的状态去更新 Endpoints 对象,这样就可以保证 Endpoints 中包含的都是准备好处理请求的 Pod。一旦新的 Pod 处于活动状态并准备就绪后,Kubernetes 就将会停止旧的 Pod,从而将 Pod 的状态更新为 “Terminating”,然后从 Endpoints 对象中移除,并且发送一个 SIGTERM 信号给 Pod 的主进程。SIGTERM 信号就会让容器以正常的方式关闭,并且不接受任何新的连接。Pod 从 Endpoints 对象中被移除后,前面的负载均衡器就会将流量路由到其他(新的)Pod 中去。因为在负载均衡器注意到变更并更新其配置之前,终止信号就会去停用 Pod,而这个重新配置过程又是异步发生的,并不能保证正确的顺序,所以就可能导致很少的请求会被路由到已经终止的 Pod 上去了,也就出现了上面我们说的情况。

零宕机实现

生命周期钩子函数是同步的,所以必须在将最终停止信号发送到容器之前完成,在我们的示例中,我们使用该钩子简单的等待,然后 SIGTERM 信号将停止应用程序进程。同时,Kubernetes 将从 Endpoints 对象中删除该 Pod,所以该 Pod 将会从我们的负载均衡器中排除,基本上来说我们的生命周期钩子函数等待的时间可以确保在应用程序停止之前重新配置负载均衡器:

readinessProbe:
  # ...
lifecycle:
  preStop:
    exec:
      command: ["/bin/bash", "-c", "sleep 20"]

我们这里使用 preStop 设置了一个 20s 的宽限期,Pod 在真正销毁前会先 sleep 等待 20s,这就相当于留了时间给 Endpoints 控制器和 kube-proxy 更新去 Endpoints 对象和转发规则,这段时间 Pod 虽然处于 Terminating 状态,即便在转发规则更新完全之前有请求被转发到这个 Terminating 的 Pod,依然可以被正常处理,因为它还在 sleep,没有被真正销毁。

现在,当我们去查看滚动更新期间的 Pod 行为时,我们将看到正在终止的 Pod 处于 Terminating 状态,但是在等待时间结束之前不会关闭的,如果我们使用 Fortio 重新测试下,则会看到零失败请求的理想状态。

$ fortio load -a -c 8 -qps 1000 -t 60s http://192.168.1.103:30413
> 0.5 <= 0.573803 , 0.536902 , 100.00, 2
# target 50% 0.0603096
# target 75% 0.0730083
# target 90% 0.0857454
# target 99% 0.13218
# target 99.9% 0.190947
Sockets used: 80 (for perfect keepalive, would be 8)
Jitter: false
Code 200 : 7716 (100.0 %)
Response Header Sizes : count 7716 avg 276.17729 +/- 1.827 min 276 max 295 sum 2130984
Response Body/Total Sizes : count 7716 avg 27829.177 +/- 1.827 min 27829 max 27848 sum 214729932
All done 7716 calls (plus 8 warmup) 62.217 ms avg, 128.5 qps
Successfully wrote 3483 bytes of Json data to 2021-10-14-171008_192_168_1_103_30413_es.json

温馨提示: Kubernetes 将在 30 秒后强行终止该进程( Pod 定义中的 terminationGracePeriodSeconds) terminationGracePeriodSeconds默认值是30s

0

评论区