Kubernetes 优雅停止 Pod

Kubernetes Pod Hook
标签(空格分隔): Kubernetes

原文:https://i4t.com/4424.html

首先我们先简单的分析一下”优雅的停止 Pod”

优雅停止 (Graceful shutdown) 这个说法来自于操作系统,比如我们 windows 关机系统首先会退出软件然后一步步到达关机,而相对的就是硬终止(Hard shutdown),简单的理解就是直接拔电源

到了微服务中,网关会把流量分配给每个 Pod 节点上,比如我们上线更新 Pod 的时候

如果我们直接将 Pod 杀死,那这部分流量就无法得到正确处理,会影响部分用户,通常来说网关或者注册中心会将我们的服务保持一个心跳,过了心跳超时之后会自动摘除我们的服务,但是有一个问题就是超时时间可能是 30 秒也可能是 60 秒,虽然不会影响我们的系统,但是会产生用户轻微抖动。
如果我们在停止前执行一条命令,通知网关或者注册中心这台主机进行下线,那么注册中心就会标记这台主机已经下线,不进行流量转发,用户就不会有任何影响,这就是优雅停止,将滚动更新影响最小化
Pod Hook
Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中。我们可以同时为 Pod 中的所有容器都配置 hook

在 k8s 中,理想的状态是 pod 优雅释放,并产生新的 Pod。但是并不是每一个 Pod 都会这么顺利

Pod 卡死,处理不了优雅退出的命令或者操作
优雅退出的逻辑有 BUG,陷入死循环
代码问题,导致执行的命令没有效果
对于以上问题,k8s 的 Pod 终止流程中还有一个”最多可以容忍的时间”,即 grace period (在 pod 的.spec.terminationGracePeriodSeconds 字段定义),这个值默认是 30 秒,当我们执行 kubectl delete 的时候也可以通过–grace-period 参数显示指定一个优雅退出时间来覆盖 Pod 中的配置,如果我们配置的 grace period 超过时间之后,k8s 就只能选择强制 kill Pod

Kubernetes 为我们提供了两种钩子函数:

PostStart : 这个钩子在容器创建后立即执行。但是,并不能保证钩子将在容器 ENTRYPOINT 之前运行,因为没有参数传递给处理程序。 主要用于资源部署、环境准备等。不过需要注意的是如果钩子花费时间过长以及于不能运行或者挂起,容器将不能达到 Running 状态。
PreStop : 钩子在容器终止前立即被调用。它是阻塞的,意味着它是同步的,所以它必须在删除容器的调用出发之前完成。主要用于优雅关闭应用程序、通知其他系统等。如果钩子在执行期间挂起,Pod 阶段将停留在 Running 状态并且不会达到 failed 状态
如果 PostStart 或者 PreStop 钩子失败,它会杀死容器。所以我们应该让钩子函数尽可能的轻量。当然有些情况下,长时间运行命令是合理的,比如在停止容器之前预先保留状态。

这里稍微简单说一下 Pod 终止的过程

用户发送命令删除 Pod,Pod 进入 Terminating 状态
service 摘除 Pod 节点
当 kubelet 看到 Pod 已被标记终止,开始执行 preStop 钩子,假如 preStop hook 的运行时间超过了 grace period,kubelet 会发送 SIGTERM 并等 2 秒
官方文档介绍
在 Pod Hook 钩子函数中有 Exec 和 HTTP 两种方式

Exec - 用于执行一段特定的命令,不过要注意的是该命令小号的资源会被计入容器
HTTP - 对容器上的特定端点执行 HTTP 请求
基于 PostStart 命令演示
首先我们先进行演示 PostStart 的两种方式

第一种 Exec
我们 echo 一段话追加到 /tmp/message,在 Pod 启动前进行操作

cat >>exec_test.yaml<<EOF
apiVersion: v1
kind: Pod
metadata:
name: abcdocker
labels:
name: abcdocker
spec:
containers:
- name: abcdocker
image: nginx
ports:
- containerPort: 80
lifecycle:
postStart:
exec:
command:
- bash
- -c
- ‘echo “https://i4t.com” > /tmp/message’
EOF
使用 kubectl apply -f exec_test.yaml 进行创建

可以通过下面查看结果,pod 的目录已经有我们在 yaml 文件写的测试文件

[root@abcdocker yaml]# kubectl get pod
NAME READY STATUS RESTARTS AGE
abcdocker 1/1 Running 0 37s
[root@abcdocker yaml]# kubectl exec -it -n default abcdocker /bin/bash
root@abcdocker:/# cat /tmp/message
https://i4t.com
root@abcdocker:/#
root@abcdocker:/# exit
创建容器后,Kubernetes 立即发送 postStart 事件。但是,不能保证在调用 Container 的入口点之前先调用 postStart 处理程序。postStart 处理程序相对于 Container 的代码异步运行,但是 Kubernetes 对容器的管理会阻塞,直到 postStart 处理程序完成。在 postStart 处理程序完成之前,容器的状态不会设置为 RUNNING。

第二种 HTTP 方式
使用 HttpGet 配置 Host、Path、Port

apiVersion: v1
kind: Pod
metadata:
name: abcdocker
labels:
name: abcdocker
spec:
containers:
- name: abcdocker
image: nginx
ports:
- containerPort: 80
lifecycle:
postStart:
httpGet:
host: i4t.com
path: index.html
port: 80
这里就不进行演示了,因为日志会看不到这个请求

基于 PreStop 环境演示
起因:
在生产环境中使用 spring 框架,由于服务更新过程中,服务容器被直接充值,部分请求仍被分发到终止的容器 (没有配置钩子,熟悉默认环境),导致服务出现 500 错误,这部分错误请求数据占用比较少,因为 Pod 滚动更新都是一对一。因为部分用户会产生服务器错误的情况,考虑使用优雅的终止方式,将错误请求降到最低,直至滚动更新不影响用户

Eureka 是一个基于 REST 的服务,作为 Spring Cloud 服务注册中心,用于定位服务来进行中间层服务器的负载均衡和故障转移。各服务启动时,会向 Eureka Server 注册自己的信息 (IP、端口、服务信息等),Eureka Server 会存储这些信息,微服务启动后,会周期性(默认 30 秒) 的向 Eureka Server 发送心跳以续约自己的租期,并且可以从 eureka 中获取其他微服务的地址信息,执行相关逻辑

image_1dpi0idnqk981okaacv16l4172p9.png-61kB

由于 Eureka 默认的心跳检测为 30 秒,当 K8S 下线 Pod 时 Eureka 会有 30 秒的异常问题,所以我们需要在 Pod 停止前发送一条请求,通知 Eureka 进行下线操作,这样进行优雅的停止对用户的影响做到最小

具体 yaml 如下

apiVersion: v1
kind: Pod
metadata:
name: abcdocker
labels:
name: abcdocker
spec:
containers:
- name: abcdocker
image: nginx
ports:
- containerPort: 80
lifecycle:
preStop:
exec:
command:
- bash
- -c
- ‘curl -X POST –data DOWN http://127.0.0.1:8080/service-registry/instance-status -H
“Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8”;sleep 30’
####### 参数解释
127.0.0.1:8080 #代表 eureka 地址
service-registry #代表注册中心
DOWN #执行 down 请求
sleep #等待 30 秒
当我们删除 Pod 的时候就会执行上面的命令操作,并且等待 30 秒

[root@yzsjhl82-135 yaml]# kubectl get pod
NAME READY STATUS RESTARTS AGE
abcdocker 1/1 Running 0 2m16s
[root@yzsjhl82-135 yaml]# kubectl delete pod abcdocker
pod “abcdocker” deleted
#此刻 Pod 不会马上删除,而是执行 Exec 中的命令,并等待 30 秒
配置中添加了一个 sleep 时间,主要是作为服务停止的缓冲时间

总结: Hook 调用的日志没有暴露给 Pod 的 Event,所以只能到通过 describe 命令来获取,如果是正常的操作是不会有 event,如果有错误可以看到 FailedPostStartHook 和 FailedPreStopHook 这种 event。并且如果 Hook 调用出现错误,则 Pod 状态不会是 Running