哈喽,大家好,我是明智

今天跟大家讨论一个服务稳定性相关的话题,对于大部分做业务的小伙伴来说,很少会被问到这类问题

不过你如果你希望面试公司的一些基础部门,例如:基础架构、效能开发、服务稳定性保障等,就很可能遇到

笔者在前段时间的面试中就被问到了,这个问题还有一系列的问法,例如:

  1. 服务发布过程中总是出现5xx怎么办?

  2. 如何实现服务的优雅启停?

本文主要分为以下几个部分

  1. 保证流量无损的关键是什么?

  2. K8s环境下如何保证流量无损?

  3. 如何验证?

  4. 会有什么消极影响?

本文只讨论K8s环境下的如何做到优雅启停

流量无损的关键

发布过程中损失的流量主要包括:

  1. 在启动一个新pod时,尚未完全就绪,就接收到了流量,导致服务响应500。例如服务依赖的db、redis还未启动

  2. 在停止一个旧pod时,这个pod中仍然存在正在处理中的请求,请求尚未处理完,pod就被杀死

  3. 在pod停止的过程中,仍然有部分流量被路由到这个pod上

  4. pod已经被杀死了,但是网关、注册中心等没有及时更新数据,导致仍然有请求被路由到一个不存在的实例上

因此我们要保证流量无损,要做到以下几点

  1. 每次杀死一个pod前,应该先启动一个新pod。即:滚动发布

  2. 启动一个新pod时,需要等待pod完全就绪,才能让这个pod接收流量。这需要使用到K8s的三种探针

  3. pod在被杀死前,需要先保证流量被摘除干净。这需要使用到K8s提供的preStopHook

  4. pod的流量摘除干净后,需要保证pod中正在处理的流量正常响应。这需要服务自身响应kill信息

K8s环境下如何保证流量无损

K8s中的滚动发布

如果是Deployment ,可以通过配置 DeploymentrollingUpdate 策略来控制滚动更新的行为。

关键参数

  • maxUnavailable:指定在更新过程中,最多可以有多少个 Pod 处于不可用状态。可以设置为绝对数量(如 1)或百分比(如 25%)。

  • maxSurge:指定在更新过程中,最多可以额外创建多少个 Pod。可以设置为绝对数量(如 1)或百分比(如 25%)。

piVersion: apps/v1
kind: Deployment
metadata:
  name: example-deployment
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1        # 在更新过程中最多允许 1 个 Pod 不可用
      maxSurge: 1              # 在更新过程中最多允许额外创建 1 个 Pod

如果是statefulSet,其滚动更新策略由 updateStrategy 字段指定,主要有以下几个配置选项:

  • **RollingUpdate**:这是 StatefulSet 唯一支持的更新策略类型,按序号从高到低逐个更新 Pod。

  • **Partition**:可选字段,用于指定更新过程中保留的 Pod 的最小编号。只有编号大于该分区的 Pod 会被更新。默认值为 0,这意味着所有 Pod 都会被更新。

K8s的三种探针

我们使用这三种探针的目的在于:

  1. 保证pod在完全就绪后才承接流量

  2. 当应用出现故障时可以及时重启,实现故障自动恢复

da8c1d0bdde42d911c06942f71352533.jpeg
webp

Startup Probe   (启动探针):如果配置了启动探针,在进行启动探测成功之前,不会进行存活探测。如果启动探测失败,k8s会重启pod。启动探针探测成功后,Liveness ProbeReadiness Probe 将接管健康检查。Startup Probe 适合用于那些启动时间较长的应用程序,防止它们在启动过程中被误判为失败。如果应用启动时间不是特别长,没有必要配置这个探针。

Liveness Probe (存活探针):存活探测如果失败,K8s会重启Pod。主要用于检测可通过重启解决的故障,例如:服务端可用线程耗尽、死锁等

Readiness Probe (就绪探针):就绪探测成功之后,才会将pod加入到svc的端点列表中。如果就绪探测失败,会将pod从端点中剔除

使用示例

deployment文件添加探针:

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      # 在containers下添加两个指针
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          # pod启动后延迟60s探测
          initialDelaySeconds: 45
          # 每30s测一次
          periodSeconds: 10
          # 每次探测最大超时时间3s
          timeoutSeconds: 3
          # 连续6次探测失败则重启pod
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
                  livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 45
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3

注意事项

1、添加liveness跟readiness探针后,如果组件状态异常,会导致pod重启, 在特殊场景下, 组件异常并不需要重启, 需要使用方自行判断。

例如:redis挂掉, 但是在服务中, redis缓存允许穿透或者异常, redis挂掉不应该重启机器, 这个在healthcheck中,redis状态异常不应该触发liveness和readness失败

2、initialDelaySeconds的值应该根据应用启动时间进行合理设置,如果设置过短,会导致pod反复被kill无法正常启动

K8s的preStopHook

K8s杀死一个pod的过程

在 Kubernetes Pod 的删除过程中,同时会存在两条并行的时间线,如下图所示。

  1. 一条时间线是网络规则的更新过程。

  2. 另一条时间线是 Pod 的删除过程。

c162be9d88f8bfca7d17bbeb304b9ada.png
pod删除过程

网络层面

  1. Pod 被删除,状态置为 Terminating。

  2. Endpoint Controller 将该 Pod 的 ip 从 Endpoint 对象中删除。

  3. Kube-proxy 根据 Endpoint 对象的改变更新 iptables 规则,不再将流量路由到被删除的 Pod。

  4. 网关、注册中心等更新数据,剔除对应的pod ip。

Pod 层面

  1. Pod 被删除,状态置为 Terminating。

  2. Kubelet 捕获到 ApiServer 中 Pod 状态变化,执行 syncPod 动作。

  3. 如果 Pod 配置了 preStop Hook ,将会执行。

  4. kubelet 对 Pod 中各个 container 发送调用 cri 接口中 StopContainer 方法,向 dockerd 发送 stop -t 指令,用 SIGTERM 信号以通知容器内应用进程开始优雅停止。

  5. 等待容器内应用进程完全停止,如果在 terminationGracePeriodSeconds (默认 30s) - preStop 执行时间内还未完全停止,就发送 SIGKILL 信号强制杀死应用进程。

  6. 所有容器进程终止,清理 Pod 资源。

由于网络层面跟pod层面的变更是并行的,二者状态的不一致,会导致pod在停机的过程中仍然在接收流量。为了规避这个问题,我们需要使用到K8s提供的preStopHook

ddd3c1928aedfa6978e3fb5eebb9a100.png

引入preStopHook后,可以在pod杀死前,先执行preStopHook,在preStopHook中我们可以执行一个脚本,这个脚本可以简单的sleep一段时间,等待网络层变更完成后pod再响应SIGTERM信号,确保pod停止时,流量已经完全摘除,如下:

spec:
  contaienrs:
  - name: my-awesome-container
    lifecycle:
      preStop:
        exec:
         command: ["sh", "-c", "sleep 10"] # 延迟关闭 10秒

很多情况下,pod除了接收K8s调度的流量外,还会接收来自rpc的流量,例如:dubbo,这个时候,我们可以在实现一个负责的脚本,除了等待k8s网络层变更完成外,还需要将服务从注册中心中下限

spec:
  contaienrs:
  - name: my-awesome-container
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh","-c","/pre-stop.sh"]

应用程序提供一个服务下线的借口,在pre-stop.sh中,可以调用这个接口,实现服务下线

如何验证

这里我直接给出一个验证脚本,大家将其中的参数替换为服务核心接口的url即可:

#!/bin/bash

# 目标 URL
url="your url"

# 请求失败的次数计数器
fail_count=0

# 总请求次数计数器
total_count=0

# 日志文件
log_file="request_log.txt"

# 发送请求并统计失败次数的函数
send_request() {
    response=$(curl --silent --show-error --write-out "HTTPSTATUS:%{http_code}" "$url")

    # 提取HTTP状态码
    http_status=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
    response_body=$(echo "$response" | sed -e 's/HTTPSTATUS\:.*//g')

    timestamp=$(date "+%Y-%m-%d %H:%M:%S")
 ((total_count++))
    if [ "$http_status" -ne 200 ]; then
        ((fail_count++))
        echo "$timestamp - 请求失败, HTTP状态码: $http_status, 响应: $response_body" | tee -a "$log_file"
    else
        echo "$timestamp - 请求成功, HTTP状态码: $http_status, 响应: $response_body" | tee -a "$log_file"
    fi
}

# 每秒钟发送一个请求
while true; do
    send_request
    echo "失败次数: $fail_count,请求总次数:$total_count" | tee -a "$log_file"
    sleep 1
done
  1. 将以下文件保存为xxx.sh

  2. 执行chmod +x xxx.sh

  3. ./ xxx.sh,运行这个脚本

  4. 发布服务,观察脚本输出

    62bcc8ce387c44a7dd83d9e9dbf8bc9c.png

    在服务发布过程中,没有出现一次请求失败,说明接入成功

带来的影响

接入优雅启停对服务最大的影响在于发布时间会延长。发布时间包括:新pod的启动时间+旧pod的终止时间

如果按照以下配置:

  • 探针初始延迟45s

  • preStopHook中等待15s

  • 滚动比例为25%,分4批 一个应用一个批次滚动完需要先启动新的pod,再杀死老的pod,因此如果一个应用的pod数量超过4个,单次发布的时间预计在4分钟左右

    解决方案:可以通过调整滚动比例跟探针的检测时间来调整这个时间,例如滚动比例调整到30%,探针初始延迟调整到30s

总结

本文主要跟大家介绍了K8s环境下如何实现服务的优雅启停来保证服务发布过程中流量无损,咱们不仅学会了怎么做,也知道了为什么要怎么做。

在文中我有提到,要实现服务的优雅启停还需要服务自身正确的响应kill信息,其实就是K8s发出的SIGTERM信号,对应kill -15命令。

服务正确响应SIGTERM信号要怎么做本文没有提到,另外本文只提到了如何配置K8s的探针,但没有涉及到探针如何实现。考虑到篇幅原因,关于应用自身需要做的事情,我们放到下篇文章中。

作者简介

  1. 大三退学,创业、求职、自考,一路升级

  2. 7年it从业经验,多个开源社区contributor

  3. 专注分享成长路上的所悟所得

  4. 长期探索 个人成长职业发展副业探索

推荐阅读

美团一面问我i++跟++i的区别是什么

美团一面,你碰到过CPU 100%的情况吗?你是怎么处理的?

美团一面:碰到过OOM吗?你是怎么处理的?

美团一面,发生OOM了,程序还能继续运行吗?

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐