雪球内部很早就开始尝试使用 Spring Boot,我们团队是第一批吃螃蟹的人(使用的版本是 v1.2.4,发布于 2015 年中)。到目前 Spring Boot 已经成为公司内普及的基础底层框架,但各个团队之间使用上并不统一,1.2、1.3、1.5 版本都有在使用,也引发了一些中间件的版本依赖问题。
我们的版本是其中最古老的,已经偏离了四个大版本。其实很早就有升级的规划,在 1.5 版本期间有尝试过一次,因为涉及的改动很多加上当时业务繁忙所以停滞了。Spring Boot 2.0 release 版本在三月份发布,四月正好空余出较大块的时间,趁这个天时地利机会把整个后端模块全部升级到了 2.0 基准。
这次会分享一些升级过程中的一些经验及遇到问题、踩过的坑,旨在帮助大家能更顺畅的升级,顺便推动大家在合适的机会把版本升级到统一基准下。
简单介绍下新特性
Spinrg Boot 2.0 带来最大的新特性是 Reactive Spring & WebFlux,准确的说是因为引入了 Spring 5.0。
Reactive Programing(响应式编程,以下简称 RP)核心思想是利用异步和消息驱动构建一个流畅的高弹性的系统。 RP 不是什么新鲜的技术,这里就不展开介绍了。WebFlux 是基于 Reactive Streams 构建的一整套新的响应式编程模型,并提供像 WebClient 响应式的客户端,对标的是 Spring MVC 模块。
在 RP 模型下,务必要保证整个流程的异步性,如果一个 Callback 堵塞会直接影响到整系统,但目前大多数的 JAVA 库都是阻塞模型的,虽然 Spring 这次带来了一整套的响应式技术栈,但 JDBC 这种阻塞模型还没有得到支持(阿里内部通过 JVM 的 Wisp 协程技术解决此类问题,并基于 RX Java 也实现了一整套的 RP 模型框架),希望 Spring Data Reactive 也可以早日解决。
还有其他一些特性例如 HTTP/2 的支持、Actuator 模块的增强、Quartz 模块的支持等更多特性大家可以查阅官方文档。
除了新特性还有其他一些有意思的改动,或许算是技术方向的指引?
- Lettuce 替代 Jedis
- Caffeine 替代 Guava
- HikariCP 替代 Tomcat Pool
市面上常用的 Redis Client 除了 Jedis 还有 Lettuce 和 Redisson,两者都是基于 Netty 实现,并也都支持了响应式的 API,Spring 选择 Lettuce 的原因我猜测或许是因为它相对 Redisson 来说足够简单~
Guava Cache 是非常常用的组件,Caffeine 采用了更高效的 LRU 算法,并优化了内存使用。官方性能评测优于 Guava 10 倍左右,这应该是 Spring 放弃 Guava 的原因。Caffeine 也提供了 Guava Adapter,可以让用户无缝迁移过来,目前我们团队也逐步替换中。
HikariCP 一直号称是最快的连接池组件,这次变为 Spring Boot 默认组件算是官方对它的认可了。雪球团队一直使用 Druid 连接池,主要是因为它在监控方面做的很全面,插件很多,还经过了国内诸多大厂的全面“测试”,使用起来较顺手。
升级过程
对于过于老旧的版本,官方建议的是先升级到 1.5 作为跳板再升级到 2.0。但我们的做法比较激进, 也导致涉及依赖包的升级,配置的变动,及代码层面的改动非常之多,在提交上线的 PR 时改动文件有 250+,看到还挺惊讶。
这里推荐几个组件,利用好 starters 可以有效减少依赖包的管理难度
- spring-boot-starter-json
- spring-boot-starter-test
- spring-boot-starter-cache
这几个官方组件支持了一些常用的模块,例如 jackson,测试的 JUnit、mockito 及常用的 cache manager 支持等。
配置层面也有许多变化,重构了很多 key 结构,也提供了 spring-boot-properties-migrator 模块来协助迁移。 并提供了更松散的绑定规则(破折号,驼峰,下划线),但建议在 .properties 和 .yml 文件中使用小写字母+破折号规范(个人建议所有配置都使用这个标准)
|
|
以上这些改动虽然非常多但较容易发现,或是编译失败或无法启动。整个升级过程中最可怕的是一些隐性的风险,非常容易忽略。
Path Matching 默认关闭后缀匹配
在以前你可以任意修改路径后缀名都能得到正确的解析,例如 /fund/000961.xyz 也能正确解析到 /fund/000961。但是 2.0 默认禁止了后缀匹配模式,这也是 Spring 的官方建议。
其实在做升级的过程中已经知道有这个改动,但我们团队一直使用的 REST 风格不带后缀的形式,所以没太在意。但万万没想到,其他一个团队的使用了我们的开放接口,但却采用了他们自己的路径规则,加上了 .json 后缀,在以前一直稳定运行,但现在这部分接口全部挂掉。这也是整个升级过程造成的唯一线上事故,幸好及时发现修复,没有产生大的影响。
解决方案就是把后缀匹配打开:
|
|
新增 HTTP PUT Method Filter
在升级后的测试过程中发现部分接口明明参数正确却一直提示失败,经排查发现 PUT 类型的参数出现了数据重叠
|
|
随后跟踪源码 debug 发现新增了几个 Filter,其中 HttpPutFormContentFilter 用来处理 PUT 及 PATCH 的 body 传参问题(这个补丁在 1.5 版本后就已经引入并默认开启)。
因为浏览器 form 表单只支持 GET 与 POST 请求,所以早期的 Spring 不支持 PUT 请求使用 body 传参,正确的解决方案应该是在后端做 Method 的隐式转换。但之前并没有使用此方案,而是客户端同学把参数改成以 param 传递,这本身没有太大问题,但却忘记了把 body 里参数去掉,在新版本就变成了重名参数,以字符数组接收(数值类的不受影响)
解决方案是暂时关闭这个 Filter,待客户端修复后,在未来不再支持这些有问题的老版本客户端时再打开:
|
|
Swagger 官方不支持 Spring Boot 2.0
在做完了所有的升级后发现 Swagger 无法使用,去官方 repo 寻求方案,发现有人已经问过了,官方回应暂不支持 2.0 版本😱。幸好在其他帖子内发现有人说使用成功,但没有给出清晰的解决方案,只好自己一点点摸索了,好在最终还是解决了。
方案是去除老的 security 配置以及添加部分资源的映射
|
|
总结
本次升级涉及十多个子系统模块,从开发->测试->灰度->上线大概 3 周多时间,除了上面引发的小事故外整个升级过程比预期要顺利很多,总结下经验有以下几点
- 对系统代码及方案足够的熟悉
- 不要只看升级指南,要详读官方文档
- 有较大块的时间及足够细心
有些人因为担心风险而不愿意升级,觉得够用就行。但在我看来合理的升级是必要的,收益也是显而易见的,只要使用了正确的升级方法,大部分风险还是可控的。对于升级这件事来说,时间越久,版本差异越大意味着风险也越高,制定合适的升级周期也能有效的降低风险。