目录
背景
在Kubernetes部署应用时,尽管Kubernetes使用滚动升级的方式,先启动一个新Pod,等新Pod成功运行后再删除旧Pod,但在此过程中,Pod仍然会接收请求。如果在Pod被删除的瞬间正好有请求到达,Pod被杀死后将无法处理这些请求,导致客户端出现500错误,这样就无法实现平滑升级。
我们使用Java作为编程语言,注册中心采用Eureka。应用启动时会注册到Eureka,然后通过内部调用进行服务间通信。服务会定期向Eureka发送心跳信号,并在每个Pod服务中维护一份本地缓存映射表,缓存的更新窗口时间为30秒。当服务停止时,它会在Eureka中进行下线操作。然而,如果在服务下线的窗口期内有请求发起,可能会出现以下问题:
- K8s Pod服务已下线,但Eureka在窗口期内未更新注册列表,导致请求被路由到已不存在的旧服务,返回404错误。
- 请求已到达旧服务,但在高峰期时,由于处理速度较慢,服务还未能返回响应体,便已被关闭,导致返回500错误。如下所示
解决办法
使用的解决方案是,在应用下线的第一时间(Pod被删除)先进行在Eureka的下线操作不让其接受新的请求,然后等待Pod已接收的请求处理完成后 再进行删除Pod;
这里会使用到的知识以及需要自身考虑的点有:
- Pod容器终止流程
- k8s的prestop钩子(容器关闭前执行操作)
- 需要判断自己应用处理的请求的时间(基本上30s内都能处理完成 如果不放心的话调整成50s 但是这样的话也会相应的增加上线时长,需要注意)
Pod容器终止流程
1)新Pod启动,通过Readiness就绪性探测,加入service的endpoint服务列表。
2)老pod进入Termination状态,从service的endpoint服务列表摘除,此时不会有新请求打到即将终止的老pod上。
3)如果设置了Prestop钩子,则优先执行Prestop里的优雅动作。如果在规定的terminationGracePeriodSeconds优雅时间内(默认30s)完成不了,则kubelet会发送SIGTERM终止信号,并等待2秒,如果2秒后还未终止pod容器,则发送SIGKILL信号强制终止。
4)如果没有设置Prestop钩子,则发送SIGTERM终止信号优雅关闭容器进程,如果在规定的terminationGracePeriodSeconds优雅时间内(默认30s)未能终止pod容器,则发送SIGKILL信号强制终止。
需要注意:
1)SIGTERM终止信号只能被那些pid为1的父进程捕捉到,并优雅关闭容器进程。对于那些pid不为1的子进程是捕捉不到SIGTERM终止信号的。
所以对于单个容器只有一个pid为1的进程来说,使用K8S默认的优雅机制就可以,只需要拉长terminationGracePeriodSeconds优雅时间,确保在规定时间内完成容器优雅终止。
2)对于那些单个容器里有多个进程,即除了pid为1的进程外,还有子进程。这种情况下就需要设置Prestop钩子函数,在prestop里提前优雅处理掉那些子进程,然后再通过SIGTERM正常终止掉pod容器。
注意设置好terminationGracePeriodSeconds优雅时间。
模拟请求报错
模拟请求报错就是不加任何配置直接使用测试脚本(这里用的是jmeter)不间断的去调用我们的应用,然后发布我们的新应用会有失败的请求
发布服务
此时我们修改镜像apply可以模拟下我们的服务发布,当新Pod启动后 删除旧Pod时注意观察请求是否有报错!
[root@master01 mdl]# kubectl apply -f mdl-deployment.yaml
请求接口
设置50个线程开始去请求我们服务的接口
可以发现在删除pod时的这个动作会出现错误请求,随后就会正常
以上就是发版(新Pod替换旧Pod)的过程中会出现的问题
基于Eureka优雅上下线
正确的做法
1)拉长terminationGracePeriodSeconds的优雅时间。
2)设置Prestop钩子,在Pod容器终止之前,在Prestop里通过eureka提供的API接口,主动摘除eureka注册。接着sleep 30秒时间,用于刷新eureka网关缓存,摘除下线的pod地址。
3)最后再执行pod容器的优雅终止。
修改deployment配置
需要添加k8s的prestop钩子,以及设置强制关闭pod的时间要比sleep的时间长
terminationGracePeriodSeconds: 45
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", 'curl -X PUT "http://eureka-service:8761/eureka/apps/mdl-web/${POD_IP}:mdl-web:8080/status?value=DOWN" -H "Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8" && sleep 30']
发布服务
此时我们再次修改镜像apply可以模拟下我们的服务发布
[root@master01 mdl]# kubectl apply -f mdl-deployment.yaml
如上当开始停止旧pod时, 会先调用我们配置的prestop钩子 如下 先把eureka中旧pod下线 不让其接受请求,然后再处理已接受的请求最后彻底关闭pod
在此过程中,如果我们使用JMeter进行持续循环访问接口,所有请求都会正常响应啦。