组件和框架初始化顺序背后隐藏的线上故障

原创 吴就业 142 0 2023-10-26

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/541449236e0b448a94d210a565d37ba1

作者:吴就业
链接:https://wujiuye.com/article/541449236e0b448a94d210a565d37ba1
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

一个微服务可能引入非常多的SDK,例如消息中间件kafka的组件、RPC框架dubbo、定时任务调度平台xxl-job的组件,以及提供web服务的jetty/tomcat等。

你有注意过这些组件或框架的启动顺序吗?

说起Java的微服务开发,肯定离不开spring boot。而我们所使用的组件,都会通过spring boot提供的AutoConfiguration机制完成自动初始化。或者是我们自己封装的通过AutoConfiguration机制实现的初始化。

spring boot虽然提供Ordered机制实现排序,但如果都是开源的组件,背后不是同一个组件,那么大概率就是默认的要么最高、要么最低排序值。实际就是没有顺序。

当某个组件或框架初始化故障会发生什么?

假如jetty/tomcat先启动,就会开始接收用户流量,如果接着初始化rpc框架失败了,那么这些已经接收的用户流量就无法被正常处理。

假如rpc提供者先初始化注册到了zk,就会开始接收rpc消费者过来的流量,但是提供者处理逻辑需要发kafka消息,但是kafka如果初始化失败了,那么这批流量就会有问题。

假如xxl先初始化注册了,Job调度到这个节点执行,而执行逻辑依赖rpc和kafka,如果rpc或者kafka初始化失败,那么这次调度就会失败。

因此,如果发生组件初始化失败,就会出现流量处理异常,即服务不平滑上线,最终触发告警。

逆方向也是一样的, Shutdown如果没有顺序的话,也可能会出现服务不平滑下线。

例如,kafka组件先一步处理Shutdown信号,那么如果此时rpc接收到流量,需要依赖kafka处理逻辑,就会出现问题。

怎么定这些组件或框架的初始化顺序?

一般kafka的消费者和提供者是分开初始化的、rpc的消费者和提供者也是分开初始化的。

那么有没有一个顺序,能够避免上面所有假设的事件出现。

kafka生产者->rpc消费者->xxl->kafka订阅者->rpc提供者->jetty/tomcat。

其中“kafka生产者->rpc消费者”这一段,kafka生产者和rpc消费者的顺序不分先后。

“xxl->kafka订阅者->rpc提供者->jetty/tomcat”,这一段,顺序也不分先后。

但是“kafka生产者->rpc消费者”必须先于“xxl->kafka订阅者->rpc提供者->jetty/tomcat”。

因为xxl、kafka订阅者、rpc提供者、jetty/tomcat都是流量的入口。而kafka生产者、rpc消费者是流量的出口。

因此,只要出口先于入口初始化完成,基本就没什么问题。对于Shutdown,只要入口先于出口关闭,也不会有问题。

怎么指定顺序,指定顺序就能解决问题吗?

实际上,以上的假设成立的前提条件是,rpc消费者代理实例和kafka生产者都是用到的时候再创建的,或者说创建的时候内部初始化逻辑出错没有给外部抛异常,比如懒初始化,异步建连等。kafka提供者和zookeeper组件就是这样的逻辑。

如果初始化是同步的,并且发生错误会跑出异常,那么假设就不会成立。因为在spring boot框架之上开发,基本都是自动注入bean,而xxl的job也好,rpc提供者也好,kafka的消费者也好,都是通过依赖注入注入一个rpc消费者代理实例或者kafka生产者实例,而springboot就已经保证了在注入之前肯定会先创建依赖的bean。

但是,kafkaproducter的内部初始化,与kafka服务端创建连接是不在主线程上的,假如kafka服务端挂掉了,或者网络不通,这时候是不会抛异常的,只有在发消息的时候,才能得知异常。又或者topic忘记创建。我们没办法改变,也无法通过指定组件初始化顺序解决这个问题。zookeeper的话,我们可以通过阻塞等待创建连接完成,但kafkaproducter无法做到。

另外,Shutdown的逻辑也是无顺序的,如果都是各自注册ShutdownHook。但如果是需要显示调用close,我们就可以自己完成所有组件和框架的Shutdown和编排顺序。

#中间件

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Java内存GC故障问题排查实战-推送系统频繁GC告警问题排查

记录一次工作中实战的Java内存泄漏问题排查,Pod重启后无法查看现场,但通过gc日记可以确认存在内存泄露,并通过运行一段时间发现有个Java类的实例数量非常高。

cpu负载高故障排查实战-网关故障导致业务请求堆积

因为go标准库实现tls握手性能比较差,在一台8核的机器,只能到达2000这个量级,所以当到达某个临界点的时候,握手占用CPU过高,反过头来影响正常的业务请求,导致了业务请求处理变得十分慢。

Go写的文件上传中间件内存泄露问题排查

用go开发的一个文件上传中间件,由于依赖了ceph这个c库,早期通过pprof排查,怀疑内存泄露在c层,而项目依赖ceph,于是就怀疑是ceph的问题。但通过使用jemalloc排查后,并未发现ceph有什么异常。最后使用最笨的方法,定位到github.com/chai2010/webp库存在内存泄露bug。

S3文件上传403问题排查

业务反馈有个iOS设备上传出现403问题,打点日记只能看到403,没有详细的错误信息。开了s3的日记后,也只是看到403 AccessDenied。

Go调用Lua性能压测与调优

基于go提供的基准测试能力编写并发测试用例,为排除脚本本身的性能影响,脚本只实现简单的逻辑,并实现预编译。通过调整虚拟机池策略、cpu数、并行度等,输出调用lua脚本的平均耗时、占用的内存。

被开源组件坑惨了,文件上传到MFS后MD5不一致

moosefs没有关于底层自定义二进制通信协议的文档,且各大版本api上差异很大。我们在github上找到一个go语言实现api操作moosefs的开源库,但不幸的是,这个组件实现的api版本很低,在测试阶段,我们发现上传文件到moosefs后,moosefs上存储的文件的md5与本地原文件md5不一致,如果是图片,能很明显的看出少了一块像素。