容器无限重启?会不会是 K8S 探针配置有问题
1. 项目无限重启
最近,我遇到了一个懊恼的问题。
自己的 Java 程序生产环境上了 K8S,一会儿好一会不好,Pod 每次发布有一定概率无限重启,明明测试环境都是没问题的,难道我真的不适合用 K8S ?冷静下来思考一阵子,认为 K8S 绝对不会有问题的,肯定是自身哪里配置有问题,最后定位了两个方向:
- 我的 CPU 和 内存 指标设置的有问题, OOM 导致我的项目重启
- 我设置的重启策略是 Always,探针上的配置出问题导致失败无限重启
当然后边我学会了看 Pod 事件日志,很明显的定位了是 liveness 探针失败导致的重启,但是我认为最重要的是一个学习的过程,我先贴出此时的项目 Pod 部分配置:
livenessProbe:
httpGet:
path: $HEALTH_URL
port: $PORT
scheme: HTTP
initialDelaySeconds: 60
timeoutSeconds: 2
periodSeconds: 10
successThreshold: 1
failureThreshold: 5
readinessProbe:
httpGet:
path: $HEALTH_URL
port: $PORT
scheme: HTTP
timeoutSeconds: 2
periodSeconds: 10
successThreshold: 1
failureThreshold: 5
问题现状:我的 Java 项目启动龟速,没有个 100 到 200 秒启动不了,replicas 设置多个后 Pod 启动完成更慢,Pod 每次发布有一定概率无限重启。
原因探索:由于我的 Java 容器启动很慢,重启策略设置的是 Always,liveness 探针从配置上看,最大容忍时间大约为 60+10*5=110s,项目启动时间正好在 100 到 200 秒 之间,所以才会出现概率性无限重启的问题。
解决方案:那是不是我只要暴力的修改重启策略,或者直接把时间调大就一了百了呢?作为一个合格的开发人员,直觉告诉我肯定不能这么简单解决问题,那我们就来仔细分析下这个问题。
2. Pod 常见的状态
先来看看 Pod 常见的状态有哪些:
- Pending:挂起,我们在请求创建 Pod 时,条件不满足,调度没有完成,没有任何一个节点能满足调度条件。已经创建了但是没有适合它运行的节点叫做挂起,这其中也包含集群为容器创建网络,或者下载镜像的过程。
- Running:Pod 内所有的容器都已经被创建,且至少一个容器正在处于运行状态、正在启动状态或者重启状态。
- Succeeded:Pod 中所有容器都执行成功后退出,并且没有处于重启的容器。
- Failed:Pod 中所以容器都已退出,但是至少还有一个容器退出时为失败状态。
- Unknown:未知状态,所谓 Pod 是什么状态是 api-server 和运行在 Pod 节点的 kubelet 进行通信获取状态信息的,如果节点之上的 kubelet 本身出故障,那么 api-server 就连不上 kubelet,得不到信息了,就会看 Unknown。
Pod 重启策略
- Always: 只要容器失效退出就重新启动容器。
- OnFailure: 当容器以非正常(异常)退出后才自动重新启动容器。
- Never: 无论容器状态如何,都不重新启动容器。
如果 Pod 的 restartpolicy 没有设置,那么 默认值是 Always。
3. livenessProbe 和 readinessProbe 两种探针
在 Kubernetes 中 Pod 是最小的计算单元,而一个 Pod 又由多个容器组成,相当于每个容器就是一个应用,应用在运行期间,可能因为某也意外情况致使程序挂掉。
那么如何监控这些容器状态稳定性?保证服务在运行期间不会发生问题,发生问题后进行重启等机制,就成为了重中之重的事情,考虑到这点 Kubernetes 推出了活性探针机制。
livenessProbe(存活性探针) 能保证程序在运行中如果挂掉能够自动重启。但是还有个经常遇到的问题,比如说,在 Kubernetes 中启动 Pod,显示明明Pod已经启动成功,且能访问里面的端口,但是却返回错误信息。还有就是在执行滚动更新时候,总会出现一段时间,Pod 对外提供网络访问,但是访问却发生404,这两个原因,都是因为 Pod 已经成功启动,但是 Pod 的的容器中应用程序还在启动中导致,考虑到这点Kubernetes 推出了 readinessProbe (就绪性探针) 机制。
livenessProbe:重启机制
livenessProbe:存活性探针,用于判断容器是不是健康,如果不满足健康条件,那么 Kubelet 将根据 Pod 中设置的 restartPolicy (重启策略)来判断,Pod 是否要进行重启操作。
LivenessProbe 按照配置去探测 ( 进程、或者端口、或者命令执行后是否成功等等),来判断容器是不是正常。如果探测不到,代表容器不健康(可以配置连续多少次失败才记为不健康),则 kubelet 会杀掉该容器,并根据容器的重启策略做相应的处理。如果未配置存活探针,则默认容器启动为通过(Success)状态。即探针返回的值永远是 Success。即 Success 后 Pod 状态是 RUNING。
readinessProbe:负载机制
readinessProbe 就绪性探针,用于判断容器内的程序是否存活(或者说是否健康),只有程序(服务)正常, 容器开始对外提供网络访问(启动完成并就绪)。
容器启动后按照 readinessProbe 配置进行探测,无问题后结果为成功即状态为 Success。Pod 的 READY 状态为 true,从 0/1 变为 1/1。如果失败继续为 0/1,状态为 false。若未配置就绪探针,则默认状态容器启动后为 Success。对于此 Pod、此 Pod 关联的 Service 资源、EndPoint 的关系也将基于 Pod 的 Ready 状态进行设置,如果 Pod 运行过程中 Ready 状态变为 false,则系统自动从 Service 资源 关联的 EndPoint 列表中去除此 Pod,届时 Service 资源接收到 GET 请求后,kube-proxy 将一定不会把流量引入此 Pod 中,通过这种机制就能防止将流量转发到不可用的 Pod 上。如果 Pod 恢复为 Ready 状态。将再会被加回 Endpoint 列表。kube-proxy 也将有概率通过负载机制会引入流量到此 Pod 中。
livenessProbe 和 readinessProbe 区别
ReadinessProbe 和 livenessProbe 可以使用相同探测方式,只是对 Pod 的处置方式不同:
- livenessProbe 当检测失败后,将杀死容器并根据 Pod 的重启策略来决定作出对应的措施。
- readinessProbe 当检测失败后,将 Pod 的 IP:Port 从对应的 EndPoint 列表中删除。
livenessProbe 和 readinessProbe 参数属性
探针(Probe)有许多可选字段,可以用来更加精确的控制 Liveness 和 Readiness 两种探针的行为(Probe):
- initialDelaySeconds:容器启动后要等待多少秒后就探针开始工作,单位“秒”,默认是 0 秒,最小值是 0;
- periodSeconds:执行探测的时间间隔(单位是秒),默认为 10s,单位“秒”,最小值是 1;
- timeoutSeconds:探针执行检测请求后,等待响应的超时时间,默认为 1s,单位“秒”,最小值是 1;
- successThreshold:探针检测失败后认为成功的最小连接成功次数,默认为 1s,在 Liveness 探针中必须为 1s,最小值为 1s;
- failureThreshold:探测失败的重试次数,重试一定次数后将认为失败,在 readiness 探针中,Pod 会被标记为未就绪,默认为 3s,最小值为 1s;
注意:initialDelaySeconds 在 readinessProbe 其实可以不用配置,不配置默认 pod 刚启动,开始进行 readinessProbe 探测,但那又怎么样,除了 startupProbe,readinessProbe、livenessProbe 运行在 pod 的整个生命周期,刚启动的时候 readinessProbe 检测失败了,只不过显示 READY 状 态一直是 0/1,readinessProbe 失败并不会导致重启 pod,只有 startupProbe、livenessProbe 失败才会重启 pod。而等到多少s后,真正服务启动 后,检查 success 成功后,READY 状态自然正常。
livenessProbe 和 readinessProbe 探测方法
- ExecAction:在容器中执行指定的命令,如果执行成功,退出码为 0 则探测成功。
- HTTPGetAction:通过容器的IP地址、端口号及路径调用 HTTP Get方法,如果响应的状态码大于等于200且小于400,则认为容器 健康。
- TCPSocketAction:通过容器的 IP 地址和端口号执行 TCP 检 查,如果能够建立 TCP 连接,则表明容器健康。
探针探测结果有以下值:
- Success:表示通过检测。
- Failure:表示未通过检测。
- Unknown:表示检测没有正常进行。
下边放几个使用的案例,几种探针的使用方法相同,startupProbe 后续章节会详细解释:
ExecAction:
startupProbe:
failureThreshold: 3
exec:
command: ['/bin/sh','-c','echo Hello World']
initialDelaySeconds: 20
periodSeconds: 3
successThreshold: 1
timeoutSeconds: 2
HTTPGetAction:
livenessProbe:
failureThreshold: 3
httpGet:
path: /
port: 8080
scheme: HTTP
initialDelaySeconds: 20
periodSeconds: 3
successThreshold: 1
timeoutSeconds: 2
TCPSocketAction:
readinessProbe:
failureThreshold: 3
tcpSocket:
port: 8080
initialDelaySeconds: 20
periodSeconds: 3
successThreshold: 1
timeoutSeconds: 2
4. startupProbe 启动探针
K8S 在 1.16 版本后增加 startupProbe 探针,主要解决在复杂的程序中 readinessProbe、livenessProbe探针无法更好的判断程序是否启动、是否存活。进而引入 startupProbe 探针为 readinessProbe、livenessProbe 探针服务。
- 如果三个探针同时存在,先执行 startupProbe 探针,其他两个探针将会被暂时禁用,直到 Pod 满足 startupProbe 探针配置的条件,其他两个探针启动,如果不满足按照规则重启容器。
- 另外两种探针在容器启动后,会按照配置,直到容器消亡才停止探测,而 startupProbe 探针只是在容器启动后按照配置满足一次后,不在进行后续的探测。
startupProbe 参数属性、探测方法
startupProbe 探针的使用方法跟 readinessProbe 和 livenessProbe 相同,对 Pod 的处置跟livenessProbe 方式相同,失败重启。
startupProbe 的存在意义是什么?
我们回到开头问题的粗暴的解决方案:
livenessProbe:
httpGet:
path: $HEALTH_URL
port: $PORT
scheme: HTTP
initialDelaySeconds: 60
timeoutSeconds: 2
periodSeconds: 10
successThreshold: 1
failureThreshold: 5
我的 Java 项目启动龟速,100秒还不够甚至需要200秒,我们设置存活探针延迟探测(initialDelaySeconds) 为 60s 后开始探测,然后探针探测的时候发现服务不正常,甚至失败了(failureThreshold) 5次以后仍然未启动完毕,然后就开始重启 Pod 陷入死循环。但是如果问题在这个地方,那我们可以把探针的延迟探测 (initialDelaySeconds) 时间调整大一点,或者直接把失败容忍次数 (failureThreshold) 或 循环探测涉及的其他参数 设置更大不就行了吗?为什么还要单独的设置一个 satrtupProbe 呢?
粗暴的解决方案会带来如下两个问题:
- 如果 initialDelaySeconds 设置过大(无法预测项目启动时间,粗略估计100-200秒甚至更长,那我直接设置300秒),此时每次发布,无论项目启动快慢,我都需要等到5分钟以后才能得到 Pod状态的反馈,时间过长我无法容忍;
- 按上边的 timeoutSeconds 和 periodSeconds 设置,一次失败检测周期 10秒,并提供了 failureThreshold 5次失败机会,这时候把 failureThreshold 改为 10次失败机会,60+10*10=160s 似乎接近覆盖项目启动时间,可以减少无限重启的概率,但是如果我的项目真的存在问题,在启动成功后,后续每10秒一个探测中,当项目已停止我需要 10*10=100s 左右才能被发现,然后重启,这种反射弧在线上环境是无法被容忍的;
现在我们引入 startupProbe 探针的配置:
startupProbe: #容器启动检查(可选参数)
httpGet:
path: $HEALTH_URL
port: $PORT
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
failureThreshold: 60
-
容器启动后,当 startupProbe 探针存在时,先执行 startupProbe 探针,其他两个探针将会被暂时禁用,并且 startupProbe 探针只会执行一次;
-
当容器启动后,延迟(initialDelaySeconds)5秒后,并且每(periodSeconds)5秒探测一次,(timeoutSeconds)这里没设置默认是 1秒包含在(periodSeconds)5秒内,最多容忍失败(failureThreshold)60次,当我在 60*5=300s 这个区间内,只要成功1次(例如我项目 108秒 就启动完成),大约在 108+(108-5)%5=111s 左右我就能得到 Succeeded 的状态反馈。
我们现在给出一个比较合理的完整配置:
startupProbe:
httpGet:
path: $HEALTH_URL
port: $PORT
scheme: HTTP
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 5
successThreshold: 1
failureThreshold: 60
livenessProbe:
httpGet:
path: $HEALTH_URL
port: $PORT
scheme: HTTP
initialDelaySeconds: 5
timeoutSeconds: 2
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
readinessProbe:
httpGet:
path: $HEALTH_URL
port: $PORT
scheme: HTTP
timeoutSeconds: 2
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
这个设置我们可以得到如下结论:
- 假设我的项目启动时间为 N 秒,当然 N 不能超过360秒,那么我在 N+(N-5)%5 秒左右,能收到 Pod 成功启动的反馈(假设 replicas=1);
- 如果项目运行中崩溃,我会在 5*3=15 秒左右,判定容器无响应,自动重启容器解决问题;
- 如果项目运行中崩溃,我会在 5*3=15 秒左右,判定容器无响应,自动切断负载均衡流量。