背景
Flink 提供了从 Tuple0 ~ Tuple25 的 Tuple 类供用户选择,顾名思义,每个 Tuple 对象分别可以存储 0 个 ~ 25 个任意类型的字段,例如图 1 展示了 Tuple2 的类定义。由于腾讯云 Oceanus 流计算的客户业务场景较为复杂,需要用到更高维度的固定 Tuple 类,我们将 Tuple 类进一步扩展,达到了 Tuple250 甚至 Tuple500.
但是,随着 Tuple 维度的增多,我们观察到了一个诡异的现象:虽然需要编译的源码文件增加个数不多,但是编译所需时间越来越长,且并非线性增长:原本只需要一分钟就可以完成的编译,现在需要动辄一个多小时;如果在本机进行编译,甚至几个小时都编译不完。这给我们的开发效率带来了一定程度的影响,因此有必要找出问题根源。
初探
为了找出 Tuple 数与编译时间的关系,我们还写了一个自动化脚本,每次向源码里增加 1 个更高维度的 Tuple 类(例如依次放入 Tuple26.java、Tuple27.java 等等),观察项目的构建速度,并绘制了如下的曲线(图中公式使用 Excel 的趋势线进行拟合),见下图 2:
可以看到,编译时间随 Tuple 数变化的曲线,完美符合三次函数,即该算法的时间复杂度约为 O(n^3)。如此高的时间复杂度,一定要找出根源,否则随着业务规模的进一步扩大,编译时间会越来越难以接受。
为了解决这个问题,我们首先想到的是使用 Profiling 工具进行热点和调用时长的统计分析。这里选择了 JProfiler,它提供了很多有用的分析视图,可以迅速找到问题的直接根源。
首先我们对编译缓慢的项目启动编译构建,默认情况下是基于 Maven 的,因此需要找出是不是 Maven 导致的问题。我们采用的 JDK 版本是 1.8.0_202.
首先我们使用 JProfiler 的 Sampling 模式进行采样(如图 3),它的效果类似于不断地运行 jstack 命令,不进行侵入式的修改,因此得到的数据较为准确;另一种 Instrumentation 模式适合于找到问题的热点后,使用 JVMTI 动态修改字节码机制(线上定位神器 btrace 也是基于这个原理),进行局部的细致分析。需要注意的是,默认情况下采样排除了 JVM 内部的调用,我们由于需要定位 JDK 的问题,需要在 Call tree filters 里把所有的排除规则清空,否则问题只能定位到 Maven 这一层。
当程序运行一段时间后,我们找出了热点方法(见图 4),即 javac 编译起内部的 List 相关调用;通过仔细追踪调用链,发现是 checkWithinBounds 方法过于缓慢。
既然热点方法找到了,那么下面就需要探究这个方法在 javac 编译器中是做什么的,它的算法为什么这么慢,以及是否有优化的方式。
详细定位
由于调用链里有 Infer 类,我们知道它是负责泛型的类型推断的。通过搜索泛型编译缓慢等关键字,找到了 JDK-8086048 这个 Bug 单,同时在 JDK-8080656 这里也有提到同样的问题。
随后我们又跟踪到了 JDK-8051946 和 JEP-215。在这个 2014 年就提出的 JEP-215 中,开发者设计了一种新的 javac 方法类型检测机制 TA(Tiered Attribution)来代替现有的 SA(Speculative Attribution),可以极大加速多态表达式(Poly Expression)的检查过程。
通过阅读这个 JEP(JDK Enhancement Proposal)的描述,可以知道目前的 SA 算法需要在同一颗语法树上,对多个不同的目标进行多次类型检查,例如一个 多态表达式有 N 种重载选项,那么就需要检查 N * 3 1 次。如果参数还允许嵌套的话,那么多个因子还会相乘,这样就导致了我们上述遇到的很高的时间复杂度了。
而这个新的 TA 算法提供了一种更高效的多态表达式类型检查机制。例如省去了重载解析过程的类型检查,并于重载解析前,为每一个方法调用过程中的多态参数表达式(poly argument expression)构造解析所需的自底向上结构化类型,以大大减少总的尝试次数。
根据这些 Bug 单,JEP-215 已经在 JDK 9 及更高版本上得以实现。因此我们改用当前已发布的 LTS 版本 JDK 11 进行验证。
通过修改 JAVA_HOME 环境变量,可以让 Maven 选择使用不同的 JDK 版本进行编译,我们修改为 JDK 11 的路径后,重新进行编译,并再次进行采样,结果发现类型推断已经不再是占用 CPU 最多的方法了(图 5):
同时我们欣喜地发现,整个项目只需要 1.5 分钟就构建完毕了,相对之前的 1 个多小时,有了质的飞跃(图 6):
由此可见,这个 JEP-215 起到了立竿见影的效果,让项目构建的时间恢复了往日的情景。