LinkedIn的Java 11迁移之旅


LinkedIn在2018年底开始研究Java 11,当时,Java 9、10和11在社区中还不是超级流行。作为一个轶事,在2019年底的Oracle Code One会议上,一些会议询问与会者他们的产品是否在使用Java 9或更高版本,其中只有约20%的人表示他们在使用;也很少有大公司采用Java 11。

在LinkedIn,应用程序使用四种主要类型的框架之一。Jetty、Play、Samza或Hadoop。其中,只有Jetty的版本(在LinkedIn内部)与Java 11兼容,所以这篇博客将主要介绍我们的Jetty应用程序的迁移情况。值得庆幸的是,仅Jetty应用就占了1000多个微服务和60%以上的生产份额。

准备工作可以分为三部分:升级构建框架,做性能测试,以及自动化迁移。

构建框架
准备工作的第一步是让我们的代码用Java 11构建。在LinkedIn,我们使用的是多版本策略,所以每个应用程序和库都是单独构建的,并有自己的版本库。这意味着超过2000个仓库(应用程序+库)需要将它们的构建改变为Java 11。

对于我们的构建系统,我们需要升级到Gradle 5或更高版本,以便与Java 11的升级兼容。值得庆幸的是,我们的构建工具团队很清楚这一要求,并且已经开始工作,将构建系统迁移到Gradle 5。

在看到有可能让我们的应用程序在Java 11下运行后,我们的团队迅速过渡到早期采用者测试。

性能测试
我们挑选了20个应用程序作为早期采用者,用Java 11对它们进行测试,主要是使用G1垃圾收集器,因为我们80%的应用程序都使用了G1(剩下的20%大部分是CMS)。我们的选择标准优先考虑具有较大堆尺寸、较高主机数和较高QPS的应用程序,有状态和无状态的应用程序都被选中。

总的来说,结果是积极的,没有应用程序在升级到Java 11后出现性能下降。最好的案例显示性能提高(在延迟和吞吐量方面)高达200%。这些性能提升主要体现在G1、Shenandoah和ZGC上。CMS的性能没有变化。听说JVM性能提高是一回事,但在我们广泛的应用中了解和见证它的第一手资料则完全是另一回事。

自动化
除了改变2000多个存储库的构建过程之外,还需要改变1000多个应用程序的运行时间,这对一个很小的工作组来说是一个很高的目标。值得庆幸的是,自动化在这里真正拯救了我们 我们能够将迁移到Java 11所需的许多变化自动化。虽然这并没有给我们带来100%的成功率,但将任何比例的资源库迁移到Java 11的自动化都会使工作量变得更容易接受。

在对我们的基础设施做了一些小改动后,我们有可能改变资源库的构建系统以使用Java 11。然后我们能够在测试环境中触发大规模的Java 11构建,以找出需要解决的问题。毫无疑问,这是我们在Java 11迁移过程中最重要的功能和学习之一。这种测试让我们发现了大量的边缘案例以及几个主要的挑战。以下是我们发现的一些主要挑战。

JDK交叉兼容问题
第一个也是最紧迫的问题是Java 8和Java 11之间的交叉兼容问题。我们意识到,为公司完成这次升级需要多年时间,这意味着我们将处于过渡状态,JDK 8和JDK 11都将使用一段时间。LinkedIn运行的是多版本源码控制,这意味着我们需要确保每个版本库都能同时适用于Java 8和Java 11的上游。我们称之为交叉兼容而不是向后兼容(因为字节码级别)的原因是,我们也发现了一些案例,代码可以在Java 8上编译,但在Java 11上却无法正常运行。这些情况包括删除JavaEE库,改变默认的类加载器类型,以及Java 11中更严格的类cast。

我们发现有太多的问题需要单独解决,所以我们决定使用"--release 8 "标志,以使Java 11编译器编译到Java 8级别的字节码,并限制新API的使用。这样做的坏处是不能使用新的API和语言特性,如Set.of()和var关键字。然而,好处是,我们能够更容易地保持Java 8和11之间的兼容性,这是团队一致同意的权衡。

移除库
JavaEE库已从JDK 11中删除,但它们在我们的代码库中被广泛使用。这些库中的许多都有开放源码的替代品。

我们不得不在这里做出决定,是让 repo 的所有者手动替换这些库的实例,还是将其添加到我们的构建工具链中。我们决定,对于我们的工作团队来说,移除这些使用的成本太高了,所以我们决定在构建工具链中默认添加一个静态的最终版本的JavaEE库。这些库是相对轻量级的,所以把它们打上补丁并不是什么大问题。

JVM选项的变化
JVM选项在Java 8和11之间也有很大的变化。有几个选项被淘汰了,而其他选项则被废弃,以支持更新的选项。对于大多数选项,我们使用了一个名为JaCoLine的开源服务,它可以帮助删除过时的选项。GC日志选项是由于JEP 271而得到重大修改的一组选项之一。在意识到日志会看起来完全不同,而且新旧GC日志选项之间并不总是有一个很好的映射之后,我们决定只创建一个默认选项,并要求用户在需要时修改它。

也就是说,统一的GC日志是迈过Java 8的另一个有力理由。它使阅读GC日志变得非常容易,而且它是一个可以利用来简化很多工具的特性。

内部依赖性
LinkedIn运行在一个微服务架构上。这意味着有许多存储库是通过依赖关系图相互连接的。这里的挑战是,如果一个依赖的 repo 没有完成迁移,它可能会阻止一个依赖的 repo 的迁移,因为依赖的 repo 可能需要改变以兼容 Java 11。这不是一个容易解决的问题。通过在依赖关系图上使用一些图形算法,我们发现目标应用程序有超过25级的依赖关系。我们希望鼓励较低层次的依赖关系首先迁移,但遵循严格的顺序会限制迁移的速度。

最后,我们决定使用粗略的bucketing,将迁移基本上分成三部分。在每个部分中,依赖关系图中同一级别的应用程序将被迁移。这是我们在正确性和速度之间做出的妥协,允许大多数应用程序完全不受依赖关系的阻碍,同时保持一个合适的迁移吞吐量。了解我们的依赖关系图无疑是对如何做到这一点做出明智决定的关键。

在处理了这些路障和更多的问题之后,我们使用基础设施的模拟测试机制反复测试了基础设施的变化和自动化修复,直到我们成功地自动迁移了大约一半的库(约500)。我们也将自动化应用于应用程序,但没有尝试提交,因为我们要求所有者仍然要进行运行时验证。这种运行时的验证包括功能和非功能的约束。

问题比我们之前预计的要多,我们意识到这些变化中有几个需要在未来的主要Java版本升级中得到解决。因此,当务之急是花一些时间来建立我们可以重复使用的高质量的基础设施,现在Java 17已经到来,我们非常高兴我们这样做了! 

总而言之,迁移的准备工作,包括早期采用者测试、基础设施变化、建立自动化和自动升级500个库,花了三个季度。

迁移
实际的迁移工作计划再进行三个季度,其中500个库和大约1100个应用程序将被迁移到Java 11,由两名工程师和一名技术项目经理组成的团队负责。

由于我们在迁移前进行了彻底的测试和自动化,在整个迁移过程中,我们没有看到太多的问题。准备工作确实得到了回报! 大多数团队都能在几个小时内完成迁移。

然而,我们确实看到了一些常见的运行时问题。

我们面临的一个挑战是,由于JVM尊重cgroup的限制,Java进程有较少的GC线程,因此一些应用程序的GC性能受到影响。迁移到Java 11后,这个问题在几个应用程序中暴露出来,这些应用程序基本上都在利用LinkedIn的软限制(cpu.shares),即可以从同一主机上闲置的 "邻居 "应用程序中 "借用 "CPU周期。随着cgroup限制的实施,对这些核心的访问就会丧失。在某些情况下,需要手动增加GC线程的数量以保持相同的性能。

我们在所有Java 11版本中看到的另一个问题是堆外内存使用量的明显增加。这似乎并不符合任何特定的操作,似乎更像是一个碎片化问题。从glibc内存分配器切换到mimalloc或jemalloc对这些问题有很大的帮助。

虽然这些问题一开始有点吓人,但能够挖掘出根本原因,找到适当的解决方案,并能够在这篇博文中分享我们的发现,这是件好事。

在迁移期间和之后,我们试图尽可能地测量性能。我们建立了自动化系统,利用我们的指标收集系统,以便对Java 11迁移前后的性能进行粗略测量。我们总共收集了200多个应用程序的数据,发现Java 11的P99延迟平均减少了10%,最大吞吐量平均增加了20%。值得注意的是,我们在迁移中没有改变GC类型,以减少干扰程度并对性能进行更公平的比较。希望这些在适当的样本量上的数字能对读者有所帮助。

除了性能上的改进,Java 11还带来了一些运行时的改进,比如现在开源的JFR工具。总的来说,这次迁移可以说是卓有成效的! 

未来的工作
还有很多工作要做。虽然Jetty已经完成,但我们仍然需要将剩余的三个Java应用轨道迁移到Java 11。之后,将有可能以最小的努力启用完整的Java 11字节码。此外,像ZGC、Shenandoah和Project Jigsaw这样的新功能都可以进行试验,看看在那里是否有什么好处。CMS在Java 11中也被废弃了,并在Java 14中被移除,这意味着LinkedIn关闭CMS的使用将是另一项重大举措。最后,Java 17已经出现,需要在未来考虑。