Salesforce 如何用一年的时间大规模迁移到 OpenJDK 11

Salesforce 是首批大规模采用 OpenJDK 11 的大型企业之一,在 2018 年底 OpenJDK 11 发布后不久,Salesforce 就开始了 OpenJDK 11 的采用之旅。

前沿吗?当然是。

安全吗?绝对地。

你可能还不知道,Salesforce 在整合前沿、转型技术,并以安全、可靠、无缝的方式,同时在不损害其核心价值:信任 的前提下,将这些技术提供给客户方面一直处于行业的领先地位。从 gRPC 到 Kubernetes,Salesforce 在新技术领域有着早期大胆探索的历史。

在本文的案例中,将主要的 Salesforce CRM 应用程序升级到 OpenJDK 11 是一项庞大的跨组织工作。把它做好,不仅能为我们带来几年的 Java 运行时创新红利,也能为我们的客户提供更好的体验,并为开源社区做出贡献。

背景

2018 年末,OpenJDK 11 作为 Java 最新的长期支持(LTS)版本面世。这开启了一个人们期待已久的机遇,推动了 Salesforce 应用程序的向前发展,并为我们的内部开发人员带来了巨大的新特性和创新。

为什么我们认为我们可以安全地从 OpenJDK 8(上一个最新的 LTS 版本)过渡到 OpenJDK 11 呢?

首先,我们并不是一蹴而就的。对于 OpenJDK 9 和 OpenJDK 10 这两个版本,虽然我们只打算将它们作为垫脚石,而不在生产环境中使用,但一旦它们的版本可用,我们就会立即升级到对应版本。正如你所料,最困难的部分是从 OpenJDK 8 升级到 OpenJDK 9,这需要对 Salesforce 应用程序进行重大的更改。从 OpenJDK 9 升级到 OpenJDK 10,再从 OpenJDK 10 升级到 OpenJDK 11,都只需做相对较小的改动。

另外,Java 的向后兼容性保证允许用旧版本 Java 开发编译的应用程序代码能运行在新的版本上,这一功能的威力不容小觑。向后兼容性为迁移提供了巨大的帮助,这样我们的大多数代码都不需要更改。

Salesforce 应用程序利用 Java 向后兼容性的方法之一是,将用于构建 Salesforce 应用程序的 Java 版本与用于启动它的 Java 版本分开。这使我们能够首先集中精力将过程的一端从 OpenJDK 8 升级到 OpenJDK 11,而另一端仍保持在 OpenJDK 8 上不变,并将其升级到 OpenJDK 11 的时间往后推迟。我们内部开发人员的目标是,通过在初始化和启动 Salesforce 应用程序的脚本中隐藏所有的差异和复杂性的方式,让 OpenJDK 8 和 OpenJDK 11 运行时之间的切换尽可能的简单和无缝。因此,对于我们的开发人员来说,升级到 OpenJDK 11 运行时就像用 OpenJDK 11 版本的字符串覆盖配置属性一样简单。

也就是说,我们面临的另一个挑战是,我们的 OpenJDK 11 迁移工作跨越了多个版本周期,我们必须确保任何支持 OpenJDK 11 的增量更改都不会打破我们的生产环境(生产环境仍然是基于 OpenJDK 8 部署的),也不能对客户的信任产生任何负面影响。

平台的变更 & 挑战

当我们一个接一个地升级 OpenJDK 版本时,我们遇到了 Java 平台的许多显著变更。而我们的迁移之路漫长且需有条不紊地推进,这意味着这些变更会给我们带来了很多挑战,但在这里我们只讨论其中的几个。

类路径与模块化

Java SE 9 平台引入的重大变更之一就是 Java 平台模块系统(Java Platform Module System,JPMS)。JPMS 将 JDK 划分为多个模块,每个模块都是一组命名唯一且可重用的相关包。

好消息是,Java 9 仍然支持传统的类路径,它能与模块路径一起工作,并映射到一个被称为未命名模块的特殊模块上。因此,构成 Salesforce 应用程序类路径的所有 JAR 文件都会自动加入模块系统,从而导致了传统类路径和模块路径的混合。

实际上,Salesforce 应用程序的整个类加载器层次结构都保留在 Java 9 及更高版本中。它由我们的 Web 服务器和 Servlet 容器锚定,委托给 OSGi 类加载器,而 OSGi 类加载器又委托给 Java 运行时的内置类加载器。

然而,作为 Jigsaw 计划的一部分,Java 9 带来了一个影响类加载的重大变更。这是对授权标准覆盖机制(Endorsed Standards Override Mechanism,用于支持加载包含授权标准和独立技术实现的 JAR 文件)和扩展机制( Extension Mechanism,用于支持加载包含扩展或可选软件包的 JAR 文件)的移除。由于 Salesforce 应用程序过去依赖于这两种机制,因此必须使用 -module-path-upgrade-module-path-patch-module 标志的组合,将所有受影响的 JAR 文件迁移到 Salesforce 应用程序的模块路径下。不过,这些非模块化的 JAR 文件都无需转换为模块:它们作为依赖项被放置在 Salesforce 应用程序的模块路径上,从而自动成为模块化的。此功能被称为自动模块化,创建它是为了减轻将现有应用程序转换为新模块系统的负担。

影响 Salesforce 应用程序的另一个变更是删除了 Salesforce 应用程序所依赖的 Java Enterprise Edition(“Java EE”)API。Java 9 开始将这些 API 分离到它们各自的模块中,这些模块被注解为不推荐使用,以便删除,这表明了在将来的版本中会删除它们的意图。这些模块包含在运行时镜像中,但默认情况下未启用。因而,它们必须通过 --add modules 标识显式“激活”。

从 Java11 开始,这些模块不再包含在运行时中(参见 JEP 320:删除 Java EE 和 CORBA 模块)。相反,Java EE 和 CORBA 技术的独立版本作为 Maven 构件发布,并可以从第三方网站(如 Maven Central)上获取,我们从那里下载了它们并将它们添加到了 Salesforce 应用程序的模块路径中。

向后不兼容

在将 Salesforce 应用程序的 Java 运行时迁移到 OpenJDK 11 时,我们发现了许多向后不兼容的变更。其中大多数都是“设计使然”,并且是涵盖在版本说明中了的,正如下面所要讨论的那样。(有一个 true 的回归影响了布尔型 bean 属性的内省;这是由 OpenJDK 实现本身的一个 bug 引起的,我们报告了这个 bug,并且它已经被修复了。)

设计上向后不兼容变更的例子很明显,因为它会导致 JVM 在启动时中断,并出现如下的错误:

Unrecognized VM option '<Option>'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

引发该错误的原因是 Salesforce 应用程序一直在使用一些 Java 9 以后不再支持的垃圾回收(GC)选项。某些受影响的 GC 选项(例如 UseParNewGC )在 JDK 8(JEP 173)中已被弃用了,而在 Java 9(JEP 214)中已经被移除了。其他的,包括 PrintGCDateStampsPrintGCTimeStamps 在内 ,由于已经在 Java 9 中重新实现了 GC 日志(请参阅 JEP 271)以便使用 JEP 158 中引入的 Unified JVM Logging 框架,它们都变成非法的了。

接下来的挑战就变成,继续为仍运行在 OpenJDK 8 上的 Salesforce 生产实例提供这些 GC 选项的支持,同时避免这些选项用在已经升级到 OpenJDK 11 的 Salesforce 生产实例上。

我们采用了一种可扩展方法,在启动 Salesforce 应用程序之前,扩充负责组装该应用程序的 JVM 参数列表的 ant 目标,这样,当 Java 运行时被设置为 OpenJDK 11 时,它会过滤掉(使用 ant 语法)所有不受支持的 GC 选项。事实证明,这种方法非常灵活,允许我们将选定的 Salesforce 生产实例升级到 OpenJDK 11,并在需要时回滚到 OpenJDK 8。一旦 OpenJDK 11 成为新的默认 Java 运行时,并且所有的生产实例都已经成功迁移,过滤器就可以从 ant 目标中移除了。

虽然影响 GC 选项的变更明显破坏了 Salesforce 应用程序,但其他设计上的变更却以更微妙的方式对应用程序造成了破坏。其中一个变更影响了 fork/join 公共池线程的上下文类加载器,它不再继承任务提交线程的上下文类加载器,而是使用系统类加载器进行初始化。JDK 9 版本说明中涵盖了这一变更,并提供了恢复以前行为的解决方法。这一变更的影响在 Salesforce 应用程序中以许多不同的方式表现出来了。

其他设计方面的变更影响来自核心库的 Java 语言 API,核心库的实现已经被更改,以便更严格地执行其原始 API 契约。其中一个例子是 java.util.HashMap.computeIfAbsent ,它的实现已经被增强以防止可重入使用,其中传递给 computeIfAbsent() 的映射函数修改了调用 computeIfAbsent() 的映射。以前,这种情况并未引起注意,但可能会使映射(map)处于不一致的状态。但是,从 OpenJDK 9 开始,它会被检测到并被标记成 ConcurrentModificationException

第三方依赖 & 开源贡献

除了升级 OpenJDK 外,我们还需要升级 Salesforce 应用程序的一些底层第三方依赖。如果你忽略团队为升级 PowerMock 而修改 2700 多个 Java 测试类的时间,那么大部分工作都是相当简单的。也就是说,作为 OpenJDK 11 的早期使用者,考虑到 Salesforce 应用程序的复杂性,我们有望在开发过程中解决一些 bug。这就为向开源社区贡献一些修复程序带来了很好的机会。

OSGi

OSGi 就为我们带来了一个机会,在启动过程中,我们遇到了 javax.annotation 的问题。javax.annotation 包是受 JEP 320 影响的包之一,JEP 320 对 Java SE 11 生效,并从 JDK 中删除了这个包和所有其他 Java EE 和 CORBA 包。按照该 JEP 的建议,我们已经将所有提供缺失包的 JAR 文件(包括 javax.annotation-api.jar )添加到了 Salesforce 应用程序的模块路径中,在那里它们将被视为自动模块(请参见上文)。根据 JPMS 规范,自动模块应该导出其所有的包——显然在我们的例子中不会发生这种情况!

事实证明,我们在 OSGi 框架的包解析逻辑中发现了一个 bug(违反了 JPMS 规范)。我们向管理 OSGi 项目的 Eclipse 基金会报告了这个问题,并提交了一个修复程序。我们的修复程序可以确保将自动模块的所有包自动添加到 VM 提供的包列表中,它被接受并被合并发布到了 OSGi 社区。

Procyon

作为检入 Salesforce 应用程序代码变更的一部分,开发人员将其变更列表(CL)提交给预签入(Pre-checkin),预检入会对其进行检查以确保 CL 不会将任何重复的类引入到 Salesforce 应用程序的类路径中。重复的类是指具有相同 FQCN 但内容不同的类。预检入的重复类查找器(Duplicate-Class-Finder,DCF)依赖于 Procyon 的 Java 反编译器,该反编译器使用给定的 FQCN 搜索和反编译类,能在类路径上搜索 JAR 文件列表。

DCF 已经被集成到 Salesforce 应用程序中,并从该应用程序继承了它的 Java 运行时。当在 OpenJDK 11 运行时上执行时,Procyon 的反编译器会失败。我们向 Procyon 报告了这个问题,并提交了一个简化可执行的测试用例来重现该问题。Procyon 开发人员重现并修复了这个问题,解除了 Salesforce 应用程序当 Java 运行时设置为 OpenJDK 11 时的预检入阻塞问题。

首发优势

Multi-release JAR 文件

正如前文所述,Salesforce 应用程序利用 Java 向后兼容性的方法之一是,将用于构建 Salesforce 应用程序的 Java 版本与用于启动它的 Java 版本分开。这样可以隔离风险,因此,即使 Salesforce 应用程序及其依赖项仍然是使用 OpenJDK 8 构建的,在运行时,我们也可以利用一些从 Java 9 才开始添加的新的核心 Java API(例如,JEP 259 引入的新 stack-walking API),利用 Multi-releaseJAR 文件(JEP 238)的优势。

Multi-release JAR 是在 Java 9 中引入的一个新特性:它扩展了 JAR 文件的格式,允许同一 Java 类资源的多个版本共存于同一 JAR 文件中,其中该类的每个版本可以是以不同方式实现并根据不同 JDK 版本编译的。

支持多版本的类加载器会从多版本 JAR 文件中自动加载适当的类(即,那些与 Java 运行时 JDK 版本相匹配的类)。我们的 Servlet 容器和 OSGi 类加载器都支持多版本 JAR 文件,并且随着 JDK 11 及以上版本的广泛使用,我们预计将有越来越多的第三方依赖项会使用这种格式打包。

内置的性能改进

“紧凑字符串”(JEP 254)为我们提供了一个免费的性能优化。这个特性最初是在 Java 9 中引入的,它通过将字符(char)数组迁移到更紧凑的字节(byte)数组(加上一个编码标识字段)来提供更节省内存的字符串内部表示。这降低了字符串的总体堆使用和内存压力,进而对垃圾回收和应用程序整体性能产生了积极影响。

监控改进

Java Flight Recorder(JFR)是一种分析工具,用于从正在运行的 Java 应用程序中收集诊断信息并分析数据。JFR 过去只作为商业 JDK 插件提供,但是从 OpenJDK 11 开始,它就与 Java Mission Control 一起开源了。现在可以在单个 Salesforce 应用程序服务器实例上启用 JFR 来解决性能问题,这是一个巨大的利好。

期待

OpenJDK 11 的升级发布没有出现任何大的问题。它的推出从开始到结束大约花了 6 个月的时间,遵循了我们通常所遵循的经过充分审查的分散策略,以减轻客户影响。

在推出完成后不久,我们就将重点转移到了用于构建 Salesforce 应用程序的 Java 版本上。它仍然被设置为 OpenJDK 8,然后我们也将它升级到了 OpenJDK 11。通过将应用程序的编译时版本升级到 OpenJDK 11,我们的开发人员可以使用自 Java 9 以来引入的所有新的 Java 语言功能,其中包括新的 stack-walking API、新的 HTTP 客户端、对 try-with-resources 语句的改进、允许在接口中使用私有方法、OptionalCollectors 类中的新方法、 CompletableFuture 的改进、用于局部变量类型推断的新 var 关键字,等等。

我们期望这些新的 Java 语言特性能给我们带来显著的生产力提升和创新收益。将运行时和编译时的 Java 版本升级到 OpenJDK 11 使我们能够更快、更无缝地采用未来的 Java 版本。

附加资源

JDK 版本说明

https://www.oracle.com/java/technologies/javase/jdk-relnotes-index.html

Java 平台,标准版 Oracle JDK 9 迁移指南

https://docs.oracle.com/javase/9/migrate/toc.htm#JSMIG-GUID-7744EF96-5899-4FB2-B34E-86D49B2E89B6

JSR 376(Java 平台模块系统)

https://openjdk.java.net/projects/jigsaw/spec/

原文链接: