springboot + nacos + k8s 优雅停机
优雅停机是什么?网上说的优雅下线、无损下线,都是一个意思。上面的步骤,其实还缺了不少基础的内容,比如,停止请求外,还要停止接收定时任务、停止接收mq消息,等待他们的完成,这2项都是我们微服务中必不可缺的能力。因此,我希望通过本文,能够更清晰,更详细的讲解,在我已知的真实业务场景下,如何做优雅停机。文中,很多内容不会讲得太详细,需要大家有一定的搜索能力或者经验!Java 技术栈中间件优雅停机方案设计
优雅停机是什么? 网上说的优雅下线、无损下线,都是一个意思。
优雅停机,通常是指在设备、系统或应用程序中止运作前,先执行一定的流程或动作,以确保数据的安全、预防错误并保证系统的整体稳定。
一般来说,优雅停机可以参考以下步骤以实现:
1. **备份数据**:立即将内存中的所有未保存的修改、缓存等数据保存到数据库或磁盘中。
2. **停止接收新的请求**
3. **处理未完成的请求**
5. **通知其他依赖组件**
6. **等待所有要素安全退出后,关闭系统**
在具体实施时,不同的设备、不同的系统、不同的应用,所需要的优雅停机步骤也不尽相同,甚至需要根据不同的场景来选择不同的方法。
例如,在某些情况下,你可能需要让用户知道,系统即将关闭,并告诉他们应当保存所有的工作并退出系统;而在另一些情况下,你可能需要设计一种策略,能够让系统在无用户介入的情况下,自动保存所有的状态,并在下次启动时恢复之。
但是,无论在哪种情况下,优雅停机的目标都是保护数据,避免错误,并尽量减少到访用户或使用者的不便。
上面的步骤,其实还缺了不少基础的内容,比如,停止请求外,还要停止接收定时任务、停止接收mq消息,等待他们的完成,这2项都是我们微服务中必不可缺的能力。
因此,我希望通过本文,能够更清晰,更详细的讲解,在我已知的真实业务场景下,如何做优雅停机。
文中,很多内容不会讲得太详细,需要大家有一定的搜索能力或者经验!
-
Java 技术栈中间件优雅停机方案设计与实现全景图[1]
-
Spring——项目优雅停机[2]
-
Graceful shutdown and zero downtime deployments in Kubernetes (learnk8s.io)[3]
-
Graceful Shutdown Services in Kubernetes | Thoughtworks[4]
-
Nacos-服务发现[5]
-
k8s容器生命周期回调[6]
其他就不一一列举了
随着微服务的兴起,运维方式由docker -> k8s 变化,优雅停机涉及到的点就越来越多! 下面,我们用一个案例,说明优雅停机中的问题和问题解决方案。
案例前:k8s 停机流程
当程序员执行 kubectl delete pod 命令时,两个过程开始:
网络规则即将生效:
-
Kube-apiserver 收到 pod 删除请求,并将 pod 的状态更新为 Extinating at Etcd;
-
终结点控制器从终结点对象中删除 Pod 的 IP;
-
Kuber-proxy 根据 Endpoint 对象的更改更新 iptables 的规则,并且不再将流量路由到已删除的 pod。
删除容器:
-
Kube-apiserver 收到 pod 删除请求,并将 pod 的状态更新为 Extinating at Etcd;
-
Kubelet 清理节点处的容器相关资源,如存储、网络;
-
添加 Prestop hook 钩子,等待流量不再发给pod;
-
Kubelet 将SIGTERM发送到容器;
-
如果容器在默认的 30 秒内没有退出,Kubelet 将发送 SIGKILL 并强制其退出。
k8s + springboot + nacos 案例
-
PreStopHook 做了2件事情:
a. nacos反注册
b. 休眠35秒 -
通过信号量关闭springboot程序;
其中,k8s的 terminationGracePeriodSeconds(宽限期)设置为35s。
问题
-
springBoot程序关闭时间只有2s, 那么该程序就无法处理完一些线程任务、异步消息、定时任务等。为什么呢?
宽限期设置了35s,PreStop休眠了35s + 一个请求的时间,超过了宽限期,那么 kubelet 就会给与 pod 增加一次性2s的宽限时间。Pod 的生命周期[7],2s不管程序是否正常结束,都会被Kill -9。 -
为什么反注册之后需要休眠35s?
这里涉及到nacos服务发现原理,nacos服务变更响应时间:实时;ribbon 默认缓存刷新时间30s;因此,一开始是设置30s的,发现还有feign请求失败的情况,所以设置成了35s以解决这个问题! -
nacos服务变更响应时间真的是实时吗?
其实并不一定,nacos服务发现是通过http和udp实现的,udp是实时的,http最大等待时间是10s,但是,udp端口生产环境可能没有开放!所以,案例中的nacos服务发现仅通过http定时轮询实现。
案例优化
上面的案例可以优化的点
-
nacos 反注册后休眠35s,是否可以减少;
-
terminationGracePeriodSeconds 设置多少合理?
优化点1
反注册后休眠的35s时候受到nacos服务发现 + ribbon 缓存刷新时间影响,正常应该是 服务发现时间 + 缓存刷新时间 40s才能在极端情况下保证服务停机时,不会再有feign 请求进入。 如果想要缩短这个时间
-
启用udp,这个需要和运维同学商量,否则10s等待少不了;
-
监听nacos服务变更通知,发现服务下线后,及时刷新ribbon缓存;
* 订阅 nacos 实例变更通知
* 手动刷新 ribbon 服务实例缓存
* nacos client 1.4.0+
*/
@Component
@Slf4j
public class NacosInstancesChangeEventListener extends Subscriber<InstancesChangeEvent> {
@Resource
private SpringClientFactory springClientFactory;
@PostConstruct
public void registerToNotifyCenter(){
NotifyCenter.registerSubscriber(this);
}
@Override
public void onEvent(InstancesChangeEvent event) {
String service = event.getServiceName();
String ribbonService = service.substring(service.indexOf("@@") + 2);
log.info("##### 接收到微服务nacos实例变更事件:{} ribbonServiceName: {}", event.getServiceName(), ribbonService);
ILoadBalancer loadBalancer = springClientFactory.getLoadBalancer(ribbonService);
if(loadBalancer != null){
((ZoneAwareLoadBalancer<?>) loadBalancer).updateListOfServers();
log.info("刷新 ribbon 服务实例:{} 缓存成功", ribbonService);
}
}
@Override
public Class<? extends com.alibaba.nacos.common.notify.Event> subscribeType() {
return InstancesChangeEvent.class;
}
}
优化点2
terminationGracePeriodSeconds 的值应该略大于 PreStop耗时 + springBoot 停机时间,springBoot 停机时间是由程序业务决定的(mq消息、定时任务、线程池任务、以及备份数据),网上的推荐做法是启用springBoot的优雅停机功能,并实现自定义的关闭逻辑。 springBoot优雅停机的默认缓冲时间是30s,因此,terminationGracePeriodSeconds的时间个人建议10 + 30s即可。
经过优化后
使用 actuator shutdown 方案
有些网贴推荐使用 actuator shutdown 进行优雅停机,那么看下其流程图:
其实,真正的情况并非如上图所示,因为调用shutdown后,springBoot就会进入优雅停机流程,但是这个流程没有结束,然后就会被kill -15 中断,如果线程池没有做好配置,线程池任务没有结束,服务就会关闭。
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
threadPoolTaskExecutor.setAwaitTerminationSeconds(30);
mq 和 定时任务
上面的方案中,提到nacos反注册时,其他服务监听反注册事件,进行ribbon缓存刷新,那么,反注册的服务(停机服务)自身,是否可以也监听该事件呢? 答案是可以的。
停机的服务监听nacos反注册事件,判断是自己反注册了,表示准备关机,那么就可以停止对mq消息的监听,停止定时任务,这样就比在优雅停机时,进行mq 和 定时任务的停止更完美。
流量控制
如果没有使用k8s进行pod节点的流量控制,那么大概率会使用 springCloud gateway作为服务网关,因此,gateway 服务也应该监听nacos的反注册事件,从而及时刷新ribbon的缓存,关闭停机服务的流量。
经过大量的资料参考、学习,最终得到的一份自己认为合格的优雅停机方案,里面可能有较多的不专业表述,敬请谅解和指正,谢谢。
在本文的最后,还要说下,优雅停机最大的挑战并不是来源于这个优雅停机流程,机械化的流程前人都帮忙躺过了,剩下的是业务服务自身的逻辑:
-
有没有包含超过30s的业务逻辑,如执行超过30s的请求,定时任务、线程池任务或mq消息;
-
服务关闭时,如何保存未完成的任务、数据,实现自定义的关闭逻辑;
-
接口逻辑是否做了幂等;
参考资料
[1]
https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&mid=2247485290&idx=1&sn=58d7eb2c25c2e51b41e35a4ce4dab517&chksm=ce77c12df900483bf5a0edc6b31e710d6e3019deeeb788ed89faa69f9055872e85d518ee52f6#rd: https://link.juejin.cn/?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzg2MzU3Mjc3Ng%3D%3D%26mid%3D2247485290%26idx%3D1%26sn%3D58d7eb2c25c2e51b41e35a4ce4dab517%26chksm%3Dce77c12df900483bf5a0edc6b31e710d6e3019deeeb788ed89faa69f9055872e85d518ee52f6%23rd
[2]
https://www.cnblogs.com/caoweixiong/p/15650333.html: https://link.juejin.cn/?target=https%3A%2F%2Fwww.cnblogs.com%2Fcaoweixiong%2Fp%2F15650333.html
[3]
https://learnk8s.io/graceful-shutdown: https://link.juejin.cn/?target=https%3A%2F%2Flearnk8s.io%2Fgraceful-shutdown
[4]
https://www.thoughtworks.com/insights/blog/cloud/shutdown-services-kubernetes: https://link.juejin.cn/?target=https%3A%2F%2Fwww.thoughtworks.com%2Finsights%2Fblog%2Fcloud%2Fshutdown-services-kubernetes
[5]
https://zhuanlan.zhihu.com/p/484990652: https://link.juejin.cn/?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F484990652
[6]
https://kubernetes.io/zh-cn/docs/concepts/containers/container-lifecycle-hooks/: https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Fconcepts%2Fcontainers%2Fcontainer-lifecycle-hooks%2F
[7]
https://kubernetes.io/zh-cn/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination: https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Fconcepts%2Fworkloads%2Fpods%2Fpod-lifecycle%2F%23pod-termination
更多推荐
所有评论(0)