Spring Boot应用优化与升级

Spring Boot

前言

厂里有个21W行Java代码的项目,通过Gradle管理,子项目有104个,编译之后产生3个可启动的应用,初次编译时间要95秒,产生的关键应用的大小在90M左右。上传到生产服务器运行,启动完成需要137秒。整个过程比较漫长,一次Hotfix会耗掉Ops近10分钟时间。

上周三一同事回家试验升级Spring Boot 1.4.0和Gradle 3.0,说好像还不错,这个项目还在使用1.2.7版本,可以找时间的时候升级上去。

周四下午有了点时间,就着手开始动手做,三个目标,有优先级地开始进行:

  1. 加速应用的编译和启动速度
  2. 将Spring Boot升级到1.4.0版本
  3. 将Spring Boot相关依赖升级到合适的版本

加速应用的编译和启动速度

Spring Boot应用是一个fat jar,启动的时候会将fat jar中的jar(jar in jar)一个个扫描,将里面的jar扫描之后通过链接方式重新链入classpath中。应用越大,这个工作就越繁重,耗时越长。

将项目编译一次,得出来的war/jar文件使用BetterZip打开,将WEB-INF/lib文件夹展开,按照文件大小由大到小排序,这个文件夹中放置的全是依赖,可以从最大的依赖开始。

BetterZip open App

挪出静态文件

最大的依赖是项目中的一个子项目,21MB,内附两个字体,将字体从resources中挪出,放入外部的配置项中,子项目编译之后缩小到181KB。

移除不必要依赖

使用new method替换依赖

icu4j也很大,在整个项目中检索之后,多处使用了这个依赖的功能,但功能单一,只用了一个功能:将数字转换为中文,即将”123“转换为“壹佰贰拾叁”。这个可以新建一个类加个静态方法就能搞定。

必要时在最上层指定依赖

打包的时候发现两个版本不同的jcl-over-slf4j,有个很基础的子项目使用的spring-data-redis版本依赖的jcl-over-slf4j是1.7.13,而在外层指定1.7.12,而jcl-over-slf4j的1.7.12版本内置依赖slf4j-api版本要1.7.13,外层指定1.7.12…解决的办法就是直接删掉子项目的版本指定,在最外层指定版本。

使用更小的依赖替换掉较大的依赖

我个人很喜欢Fluent API,一路点点点写下去,在HttpComponents增加了Fluent API之后我非常开心,但是要额外引入fluent-hc包,这个包引用的HttpCore版本一直是不一致的,上层使用更新的包有时候出现一些Bug还查不到原因。之后做Android开发时发现的OkHttp是我目前最喜欢的HttpClient,Fluent API,设计出色,语义清晰,新特性如HTTP2、SNI都支持得很好,体积却只有fluent-hc的1/4。

限制@Configuration数量

Spring Boot在1.3.0版本增加了EnableAutoConfigurationImportSelector用来加速自动配置,但spring-boot-autoconfigure包仍然会在应用启动的时候自行创建很多默认的Bean,可仅添加在AutoConfigure下有用的Configuration,也可添加自定义的Configuration。之前项目中自定义了这部分,但加入了Metric相关的Configuration而并没有引入任何Metric工具,这个会稍微影响启动的速度,都删掉。

将Spring Boot升级到1.4.0版本

Spring Boot从项目刚启动时就大方光彩,带着MicroService概念款款而来(当然没忘记天国的Spring Roo),但初期使用有各种各样的Bug,当时还不支持MyBatis(或者说MyBatis不支持Spring Boot),这次升级又踩了一次MyBatis的坑。

Spring Boot 1.4.0基于Spring Framework 4.3.2版本,也就是支持Spring Framework 4.3.2的所有新特性,比较闪耀的点是支持OkHttp3、Jackson 2.8.x、Undertow 1.4。RestTemplate下可以使用OkHttp3的ClientFactory了,非常开心。Spring Boot这次升级也有大量的新特性,例如加入SpringBootTest一个注解搞定UnitTest,还有@ConfigurationProperties直接可以引入prefix,看同事处处写一堆Settings真是醉…还有提供小前戏补唇蜜让人闭嘴精咽的工具Caffeine,受Guava Cache启发再开新档的超快缓存,可以把EhCache踢掉了(项目又可以瘦身10MB)。

由于这次升级是跨1.3版本直接升级,一些在1.3版本时提示已经Deprecated的配置和方法都出了一点小问题,如GzipFilterAutoConfigurationManagementSecurityAutoConfiguration都在1.3.x版本过程中被废掉了,删掉了就好。这次正好碰到Jackson最低版本调整为2.6.x,其中的一些方法已经开始标记为Deprecated,项目中的一些BeanSerializerModifier因为使用了这些方法,在编译的时候报警,根据源码提示修改到最新方法就没问题。

大版本升级上去了,没问题,通过编译,着手开始下一步。

将Spring Boot相关依赖升级到合适的版本

Gradle 2.14.1 升级 3.0

由于Gradle并不如Maven依赖管理直观,需要不停gradle :project:dependencies > project-dependencies.txt来一个个看,比较累,好在这段时间编译速度已经很快,梳理了里面的关系,又排掉了很多无用的依赖,还是挺开心的。

Gradle 3.0的Release Notes中并没说编译速度比之前的版本提升了多少,只提到了在解析依赖的时候会快上5-10%,而反复测试下来,性能并没有提升多少。

升级到Gradle 3.0之后,还是踩坑了,无法编译Spring Boot 1.4以下的项目,编译其他项目就会爆这样的错误

A problem occurred evaluating project ':some-project'.
> Failed to apply plugin [class 'io.spring.gradle.dependencymanagement.DependencyManagementPlugin']
   > Could not create task of type 'DependencyManagementReportTask'.

由于Gradle 3.0改动了挺多DSL,Spring Boot Gradle Plugin 1.4之前的版本都无法指引Gradle 3.0中的Tasks,要使用Gradle 3.0,Spring Boot Gradle Plugin必须升级到1.4。

MyBatis 3.3.0 尝试升级 3.4.1, 后坚守 3.3.0

MyBatis也是一个很优秀的项目,由于上文所述原因,Spring Boot 1.0和1.1版本都被我暂时标记为评估状态而未在生产中投入使用,后期确认支持没问题之后,MyBatis又新开MyBatis Spring Boot项目,让我对这个轻量ORM工具产生极大的好感,以至于用SQLAlchemy无强类型约束的前提下,仍然觉得前者更好用。

在厂里内部一个叫Fatima项目中,MyBatis的版本已经线性升级至3.4.0。在3.3.1与3.4.0版本(两个版本同期)中,MyBatis支持了@Results这个注解的id复用,即在方法上标记@Results中定义的@Result,能被同一个类中的另一个方法在@Results中加上id标记,无需再次定义即可复用,可减少大量代码。

而在我升级的时候,顺手升级了Tomcat,造成分页工具MyBatis-Paginator不断抛错,我认为是版本不兼容,所以按下来没升级,毕竟MyBatis-Paginator这个工具已经一年多没人维护,之前在Fatima中投入使用的Mybatis-PageHelper被后来者又切回MyBatis-Paginator,真是…现在发现不是这些外部扩展的问题,想了一下还是按着不动没往上升级,@Results这个功能估计没人关心也没人用,有人真要用那么再说好了。

Tomcat 8.0.28 尝试升级 8.5.4,后回退至 8.0.36

Spring Boot 1.4.0默认采用的Tomcat版本是8.5.4,我顺手也升上去了,结果这是一个大坑,我踩了进去,过了两天才爬出来。

启动的时候不断的抛出各种报警:

2016-08-22 22:13:47,258 TimboHomeMac.local[localhost-startStop-1] WARN o.a.catalina.webresources.Cache - [] Unable to add the resource at [/WEB-INF/lib/spring-aop-4.3.2.RELEASE.jar] to the cache because there was insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache
2016-08-22 22:13:47,259 TimboHomeMac.local[localhost-startStop-1] WARN o.a.catalina.webresources.Cache - [] Unable to add the resource at [/WEB-INF/lib/ecc-25519-java-1.0.1.jar] to the cache because there was insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache
2016-08-22 22:13:47,259 TimboHomeMac.local[localhost-startStop-1] WARN o.a.catalina.webresources.Cache - [] Unable to add the resource at [/WEB-INF/lib/springfox-swagger2-2.2.2.jar] to the cache because there was insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache

启动完成之后还有:

2016-08-22 22:43:08,060 TimboHomeMac.local[ContainerBackgroundProcessor[StandardEngine[Tomcat].StandardHost[localhost].StandardContext[]]] INFO o.a.catalina.webresources.Cache - [] The background cache eviction process was unable to free [10] percent of the cache for Context [] - consider increasing the maximum size of the cache. After eviction approximately [9,600] KB of data remained in the cache.
2016-08-22 22:43:31,105 TimboHomeMac.local[ContainerBackgroundProcessor[StandardEngine[Tomcat-1].StandardHost[localhost].StandardContext[]]] INFO o.a.catalina.webresources.Cache - [] The background cache eviction process was unable to free [10] percent of the cache for Context [] - consider increasing the maximum size of the cache. After eviction approximately [9,724] KB of data remained in the cache.
2016-08-22 22:44:01,109 TimboHomeMac.local[ContainerBackgroundProcessor[StandardEngine[Tomcat-1].StandardHost[localhost].StandardContext[]]] INFO o.a.catalina.webresources.Cache - [] The background cache eviction process was unable to free [10] percent of the cache for Context [] - consider increasing the maximum size of the cache. After eviction approximately [9,554] KB of data remained in the cache.
2016-08-22 22:44:31,115 TimboHomeMac.local[ContainerBackgroundProcessor[StandardEngine[Tomcat-1].StandardHost[localhost].StandardContext[]]] INFO o.a.catalina.webresources.Cache - [] The background cache eviction process was unable to free [10] percent of the cache for Context [] - consider increasing the maximum size of the cache. After eviction approximately [9,414] KB of data remained in the cache.

反复的尝试使用EmbeddedServletContainerCustomizer定制Tomcat的启动参数,但没什么用,仍然一个劲的抛错,在讨论组中的讨论认为是Tomcat的Regression Bug造成,当前Tomcat 8.5.4版本实际上就是Tomcat 9.0 M6版本,而Apache Tomcat Versions中标记8.0.x版本是已废弃的版本,要解决这些问题,等到Tomcat发布8.5.5版本fix掉之后,Spring Boot 1.4.1版本跟进,才有可能解决这个问题。

醉了…还是根据指引退回来吧,退回版本8.0.36之后,启动报错,缺少tomcat-juli包,根据官方文档指引要额外加入这个包,但是我使用tomcat-embedded-logging-juli包也可以正常工作。

想用Undertow换掉Tomcat了…

应用大小与启动速度比较

在经过三天的调整和测试之后,开始进入评估报告。

以下评估都是在开发机器上面测试的,当然阿里云和生产上都测过,机器都没我开发机器强。

开发机器配置和环境:

CPU:i5 4590
内存:16G
系统:OSX 10.11.6
JDK:8u101
Gradle:3.0
Groovy:2.4.7

三个主要的应用,分别为A,J,M,都瘦了挺多,体积:

App Size

编译速度:

Compile Time

里面加入RECOMPILE这项是考虑到开发的时候可能会反复测试造成多次重复编译,Gradle会跳过未改动的已编译项目只编译改动的项目,在开发环节会大大提升效率。

应用启动速度:

Startup Time

启动完毕后Heap占用:

Heap Size

用Java Mission Control看的,测了老半天。

当然生产的机器和阿里云的机器都比我机器弱,找时间扔到生产上启动花了65s,也比之前137s的要快上不少。

后记

Modern Agile

今天看到提出的一个概念:现代敏捷

Modern Agile

里面四项原则:

  • 让人觉得很酷炫/
  • 保证安全
  • 快速地试验和学习
  • 持续传递价值

还可优化的点

这次优化,对整个流程都十分有帮助,Dev在开发和测试的时候能够节省近一半的时间,Ops在部署阶段也节省了大量的应用传输时间和应用启动时间。

有很多点还是可以优化的,比如

  • 使用Caffeine替换掉EhCache,使用其他PDF生成工具替代Flying Saucer,则可以让应用再减少20M,编译速度和应用启动速度会提升10-20%
  • 删除项目中Flyway依赖,DataSource初始化时SqlSessionFactory无需等待
  • 子项目中仍有很多不需要的依赖,如Spring Tx已经被Spring JDBC纳入依赖,而很多项目随手加个Spring Tx,还有SimpleCaptcha,在想是不是随手复制粘贴进来的
  • 启动时候的初始化应该还有很多地方可以延缓加载

这次优化还是挺快的,超过我的预期,原本以为要自己做两周左右的时间,结果三天搞定,还顺手改了一堆不顺眼的代码,比如用HttpClient写的一坨坨看起来~~很麻烦很累就可以成佛~~的代码换成OkHttp之后就非常好看,为了保证应用正常运行,需要继续做的就是在不同环境下测试了…

现在想想还真是侥幸,改了这么多东西我编译的时候还全部跳过单元测试,其他人还有上千个UnitTest保护美女们只是比较豁出去比较敢一点

想了想我现在可不是初级开发了而是运维了,现在这几天的开发对分支管理好像已经很熟了又想再写一点,但是好像偏离主题了,还是不写了

Show Comments