1. 背景
最近和同事一起排查了下发版时应用会出现500错误的异常,感觉还是挺有意思的,这里做下记录,以便后续遇到问题时进行翻阅。
2. 问题状况
我们线上环境是使用的ack集群,发版时应用service对应的deployment下的pod会进行滚动更新,这种情况下会出现部分接口报500错误,初步判断是有请求调度到了正在下线中的pod,即Terminating状态,这样导致这部分请求出现报错。
3. 排查经过
出现这种问题时,已经考虑到是没做好优雅下线的问题,于是去网上搜了一下解决方式,做好java程序的优化下线,主要要做以下两件事情:
- 容器关闭时增加前置处理,让容器先不进行关闭操作,这样给k8s留出时间去修改服务的路由分发规则,同时增加容器优雅关闭时间 terminationGracePeriodSeconds,留出更多的时间供容器关闭。
- 应用内做好优雅下线设置。
3.1 容器配置修改
3.1.1 pod被删除原理
deployment下pod被删除时,会触发两条路径进行操作:
Pod层面
- Pod被删除会被置为Terminating状态
- Kubelet捕捉到Pod的变化,执行syncPod动作
- 如果Pod设置了PreStop Hook,会先执行PreStop Hook
- kubelet 对 Pod 中各个 container 发送调用 cri 接口中 StopContainer 方法,向 dockerd 发送 stop -t 指令,用 SIGTERM 信号以通知容器内应用进程开始优雅停止。
- 等待容器内应用进程完全停止,如果容器在 gracePeriod 执行时间内还未完全停止,就发送 SIGKILL 信号强制杀死应用进程(容器运行时处理)。
- 所有容器进程终止,清理 Pod 资源。
网络层面
- Pod 被删除,状态置为 Terminating。
- Endpoint Controller 将该 Pod 的 ip 从 Endpoint 对象中删除。
- Kube-proxy 根据 Endpoint 对象的改变更新 iptables/ipvs 规则,不再将流量路由到被删除的 Pod。
- 如果还有其他 Gateway 依赖 Endpoint 资源变化的,也会改变自己的配置(比如 Nginx Ingress Controller)。
具体关闭过程如下图所示:
通过上图可以得知,我们可以通过设置PreStop来sleep一定的时间,让网络层面有时间去修改路由规则,这样在pod关闭的时候,不会有请求进入,以避免出现问题,这也是优雅下线的主要设置。
因为TerminationGracePeriodSeconds是整个Pod的优雅下线等待时间,默认为30s,现在增加了PreStop执行时间后,TerminationGracePeriodSeconds时间也要进行相应的增加。
3.1.2 容器配置修改
修改deployment配置,进行以下操作:
增加PreStop Hook,在pod停止前sleep 30s,给k8s预留pod删除后修改endpoint中端点和iptables时间,使请求不会路由到正在关闭中的Pod上。
增加TerminationGracePeriodSeconds到60s,因为有30s分给了PreStop Hook,所以将TerminationGracePeriodSeconds从30s改到60s。
3.2 java应用内配置优雅下线
Spring Boot中启用优雅下线可以在配置中配置以下内容:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
通过使用上述配置,Spring Boot 保证在收到 SIGTERM 后不再接受新请求,并在超时内完成所有正在进行的请求的处理。即使无法及时完成,也会记录相关信息,然后强制退出。对于 timeout 的值,应参考处理请求的最大允许持续时间。
这里可以看到,主要还是说正在进行的请求要预留时间进行处理,对于不接受新请求,如果前面路由没有正确修改,还是会有请求进来,只是被拒绝连接,这个请求一样没得到正确处理。
3.3 SLB长连接问题
经过前两项修改以后,进行了尝试,本来以为会没有500错误了,结果还是有错误出现,这里我以为是PreStop里sleep时间不够,于是就改大该时间到150s,发现还是不行,排查到这里有点没思路了,因为按前面的原理分析,这个优雅下线应该是没问题的了。这时候突然想到之前看过k8s的书,里面有关于Pod优雅关闭的描述,里面有提到Pod下线后,网络路由的修改并不会影响已有连接,也就是说虽然上面的设置会让新的连接指向新的Pod,但已有连接并不会改变,于是猜测是这个原因导致的,但怎么解决呢,当时并没有太多的思路。
后面同事查到SLB也可以进行优雅下线设置,进行设置后解决了问题,这里也印证了之前的想法是对的,就是已有连接导致的请求失败,只是当时不知道已有连接怎么设置断开,SLB设置优雅下线的方式为增加如下注解。
这个配置代表开启slb优雅下线,并且排空超时时间为30s,连接排空超时指定了在负载均衡器停止接收新连接之前,它将保持现有连接的时间。这个时间段内,负载均衡器会依旧允许现有的会话继续活动,从而为正在处理的请求争取时间,使其能够完成。
可以理解为负载均衡上已有连接在Pod已经是Terminating状态的情况下,会允许长连接再保持一段时间,用来处理已有请求,和应用内优雅下线处理类似。
4 总结
应用未优雅下线这个问题,前期根据网上信息进行设置,还是比较容易理解和配置的,但配置完了之后发现并不生效,这个时候由于缺少中间链路的日志信息,并不太清楚请求是落到哪个Pod导致的超时,所以导致排查一度比较困难和没有思路。当时虽然猜到了已有长连接可能存在影响,但不是特别确定,并且对怎么停掉之前的长连接不是特别有思路,要不然可以更快的解决问题,后续遇到这种还是应该沿着自己认为的方向接着去探索。