记一次自定义starter引发的线上事故复盘
创始人
2024-02-08 05:43:58

前言

本文素材来源于业务部门技术负责人某次线上事故复盘分享。故事的背景是这样,该业务部门招了一个技术挺不错的小伙子小张,由于小张技术能力在该部门比较突出,在入职不久后,他便成为这个部门某个项目组的team leader,同时也拥有review 该项目的权利。(注: 该项目为微服务项目),在某次小张review项目的时候,他发现好几个项目,发现代码有很多重复,于是他就动了把这些重复代码封装成starter的念头,然后也是因为这次的封装,带来一次线上事故。下面就以代码示例的形式,模拟这次事故

代码示例

注: 本文仅模拟出现事故的代码片段,不涉及业务

1、模拟小张的封装的starter

@Slf4j
public class HelloSevice {private ThreadPoolTaskExecutor threadPoolTaskExecutor;public HelloSevice(ThreadPoolTaskExecutor threadPoolTaskExecutor){this.threadPoolTaskExecutor = threadPoolTaskExecutor;}public String sayHello(String username){threadPoolTaskExecutor.execute(()->{log.info("hello: {} ",username);});return " hello : " + username;}
}
@Configuration
public class HelloServiceAutoConfiguration {@Beanpublic ThreadPoolTaskExecutor threadPoolTaskExecutor(){ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();threadPoolTaskExecutor.setCorePoolSize(2);threadPoolTaskExecutor.setMaxPoolSize(4);threadPoolTaskExecutor.setQueueCapacity(1);threadPoolTaskExecutor.setThreadFactory(new ThreadFactory() {private AtomicInteger atomicInteger = new AtomicInteger();@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("hello-pool-" + atomicInteger.getAndIncrement());return thread;}});return threadPoolTaskExecutor;}@Beanpublic HelloSevice helloSevice(ThreadPoolTaskExecutor threadPoolTaskExecutor){return new HelloSevice(threadPoolTaskExecutor);}}

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.lybgeek.thirdparty.autoconfigure.HelloServiceAutoConfiguration

2、模拟有引用小张封装的starter的微服务项目

因为这些微服务中有一些耗时的任务,因此使用了spring的异步。示例如下

@Configuration
public class ThreadPoolConfig {@Beanpublic ThreadPoolTaskExecutor threadPoolTaskExecutor(){ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();threadPoolTaskExecutor.setCorePoolSize(2);threadPoolTaskExecutor.setMaxPoolSize(5);threadPoolTaskExecutor.setQueueCapacity(10);threadPoolTaskExecutor.setThreadFactory(new ThreadFactory() {private AtomicInteger atomicInteger = new AtomicInteger();@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("echo-pool-" + atomicInteger.getAndIncrement());return thread;}});threadPoolTaskExecutor.setRejectedExecutionHandler((r, executor) -> System.err.println("记录日志。。。。"));return threadPoolTaskExecutor;}
}
@Service
@Slf4j
public class EchoService {@Async("threadPoolTaskExecutor")public void echo(String content){log.info("echo -> {} ",content);try {//模拟耗时操作TimeUnit.MINUTES.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}}
}

3、和本文有关系的配置内容

spring:main:allow-bean-definition-overriding: true

4、模拟调用耗时业务代码块示例

@Component
public class BeanCommandRunner implements CommandLineRunner {@Autowiredprivate EchoService echoService;@Overridepublic void run(String... args) throws Exception {for (int i = 0; i < 6; i++) {echoService.echo("content:" + i);}}
}

相关的代码如上述内容

大家可以思考一下上面的示例有没有什么问题

我们启动一下程序,观察一下控制台
报了一个线程池拒绝异常,而且通过这个异常信息,我们发现这个线程池走是小张封装线程池,而非业务自己定义的线程池。这明显是不正常的,正常的逻辑是业务代码优先级需比公共代码优先高才合理

那如何解决呢?

仅需利用springboot的条件注解即可,在小张封装的starter下做如下改动

@Bean@ConditionalOnMissingBeanpublic ThreadPoolTaskExecutor threadPoolTaskExecutor(){ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();threadPoolTaskExecutor.setCorePoolSize(2);threadPoolTaskExecutor.setMaxPoolSize(4);threadPoolTaskExecutor.setQueueCapacity(1);threadPoolTaskExecutor.setThreadFactory(new ThreadFactory() {private AtomicInteger atomicInteger = new AtomicInteger();@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("hello-pool-" + atomicInteger.getAndIncrement());return thread;}});return threadPoolTaskExecutor;}

修改后,我们在启动一下程序,观察控制台


此时走就是业务自定义的线程池了

为什么加了一个 @ConditionalOnMissingBean就可以了

这就得从springboot的自动装配说起了,springboot的自动装配类继承了org.springframework.context.annotation.DeferredImportSelector,这个接口具有懒加载的功能,当项目启动后,先加载业务自定义的bean,再来加载starter的bean,当我们项目中没有配置

spring:main:allow-bean-definition-overriding: true

时,项目启动就会直接报类似如下异常

当时他们业务项目因为他们feign没有指定contextId,导致报了上述的异常,业务开发为了省事就直接把
allow-bean-definition-overriding设置成true,这也为后续小张自定义的starter引发的事故埋下了很好的根基。那我们再切回主线,当spring发现有两个一样的bean,且发现allow-bean-definition-overriding为true,后面加载的bean会把前面加载的bean覆盖掉,这也是为啥小张starter的bean会生效。当我们在starter上的bean上加载 @ConditionalOnMissingBean后,因为业务项目的bean已经存在了,starter的bean就不会加载进spring容器了。

我们从技术维度说明了解决方案,我们再从非技术的角度上复盘一下这次事故

复盘

不知道会不会有朋友说,你说那么多,不就加一个@ConditionalOnMissingBean就能解决这个问题,下次注意就好了啊。但据业务技术人反馈当时他们排查了挺久,因为他们业务项目平时没啥并发量,所以小张那个问题就被掩盖住了,而有次他们业务搞了一个营销活动,因为并发上去了,才把问题暴露出来。这侧面也说明项目压测的重要性,不能因为平时没啥并发,就掉以轻心

不懂大家的公司是否也有这样的情况,在我们这边,底下成员代码只能merge request,只有team leader review后,再将代码合并到主干,因为team leader拥有的权限比较大,他写的代码,只要他愿意,直接就能合并到主干了。这次也是因为小张直接将他写的代码推到主干发布,酿成事故。后面我们这边提出了一个方法,就是team leader的代码要由更高级的leader进行走查,但是这个方法我是感觉也不是很好,因为有不少项目组的team leader的老板基本上已经脱离一线,不敲代码了,也不懂能不能行。

其次因为小张入职不久,对业务其实没有完全吃透,因为看到重复的代码,出于技术洁癖,就想去改,出发点是好的,但有句话技术是为业务服务,业务都没搞懂,就去动,有时候会带来意想不到的风险

总结

对自己的不熟悉的项目或者开发公共组件,深思熟虑再动手是很重要的

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...