公司最近推出了一款一体机产品,因而老板就每天提个小箱子跑客户作POC,倍儿有范儿。跑了一阵子客户反响(问题)不错(很多),其中最大的问题就是开机进系统太慢,按照老板的说法:java
我按下开机键已经准备天花乱坠了,愣是给我一个系统维护界面5分钟才能进去。只好跟他们解释说咱们是工业一体机比较严谨,开机前要作好充分的自检。spring
其实开机慢咱们是有预期的,咱们的应用是云端微服务应用,为了快速响应公司号召稍加改造就变成了边缘应用。在资源配置各方面大幅缩水的状况下,不慢都对不起咱们30w行的代码量。 数组
言归正传,万里长征第一步:重现。经过秒表屡次测量的结果显示:安全
开机进入维护界面须要30s,session
进入登陆页须要2分30s,maven
登陆进系统须要近4分钟。ide
虽然没有反馈的那么夸张,这个速度也确实有点慢了。函数
基于上面的测量结果,咱们能够获得以下分布图:spring-boot
系统启动时间占比较小,并且在操做系统级别的优化比较复杂收益不高,咱们将优化的重心放在应用启动和系统登陆两个部分。微服务
u 应用启动
咱们经过查看应用日志能够发现应用的实际启动时间为109s,这个时间和咱们以前实测的时间也比较吻合,能够做为咱们应用优化的基线。
u 系统登陆
系统登陆时间的消耗看起来不太合理,这是由于咱们的应用使用了Eureka做为微服务的注册发现组件,致使了在应用启动后要经历屡次心跳验证才能真正可用。这部分的优化策略是在一体机中去掉Eureka,RestTemplate直接访问本机Restful服务便可。
通过以上初步分析,咱们明确了咱们优化的对象就是109s的应用启动时间。
在开始以前咱们先说一下咱们面临的应用规模,30w业务代码行,800+ spring管理的对象,Jar包大小70M左右。
经过查阅资料能够找到一些先行者,虽然案例大可能是很简单的应用,好比说只有一个依赖的状况优化到1s以内,可是原理上仍是相通的。
咱们能够经过@Lazy指定单个bean的延迟初始化,或者经过@ComponentScan指定lazyInit=true,也能够实现一个LazyInitBeanFactoryPostProcessor类来灵活的指定。
在实际过程当中咱们发现不是全部的类都能设置为lazyInit的,好比消息队列的监听类若是一开始不进行实例化那么就永远不会被实例化,这会致使消息永远都不会被消费;还有定时任务类,一样不适合设置成lazyInit。
最终咱们采用LazyInitBeanFactoryPostProcessor的方式实现了两个数组进行灵活定制:
private final String[] COMMON_INIT_LIST= { "springContextUtil", "custJobFactory" }; private final String[] CUST_INIT_LIST= { "userMsgReceiver", "dgnsOperateReceiver", "equipCondCalcReceiver", "modelAnalysisReceiver", "modelAnalysisScheduler" }; @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { for (String beanName : beanFactory.getBeanDefinitionNames()) { if(!needInitBean(beanName)) { BeanDefinition definition = beanFactory.getBeanDefinition(beanName); definition.setLazyInit(true); } } } private boolean needInitBean(String beanName) { return ArrayUtils.contains(COMMON_INIT_LIST, beanName) || ArrayUtils.contains(CUST_INIT_LIST, beanName); }
经过延迟初始化,应用启动时间从109s提高到48s,效果很是明显。
这里主要涉及的启动参数设置是下面两个
1, -XX:TieredStopAtLevel=1
使用C1编译器,又称为客户端模式,相对于C2也就是服务端模式,C1编译生成的机器码更加关注快速启动可是因为机器码没有通过编译优化因此不适合在线上环境稳定运行。
2, -Xverify:none/ -noverify
经过去除字节码的验证来提高JVM启动速度,一样不适合线上对安全有要求的环境使用。
咱们平时开发的时候可能注意到在IDE如Eclipse中启动一个SpringBoot应用的时候有一个选项叫Fast-startup,如图:
咱们通常都是默认勾选的,却不知这个选项对应的参数就是以上两个JVM参数。
这两个参数的设置能够大大提高咱们本地启动的速度,而本地启动不存在稳定性和安全性的问题,因此适用这两个参数。
实际案例中咱们经过这两个参数的设置,能够将启动时间提高到40s。
经过引入Maven依赖spring-context-indexer在编译阶段来为组件生成索引加快类扫描速度。
具体作法分为两步
1, 添加Maven依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-indexer</artifactId> <optional>true</optional> </dependency>
2, 配置Maven Plugin
<plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>1.4.2.RELEASE</version><!--$NO-MVN-MAN-VER$--> <configuration> <executable>true</executable> <annotationProcessorPaths> <path> <groupId>org.springframework</groupId> <artifactId>spring-context-indexer</artifactId> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins>
经过Maven install命令运行后在生成的jar包的META-INF目录下面会生成spring.components文件,内容以下:
若是你的项目是多模块项目,那么在每一个模块的jar下面都会生成一个索引文件。
经过这步优化,启动时间能够提高到38s,效果不算明显。
这和咱们项目自己的规模还有路径扫描的速度有关,若是项目自己类很少或者路径扫描自己很快,那建索引就没有多大意义了,目前看来2s的提高聊胜于无吧。
在通用优化建议的基础上,咱们还根据本身的经验和尝试,进行了进一步的优化。
此次的延迟初始化是从代码层面来进行。经过第一步的延迟初始化处理,咱们筛选出一些须要提早初始化的类。而这些类的初始化因为存在类依赖等因素又会牵扯出一大串的初始化,致使咱们在少许类的初始化上花费了较多的时间。
举个例子,咱们有个消息消费类经过@Autowired强依赖了5个service,那么在这个Receiver类初始化的时候这5个service也会被触发初始化,service类中又经过@Autowired引入了其余类的初始化,层层传递致使一个类的初始化实际触发了几十个类的初始化,已经破坏了咱们延迟初始化的设定,如图:
针对这种状况能够在@Autowired字段上加上@Lazy注解,可是容器在注册属性的时候会提示一个warning:AnnotationUtils - Failed to meta-introspect annotation。虽然不影响后续初始化,可是看着仍是很糟心的。
因此我选择的方式是干脆把这几个须要提早初始化的类里面的@Autowired字段所有移除,使用的时候到ApplicationContext获取。
@Autowired private EquipCondService condService; //替换为使用时获取,作到真正的延迟实例化 EquipCondService condService = SpringContextUtil.getBean(EquipCondService.class);
经过代码改造以后的延迟初始化升级,启动时间提高到29s,效果还不错。
Shiro的问题是经过查看Spring debug日志中的跳变发现的。
在正常的日志中通常两个日志的间隔也就几十毫秒,而在shiro的初始化过程当中咱们发现了一段3s的间隔,那必定是发生了什么不可告人的事情。经过二分查找的Debug终于发现了问题所在。
在shiroConfig中须要定一个securityManager,咱们使用了Apache包里自带的DefaultWebSecurityManager。如下是DefaultWebSecurityManager类的构造函数:
public DefaultWebSecurityManager() { super(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator()); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); } public AbstractRememberMeManager() { this.serializer = new DefaultSerializer<PrincipalCollection>(); AesCipherService cipherService = new AesCipherService(); this.cipherService = cipherService; setCipherKey(cipherService.generateNewKey().getEncoded()); }
咱们发如今SecurityManager初始化的时候会初始化依赖的CookieRememberMeManager,最终调用到抽象类的构造函数。在这里有一句话最终形成了3s的启动延时:
cipherService.generateNewKey()
这是生成对称加解密密钥的方法,经过单元测试发现这句话单独执行时间也是在3s左右,验证了咱们的结论。
解决方法简单粗暴,使用自定义的WebSecurityManager,去掉setRememberMeManager的调用便可:
通过这一步优化后,启动时间优化到26s,恰好是3s的提高。
在一体机开机速度提高的需求驱动下,咱们首先甄别出须要解决的关键问题就是应用启动时间。咱们经过借鉴先行者的成功经验,成功的将应用启动时间从109s提高到38s。而后从日志分析入手,找出日志中的跳变点,解决了@Autowired引起的伪延迟问题和Shiro生成密钥的时间损耗。最终咱们成功的将启动时间控制到了30s以内(26s),而相应的一体机从开机到老板开始天花乱坠也就只须要1分半钟,喝口水就掩饰过去了。
下面罗列一下咱们的优化路径,供后续参考借鉴。
最后有几点须要重申一下:
1, 延迟初始化能够加快应用的启动速度,可是不少在初始化时暴露的问题如内存不足就不能提早发现,因此若是必定要用须要通过慎重严格的测试。
2, JVM的两个优化参数都是适用于客户端模式的,针对线上系统若是更加关注运行时的效率和稳定性,则不建议采用该项优化。
3, 关于Shiro的优化是在明确不须要RememberMe功能或者本身实现RememberMe的前提下使用,并且生成密钥的方法在不一样的内存型号下表现差别很大,咱们全部的优化数据都是在DDR3下进行,若是在DDR4下运行这个方法只要600ms,如此也就没有必要特别优化了。