哈喽,大家好,我是明智
今天跟大家讨论一个服务稳定性相关的话题,对于大部分做业务的小伙伴来说,很少会被问到这类问题
不过你如果你希望面试公司的一些基础部门,例如:基础架构、效能开发、服务稳定性保障等,就很可能遇到
笔者在前段时间的面试中就被问到了,这个问题还有一系列的问法,例如:
服务发布过程中总是出现5xx怎么办?
如何实现服务的优雅启停?
本文主要分为以下几个部分
保证流量无损的关键是什么?
K8s环境下如何保证流量无损?
如何验证?
会有什么消极影响?
❝本文只讨论K8s环境下的如何做到优雅启停
流量无损的关键
发布过程中损失的流量主要包括:
在启动一个新pod时,尚未完全就绪,就接收到了流量,导致服务响应500。例如服务依赖的db、redis还未启动
在停止一个旧pod时,这个pod中仍然存在正在处理中的请求,请求尚未处理完,pod就被杀死
在pod停止的过程中,仍然有部分流量被路由到这个pod上
pod已经被杀死了,但是网关、注册中心等没有及时更新数据,导致仍然有请求被路由到一个不存在的实例上
因此我们要保证流量无损,要做到以下几点
每次杀死一个pod前,应该先启动一个新pod。即:滚动发布
启动一个新pod时,需要等待pod完全就绪,才能让这个pod接收流量。这需要使用到K8s的三种探针
pod在被杀死前,需要先保证流量被摘除干净。这需要使用到K8s提供的preStopHook
pod的流量摘除干净后,需要保证pod中正在处理的流量正常响应。这需要服务自身响应kill信息
K8s环境下如何保证流量无损
K8s中的滚动发布
如果是Deployment
,可以通过配置 Deployment
的 rollingUpdate
策略来控制滚动更新的行为。
关键参数
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的三种探针
我们使用这三种探针的目的在于:
保证pod在完全就绪后才承接流量
当应用出现故障时可以及时重启,实现故障自动恢复
Startup Probe
(启动探针):如果配置了启动探针,在进行启动探测成功之前,不会进行存活探测。如果启动探测失败,k8s会重启pod。启动探针探测成功后,Liveness Probe
和 Readiness 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 的删除过程中,同时会存在两条并行的时间线,如下图所示。
一条时间线是网络规则的更新过程。
另一条时间线是 Pod 的删除过程。
网络层面
Pod 被删除,状态置为 Terminating。
Endpoint Controller 将该 Pod 的 ip 从 Endpoint 对象中删除。
Kube-proxy 根据 Endpoint 对象的改变更新 iptables 规则,不再将流量路由到被删除的 Pod。
网关、注册中心等更新数据,剔除对应的pod ip。
Pod 层面
Pod 被删除,状态置为 Terminating。
Kubelet 捕获到 ApiServer 中 Pod 状态变化,执行 syncPod 动作。
如果 Pod 配置了 preStop Hook ,将会执行。
kubelet 对 Pod 中各个 container 发送调用 cri 接口中 StopContainer 方法,向 dockerd 发送 stop -t 指令,用 SIGTERM 信号以通知容器内应用进程开始优雅停止。
等待容器内应用进程完全停止,如果在 terminationGracePeriodSeconds (默认 30s) - preStop 执行时间内还未完全停止,就发送 SIGKILL 信号强制杀死应用进程。
所有容器进程终止,清理 Pod 资源。
由于网络层面跟pod层面的变更是并行的,二者状态的不一致,会导致pod在停机的过程中仍然在接收流量。为了规避这个问题,我们需要使用到K8s提供的preStopHook
引入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
将以下文件保存为
xxx.sh
,执行
chmod +x xxx.sh
./ xxx.sh
,运行这个脚本发布服务,观察脚本输出
在服务发布过程中,没有出现一次请求失败,说明接入成功
带来的影响
接入优雅启停对服务最大的影响在于发布时间会延长。发布时间包括:新pod的启动时间+旧pod的终止时间
如果按照以下配置:
探针初始延迟45s
preStopHook中等待15s
滚动比例为25%,分4批 一个应用一个批次滚动完需要先启动新的pod,再杀死老的pod,因此如果一个应用的pod数量超过4个,单次发布的时间预计在4分钟左右
解决方案:可以通过调整滚动比例跟探针的检测时间来调整这个时间,例如滚动比例调整到30%,探针初始延迟调整到30s
总结
本文主要跟大家介绍了K8s环境下如何实现服务的优雅启停来保证服务发布过程中流量无损,咱们不仅学会了怎么做,也知道了为什么要怎么做。
在文中我有提到,要实现服务的优雅启停还需要服务自身正确的响应kill信息
,其实就是K8s发出的SIGTERM
信号,对应kill -15
命令。
服务正确响应SIGTERM
信号要怎么做本文没有提到,另外本文只提到了如何配置K8s的探针,但没有涉及到探针如何实现。考虑到篇幅原因,关于应用自身需要做的事情,我们放到下篇文章中。
作者简介:
大三退学,创业、求职、自考,一路升级
7年it从业经验,多个开源社区contributor
专注分享成长路上的所悟所得
长期探索
个人成长
|职业发展
|副业探索
推荐阅读:
美团一面问我i++跟++i的区别是什么
美团一面,你碰到过CPU 100%的情况吗?你是怎么处理的?
美团一面:碰到过OOM吗?你是怎么处理的?
美团一面,发生OOM了,程序还能继续运行吗?