基于 GraalVM 的 ShardingSphere Proxy Native 探索(下篇)
早期处理
使 ShardingSphere 能够 Native 化的早期处理
作为临时性的措施,将 GraalVM Native Image 形态的 ShardingSphere Proxy 命名为 Apache ShardingSphere Proxy Native 以方便之后的处理。对于每夜构建,已有 PR 只考虑 Linux 下的 GraalVM Native Image 产物和包含此 GraalVM Native Image 产物的 Docker Image,这有助于在 Windows 上避开来自 MSVC 的可能的限制;另一方面,MacOS(aarch64) 在 GraalVM CE 22.3.1 上尚未完全就绪,而 Github Actions 默认情况下只有 AMD64 的设备。
org.apache.shardingsphere:shardingsphere-infra-util 等子模块中直接使用到groovy.lang.Closure
,groovy.lang.GroovyShell
,groovy.lang.Script
这三个与 Invokedynamic 或 Runtime Bytecode Generation 相关的类,直接通过 $JAVA_HOME/bin/native-image
构建 Apache ShardingSphere Proxy 的 GraalVM Native Image 会失败。
Fatal error: com.oracle.graal.pointsto.util.AnalysisError$ParsingError: Error encountered while parsing java.lang.invoke.CallSite.setTargetNormal(java.lang.invoke.MethodHandle)
Parsing context:
at java.lang.invoke.CallSite.setTargetNormal(CallSite.java:289)
at java.lang.invoke.MutableCallSite.setTarget(MutableCallSite.java:155)
at java.lang.invoke.SwitchPoint.invalidateAll(SwitchPoint.java:225)
at org.codehaus.groovy.vmplugin.v8.IndyInterface.invalidateSwitchPoints(IndyInterface.java:186)
at org.codehaus.groovy.vmplugin.v8.IndyInterface.lambda$static$0(IndyInterface.java:172)
at org.codehaus.groovy.vmplugin.v8.IndyInterface$$Lambda$4894/0x00000007c3c18490.updateConstantMetaClass(Unknown Source)
at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.fireConstantMetaClassUpdate(MetaClassRegistryImpl.java:411)
at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.setMetaClass(MetaClassRegistryImpl.java:293)
at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.setMetaClass(MetaClassRegistryImpl.java:310)
at groovy.util.ProxyGenerator.setMetaClass(ProxyGenerator.java:229)
at groovy.util.ProxyGenerator.<clinit>(ProxyGenerator.java:57)
at com.oracle.graal.pointsto.util.AnalysisError.parsingError(AnalysisError.java:153)
at com.oracle.graal.pointsto.flow.MethodTypeFlow.createFlowsGraph(MethodTypeFlow.java:104)
at com.oracle.graal.pointsto.flow.MethodTypeFlow.ensureFlowsGraphCreated(MethodTypeFlow.java:83)
at com.oracle.graal.pointsto.flow.MethodTypeFlow.getOrCreateMethodFlowsGraph(MethodTypeFlow.java:65)
at com.oracle.graal.pointsto.typestate.DefaultSpecialInvokeTypeFlow.onObservedUpdate(DefaultSpecialInvokeTypeFlow.java:61)
at com.oracle.graal.pointsto.flow.TypeFlow.update(TypeFlow.java:562)
at com.oracle.graal.pointsto.PointsToAnalysis$1.run(PointsToAnalysis.java:488)
at com.oracle.graal.pointsto.util.CompletionExecutor.executeCommand(CompletionExecutor.java:193)
at com.oracle.graal.pointsto.util.CompletionExecutor.lambda$executeService$0(CompletionExecutor.java:177)
at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1395)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
Caused by: org.graalvm.compiler.java.BytecodeParser$BytecodeParserError: com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported method java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite, MethodHandle) is reachable
To diagnose the issue, you can add the option --report-unsupported-elements-at-runtime. The unsupported element is then reported at run time when it is accessed the first time.
at parsing java.lang.invoke.CallSite.setTargetNormal(CallSite.java:290)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.throwParserError(BytecodeParser.java:2518)
at com.oracle.svm.hosted.phases.SharedGraphBuilderPhase$SharedBytecodeParser.throwParserError(SharedGraphBuilderPhase.java:110)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.iterateBytecodesForBlock(BytecodeParser.java:3393)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.handleBytecodeBlock(BytecodeParser.java:3345)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.processBlock(BytecodeParser.java:3190)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.build(BytecodeParser.java:1138)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.buildRootMethod(BytecodeParser.java:1030)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.GraphBuilderPhase$Instance.run(GraphBuilderPhase.java:97)
at com.oracle.svm.hosted.phases.SharedGraphBuilderPhase.run(SharedGraphBuilderPhase.java:84)
at jdk.internal.vm.compiler/org.graalvm.compiler.phases.Phase.run(Phase.java:49)
at jdk.internal.vm.compiler/org.graalvm.compiler.phases.BasePhase.apply(BasePhase.java:446)
at jdk.internal.vm.compiler/org.graalvm.compiler.phases.Phase.apply(Phase.java:42)
at jdk.internal.vm.compiler/org.graalvm.compiler.phases.Phase.apply(Phase.java:38)
at com.oracle.graal.pointsto.flow.AnalysisParsedGraph.parseBytecode(AnalysisParsedGraph.java:135)
at com.oracle.graal.pointsto.meta.AnalysisMethod.ensureGraphParsed(AnalysisMethod.java:685)
at com.oracle.graal.pointsto.flow.MethodTypeFlowBuilder.parse(MethodTypeFlowBuilder.java:171)
at com.oracle.graal.pointsto.flow.MethodTypeFlowBuilder.apply(MethodTypeFlowBuilder.java:349)
at com.oracle.graal.pointsto.flow.MethodTypeFlow.createFlowsGraph(MethodTypeFlow.java:93)
... 13 more
Caused by: com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported method java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite, MethodHandle) is reachable
To diagnose the issue, you can add the option --report-unsupported-elements-at-runtime. The unsupported element is then reported at run time when it is accessed the first time.
at com.oracle.svm.hosted.substitute.AnnotationSubstitutionProcessor.lookup(AnnotationSubstitutionProcessor.java:263)
at com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor$ChainedSubstitutionProcessor.lookup(SubstitutionProcessor.java:140)
at com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor$ChainedSubstitutionProcessor.lookup(SubstitutionProcessor.java:140)
at com.oracle.graal.pointsto.meta.AnalysisUniverse.lookupAllowUnresolved(AnalysisUniverse.java:438)
at com.oracle.graal.pointsto.infrastructure.WrappedConstantPool.lookupMethod(WrappedConstantPool.java:180)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.lookupMethodInPool(BytecodeParser.java:4219)
at com.oracle.svm.hosted.phases.SharedGraphBuilderPhase$SharedBytecodeParser.lookupMethodInPool(SharedGraphBuilderPhase.java:138)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.lookupMethod(BytecodeParser.java:4206)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.genInvokeStatic(BytecodeParser.java:1648)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.processBytecode(BytecodeParser.java:5288)
at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.iterateBytecodesForBlock(BytecodeParser.java:3385)
... 28 more
------------------------------------------------------------------------------------------------------------------------
13.4s (9.3% of total time) in 29 GCs | Peak RSS: 5.50GB | CPU load: 3.45
========================================================================================================================
Failed generating 'apache-shardingsphere-proxy-native' after 2m 22s.
Error: Image build request failed with exit status 1
com.oracle.svm.driver.NativeImage$NativeImageError: Image build request failed with exit status 1
at org.graalvm.nativeimage.driver/com.oracle.svm.driver.NativeImage.showError(NativeImage.java:1730)
at org.graalvm.nativeimage.driver/com.oracle.svm.driver.NativeImage.build(NativeImage.java:1427)
at org.graalvm.nativeimage.driver/com.oracle.svm.driver.NativeImage.performBuild(NativeImage.java:1387)
at org.graalvm.nativeimage.driver/com.oracle.svm.driver.NativeImage.main(NativeImage.java:1374)
这首先要求根据初步的 Error Log 在 GraalVM Native Build Tools 的 Maven Plugin 中的定义 --
report-unsupported-e
lements-at-runtime
的 buildArg,以将不支持的 elements 延迟到 run time 时才报告 Error Log。
就 Apache Groovy 社区的一般认知来说,这属于不太合理的 buildArgs。与此相关的[18],被 Abderrahim Oubidar 追踪于 OracleLabs 内部的 GR-43010,在当前可以认为必须使用到 Truffle Espresso 才能在 GraalVM Native Image 下使用这些特性。在加入位于 ghcr.io[19] 的每夜构建之前,已经存在一部分为了在 GraalVM Native Image 下使用的前置 PR。
对于 #17665,因 Rafael Winterhalter 在 Support subclass mocks on Graal VM[20] 为 Mockito 的 Subclass Plugin 提供了 GraalVM 支持,将 ShardingSphere 项目的 Mockito 版本提高到 4.5.1 是合理行为,作为配套的行为,byte-buddy 的版本被提高也是预期行为。
这有助于在未来的某一个时间点,为 ShardingSphere 的子模块在 GraalVM Native Image 下运行单元测试。对于 #18175,这涉及到 Paul King 在 GROOVY-10643[7] 恢复特定弃用类的讨论,以避免 GraalVM 内置的 Feature 类失效。
对于 #19737,这涉及到 Apache Calcite 1.28 的里程碑。在 1.28 中,Calcite 将最近引入的 configuration system[21] 从基于 ImmutableBeans 的内部系统转换为使用 Immutables annotation processor[22]。该库带来了大量附加功能,这些功能应该可以使 Calcite 中的 value-type classes 更易于构建和利用。它还减少了对 dynamic proxies 的依赖,这将提高性能并减少内存占用。最后,这一变化增加了与 GraalVM 等提前编译技术的兼容性。作为此更改的一部分,已进行了一些小更改,并且弃用了关键方法和类。
对于 #19964,旨在处理 H2Database V1 的 CVE,并额外匹配位于 GraalVM Reachability Metadata 中央仓库的 GraalVM Reachability Metadata[23]。对于 #20937 和 #21046,其同样是为了处理 CVE,但这使得一定程度上与 Junit4 的类解耦,简化迁移到 Junit 5 Jupiter 的工作量。
对于在 GraalVM Native Image 内执行 Junit 的单元测试,这一特性由 GraalVM Native Build Tools 针对 Junit Platform 提供支持,而不是由 Junit 自身提供支持。Sam Brannen 计划以 Provide built-in support for GraalVM native images[24] 为终止点,在 Junit 5.10 为 GraalVM 提供内置的支持。
在那之前,一个项目的单元测试是使用由 Junit 5 Vintage 提供支持的 Junit 4,还是使用 Junit 5 Jupiter,只要单元测试运行在 Junit Platform 支持的单元测试库上,通常认为是没有区别的 -- 当然存在例外,尤其涉及到 Kotlin 和 Scala 的 JVM 语言上主流的单元测试库的时候。
由于 Junit5 并没有提供内置的 GraalVM Native Image 支持,这导致部分 Junit 类的使用需要指定 GraalVM Native Build Tools 的 buildArgs。一个典型问题为,当在单元测试类中使用 org.junit.jupiter.api.Tag
时,如果需要执行 nativeTest,需要添加 --initialize-at-build-time=org.junit.platform.engine.TestTag
的 buildArgs。这在 GraalVM Native Build Tools 的 Gradle Pluginnative-gradle-plugin:0.9.20
中,设置类似于如下:
graalvmNative {
binaries {
test {
buildArgs.add('--initialize-at-build-time=org.junit.platform.engine.TestTag')
}
}
}
此外,没有可选途径能够在 GraalVM Native Image 下使用 org.junit.jupiter.api.Timeout
注解,这一现象临时记录在 Add support for `io.etcd:jetcd-core:0.7.5`[25] 当中。
根据 Janne Valkealahti 在 GraalVM Reachability Metadata 中央仓库的 Support testing macos/windows[26] ,org.junit.jupiter.api.condition.EnabledOnOs
也不能开箱即用地使用,这会在构建 GraalVM Native Image 的过程中表现为 Error: Classes that should be initialized at run time got initialized during image building: org.junit.jupiter.api.condition.OS was unintentionally initialized at build time.
。
Reachability Metadata
ShardingSphere 处理到的 Reachability Metadata
进一步切入到在添加了 ShardingSphere Proxy Native 的每夜构建之后的 Task。
#21341,#21571,#21657,#21688,#22169,#23728 旨在完善 ShardingSphere Proxy Native 的配置与匹配更多位于 GraalVM Reachability Metadata 中央仓库的 JSON 文件。
GraalVM Reachability Metadata 中央仓库在其 Release 时会被打包为 zip 文件,此位于 github.com 的 zip 文件会由 GraalVM Native Build Tools 的 Maven Plugin/Gradle Plugin 进行下载并被解压。
GraalVM Reachability Metadata 中央仓库的 GraalVM Reachability Metadata 要求依赖所提交的版本必须具有对应的单元测试,以完成 nativeTest 环节。这一环节目前是 GraalVM Native Build Tools 独有的,将会首先在 GraalVM JIT 的 JVM 下执行单元测试,以生成单元测试对应的 test id -- 并非所有 Junit Platform 上的单元测试库都允许这么做,然后构建成 GraalVM Native Image 后,在 Native Image 内执行单元测试。
在 GraalVM Reachability Metadata 中央仓库,一份依赖的 JSON 文件是对应于一个特定版本的,当此份 JSON 文件在对应的 index.json
中的 latest 属性为 true,如果 GraalVM Native Build Tools 的下游使用者使用的依赖版本与中央仓库的版本不匹配,则会使用 index.json
的 latest 属性为 true 的依赖的 GraalVM Reachability Metadata。以 ch.qos.logback:logback-classic[27] 为例。
对于 metadata/ch.qos.logback/logback-classic/index.json
,其定义了此依赖现存的版本的 JSON 和 latest 标签。
metadata/ch.qos.logback/logback-classic/1.2.11/index.json
& metadata/ch.qos.logback/logback-classic/1.4.1/index.json
其只是在索引同级文件夹的文件名。
[
{
"latest": true,
"metadata-version": "1.4.1",
"module": "ch.qos.logback:logback-classic",
"tested-versions": [
"1.4.1"
]
},
{
"metadata-version": "1.2.11",
"module": "ch.qos.logback:logback-classic",
"tested-versions": [
"1.2.11"
]
}
]
metadata/index.json
和 tests/src/index.json
这两个巨大的索引文件,这在当前并不是最优解,如 Sébastien Deleuze 提及的 Introduce a default-for attribute[28] 如果被解决,我们能够在细粒度定义匹配到的 Metadata 的依赖版本的范围。而琐碎的 index.json
往往导致 GraalVM Reachability Metadata 中央仓库的 PR 面临合并冲突和分支过期问题,这涉及到 Remove index.json from the metadata folder[29] 。
此 issue 未解决使得大多数情况下要求贡献者手动在 index.json
的中间位置插入元数据的索引,以避免打开的 PR 频繁面临合并冲突。围绕 oracle/graalvm-reachability-metadata 的 PR 围绕 ShardingSphere Proxy 的依赖树上的依赖进行。
针对 com.github.luben:zstd-jni
的 PR 草案最初来自 ShardingSphere 对 Netty 的元数据的需求,而 Netty 的一部分模块依赖于 Zstd JNI。考虑到 Zstd JNI 的单元测试是由 Scala 编写的,包管理器使用到 SBT,在 Zstd JNI 的 Git 提供 nativeTest 是一件不够合理的事情,因为 GraalVM Native Build Tools 0.9.20 只提供 Maven Plugin 和 Gradle Plugin。
一个思路是采用 Gradle 的 Scala Gradle Plugin ,但通过 native-image-agent 为 Zstd JNI 采集这种情况下的 GraalVM Reachability Metadata 时,采集到的 GraalVM Reachability Metadata 全为空。从这个角度出发,重构了一部分 luben karavelov 为 Zstd JNI 编写的单元测试,从 Scala 重写为 Java,以采集真正需要的 GraalVM Reachability Metadata,因此相关 PR 涉及到的单元测试并不全面。
针对 com.google.protobuf:protobuf-java-util
和 org.opengauss:opengauss-jdbc
的 PR 涉及到 ShardingSphere 对 Protobuf Java 和 OpenGauss JDBC 的依赖。OpenGauss JDBC Driver 在 Add support for `org.opengauss:opengauss-jdbc:3.1.0-og`[30] 表现出了与 JSR-310 的不兼容性,这同样导致了 nativeTest 未能测试完整的使用情况。
对 com.hazelcast:hazelcast
的 PR 草案最初围绕 ShardingSphere Proxy 对 Vertx Core 的依赖进行。
io.vertx.core.Vertx#clusteredVertx
需要用到 hazelcast 来编写单元测试。随着 Remove Vert.x from ShardingSphere-Proxy[31] 将 Project Vert.x 从 ShardingSphere 主分支移出,此 PR 提交的 metadata 对 ShardingSphere 不再具有意义。
从 Guava 的内置缓存类被取消使用开始,ShardingSphere 使用 Caffeine 作为缓存。围绕 javax.cache:cache-api
com.github.ben-manes.caffeine:caffeine
在 GraalVM Reachability Metadata 中央仓库的 PR 围绕此点开展。
org.apache.commons:commons-dbcp2
在 ShardingSphere 一侧用于验证 Commons DBCP2 的 ShardingSphere Metadata 类,同样有必要为其提交对应的 GraalVM Reachability Metadata。
ShardingSphere JDBC Core 的 Cluster Mode 存在 Zookeeper,Etcd,Nacos,Consul 四种 org.apache.shardingsphere.mode.repository.cluster.ClusterPersistRepository
的 SPI 实现。
撇开 Nacos,Consul 两种作为可选插件的依赖,与 Zookeeper Server 的交互由 Apache Curator 完成,与 Etcd 的交互由 Jetcd Core 完成,这就涉及到
io.etcd:jetcd-core
org.apache.curator:curator-*
在 GraalVM Native Image 下运行与 org.apache.curator:curat
or-*
相关的单元测试涉及到 ZOOKEEPER-4460[32], 只有包含 ZOOKEEPER-4460 的 Zookeeper Server 和 Zookeeper Client 才能够在 GraalVM Native Image 下运行。
就目前而言,2023 年 1 月 30 日完成 release 的 Apache Zookeeper 3.8.1 是 Zookeeper 历史上最让人振奋的版本之一,这是第一个能够在 GraalVM Native Image 下运行嵌入式 Zookeeper Server 的 Zookee[33] 被验证。
org.apache.shardingsphere.elasticjob:elasticjob-lite-core
执行 nativeTest 的前置障碍,与 ShardingSphere ElasticJob Lite Core 相关的 GraalVM Reachability Metadata 首先位于[34]。#24177 的发现进一步表明,cglib 会阻止 GraalVM Tracing Agent 的使用,这使得更新 ShardingSphere 在单元测试中使用到的,依赖于 cglib 的旧版本 Seata Client 变的合理。
关于 Espresso
Espresso 在 ShardingSphere 中的使用
引出 GraalVM Truffle Framework。在客观上,Truffle 语言实现框架 (Truffle) 是一个开源库,用于构建工具和编程语言实现作为 self-modifying Abstract Syntax Trees 的解释器。Truffle 与开源的 Graal compiler 一起代表了当前 dynamic languages 时代编程语言实现技术的重大进步。在狭义上,我们可以认为实现了 Truffle API 的 Truffle Language,都能够在 GraalVM Native Image 下运行,因为这实际上是实现 Truffle API 的 Language 执行单元测试的必要步骤。
Java on Truffle 是 Java Virtual Machine Specification、 Java SE 8 和 Java SE 11 的实现,构建在 GraalVM 之上作为 Truffle 解释器。它是一个缩小的 Java VM,包括 VM 的所有核心组件,实现与 Java Runtime Environment library ( libjvm.so ) 相同的 API,并重用 GraalVM 中的所有 JAR 和 native libraries。对于此组件所在项目的 GraalVM 社区分支,OracleLabs 将其命名为 Espresso ,它是一个 Java 在 GPL V2 LICENSE 下的 Truffle 实现。这使得,在 GraalVM Native Image 下,调用 GroovyShell,Apache Calcite,JShell 等涉及到 Invokedynamic 或 Runtime Bytecode Generation 相关操作的类成为现实。OracleLabs 为此做过一个 Example[35],espresso-jshell 即是所谓的混合 AOT + JIT 的应用。
从 ShardingSphere 的角度出发,几乎所有对 GroovyShell 的调用都来自[36] 一类。Espresso 的有趣之处就在于,这是在 Java 上实现的 Java( Java on Java),Host JVM 进程与 Guest JVM 进程的交互只通过 UPL LICENSE 的 Truffle API 进行交互。
这并没有在 Maven 中引入任何 GPL LICENSE 的依赖,而是通过为 GraalVM CE 安装 Espresso 组件来实现的。根据[37] 的澄清,Espresso 目前并没有独立于 GraalVM CE ,而只通过 Maven 依赖运行的能力。
从实用的角度不应认为需要考虑用 Truffle Espresso 运行完整的 ShardingSphere Proxy 的 JAR。而是将对 InlineExpr
essionParser
的调用,转移到 Truffle API 的 org.graal
vm.polyglot.Context
的类实例当中,并指定由 Espresso 在 Guest JVM 进程处理。考虑拆分此类到单独的 Maven 模块:
这就是[38] 引入的org.apache.shardingsphere:shardingsphere-infra-util-groovy
子模块,此模块并不需要涉及任何与 Truffle API 相关的内容。
在原有的 org.apache.shardingsphere:shardingsphere-infra-util
中,当检测到 java.vm.name
为 Substrate VM
时,将代码逻辑走入 org.apache.shardingsphere.infra.
util.expr.EspressoInlineExpressionParser
的类处理,否则代码逻辑走入 org.apache.shardingsphere.infra.util.groovy.expr.HotspotInlineExpressionParser
。EspressoInlineExpressionParser
当中时:
会取出在此模块的 pom.xml
定义的 org.apache.maven.plugins:maven-dependency-plugin
中的 execution 复制到 ${project.build.outputDirectory}/espresso-need-libs
的 JAR 文件作为 Espresso 涉及到的 org.graalvm.polyglot.Context
的 Classpath,来创建 Truffle Framework 的 static 实例。
作为一个不足点,由于暂时不能提前 mvn install
真正需要的 org.apache.shardingsphere:shardingsphere-infra-util-groovy
模块,只能临时使用旧版本的 ShardingSphere 的 JAR。在使用 Espresso 涉及到的 Truffle API 时,还需要一个额外的 JVM 实例,例如:
public class EspressoInlineExpressionParser {
static {
String javaHome = System.getenv("JAVA_HOME");
if (null == javaHome) {
throw new RuntimeException("Failed to determine the system's environment variable JAVA_HOME!");
}
URL resource = Thread.currentThread().getContextClassLoader().getResource("espresso-need-libs");
assert null != resource;
String dir = resource.getPath();
String javaClasspath = String.join(":", dir + "/groovy.jar", dir + "/guava.jar", dir + "/shardingsphere-infra-util.jar");
POLYGLOT = Context.newBuilder().allowAllAccess(true)
.option("java.Properties.org.graalvm.home", javaHome)
.option("java.MultiThreaded", "true")
.option("java.Classpath", javaClasspath)
.build();
}
}
当处于 GraalVM Native Image 下时,对 GroovyShell 相关类的大部分调用被转移到 Espresso 的 Guest JVM 进程,对于一个 GraalVM Native Image,通过传入 --language:java
的 buildArg 来将 Truffle Espresso 带入 GraalVM Native Image 当中。
在理想情况下,只需要普通的 Hotspot JVM 就能运行此 GraalVM Native Image,但由于 Native image + espresso, espressoHome not defined & internal GraalVM error[39] 尚未被解决,Espresso 会需要在 JAR 或 GraalVM Native Image 外寻找 Espresso 的固有文件,如果找不到就会认为 espressoHome
未定义。
这涉及到通过 GraalVM Updater 安装 Espresso 组件后,位于:$JAVA_HOME/languages/java/lib
的 6 个文件。在常规的 Hotspot JVM 下,这些文件并不存在等价物。
这也是目前 ShardingSphere Proxy Native 的 Dockerfile 需要先下载 GraalVM CE,然后安装 Espresso 的唯一原因。
此处的 JAVA_HOME
在 ShardingSphere 的模块中,会被设置为 EspressoInlineExpressionParser
的 org.graalvm.polyglot.Context
实例的 java.Properties.org
.graalvm.home
属性。
FROM alpine AS prepare
ARG APP_NAME
ENV LOCAL_PATH /opt/shardingsphere-proxy-native
ADD target/${APP_NAME}.tar.gz /opt
RUN mv /opt/${APP_NAME} ${LOCAL_PATH}
FROM oraclelinux:9-slim
MAINTAINER ShardingSphere "dev@shardingsphere.apache.org"
ARG NATIVE_IMAGE_NAME
ENV LOCAL_PATH /opt/shardingsphere-proxy-native
ENV JAVA_HOME "/opt/graalvm-ce-java17-22.3.1"
ENV PATH "$JAVA_HOME/bin:$PATH"
RUN microdnf install gzip -y && \
bash <(curl -sL https://get.graalvm.org/jdk) --to "/opt" -c espresso graalvm-ce-java17-22.3.1 && \
$JAVA_HOME/bin/gu remove native-image && \
microdnf clean all
COPY --from=prepare ${LOCAL_PATH} ${LOCAL_PATH}
ENTRYPOINT ${LOCAL_PATH}/${NATIVE_IMAGE_NAME} 3307 ${LOCAL_PATH}/conf "0.0.0.0" false
在使用 Truffle Espresso 的情况下,当 ShardingSphere Proxy Native 调用普通类时,会在 AOT 的环境下调用,而当调用到 Espresso 交互的类,会在 Truffle Espresso 的 Guest JVM 进程,以 JIT 的环境执行此类被限定在 Guest JVM 处理的内容。
Guest JVM 不能向 Host JVM 传回 Truffle API 不允许的第三方库的类实例,对于 Groovy 的 Script 类,只能操作 Truffle API 的 Value 类。这进一步加大了此类上下文操作的理解难度,也导致 org.apache.shardingsphere.infra.util.expr.InlineExpressionParser#evaluateClosure
在 GraalVM Native Image 下依然不可用,因为此 method 需要返回 groovy.lang.Closure
。
对 quarkus-shardingsphere-jdbc 的意义
在目前,这些工作不会对 Shardingsphere JDBC 的 Quarkus Extension[40] 有太大帮助,因为 ShardingSphere 的子模块并不像 netty/netty[41] 那样,有自托管的 GraalVM Reachability Metadata 的计划,因为这涉及到近百个模块的 Reachability Metadata,需要定期维护超过 500 个 JSON 文件的更新,提交这部分 JSON 文件到 GraalVM Reachability Metadata 中央仓库在当前是更合理的选择。
由于是在编译 GraalVM Native Image 阶段才使用到了标准 GraalVM 的 Truffle Espresso 组件,Truffle Espresso 并不存在于 ShardingSphere JDBC Core 的产物中,这对 Quarkus 应该是个麻烦。Quarkus 的 Maven Plugin/Gradle Pliugin 默认使用的是 GraalVM 的下游发行版 Mandrel[42],此下游发行版只专注于 GraalVM 的 native-image 组件。这同理可以延申到 Spring Boot 可能遇到的问题,因为 Spring Boot 的 Maven Plugin/Gradle Pliugin 默认使用的是 GraalVM 的下游发行版 Liberica Native Image Kit,这同样是一个只专注于 GraalVM 的 native-image 组件的下游发行版。
Quarkus 的 Maven Plugin/Gradle Pliugin 并没有与 GraalVM Native Build Tools 完成集成,目前没有太多获取 GraalVM Reachability Metadata 中央仓库的 JSON 文件的方法。Max Rydahl Andersen 在 Quarkus 一侧的想法[43] 能在一定程度上佐证 Quarkus 社区的想法。
Max Rydahl Andersen: We discussed it in past and it is possible to integrate it but the metadata we've thus far seen published been very ineffecient and resulting in too High RSS and/or faulty execution. Plus they don't work standalone as can't have all metadata published. Thus we haven't had a good reason to use it as it would mean we get inefficient usage of the libraries. If you find libraries that has usable metadata that provides value we can look again.
从另一个角度出发,2023 年 2 月 9 日 Christian Wirth 在 GraalVM Slack 社群的观点也能在一定方面上展开想象。如果 GraalVM Reachability Metadata 中央仓库的发布速度与 Windows Package Manager Community Repository[44] 对标,降低 Max Rydahl Andersen 提到的 RSS 并不是一件难事。目前从 ShardingSphere 社区一侧提交到 GraalVM Reachability Metadata 的 PR,处理速度并不快。
Ling Hengqian: Hi Team, I would like to ask if anyone can review the PRs of https://github.com/oracle/graalvm-reachability-metadata ? Maintaining 6 branches updated at the same time is a struggle for me. Some of my local unit tests must also depend on the metadata of some existing PR.
Christian Wirth: Hi, thanks for your contributions! We get quite some PRs for the repo lately and are still working on how we can streamline the process while still doing good reviews of the content. Having small PRs helps our engineers review the PRs. I see that some your PRs are rather large, 50 files or more. While it is great to have extensive testing included (we want & need that), we also need to work through all the files and understand their content & dependencies. So having concise PRs would definitely simplify our job and speed up reviews. But again, we want to have tests, so that is always a tradeoff.
在很大程度上,这些工作是由于不能保证 Java 库的百分百单元测试覆盖率而做的措施 -- 没有什么库能做到这一点,如果 ShardingSphere 做到百分百的单元测试率,仅凭子模块的单元测试采集到的所有 Standard Metadata 就可能保证 ShardingSphere Proxy Native 的运行。在 ShardingSphere 外围,Mockito Inline Plugin 尚未能够在 GraalVM Native Image 下直接使用,这使得 ShardingSphere 大部分与 Mockito 的单元测试都不能直接在 GraalVM Native Image 下运行。而 GraalVM Reachability Metadata 中央仓库只允许 Conditional Metadata,这通常使得需要更大的精力来准备 PR,因为提交某个第三方库的 Reachability Metadata 的 PR 会需要考虑更多的第三方库。
未来规划
从宏观来看,需要找到一种可持续性的方法来为 ShardingSphere Proxy 生成 Standard 形态的 GraalVM Reachability Metadata,并将涉及到的子模块与第三方库的 Reachability Metadata 尽可能的转换为 Conditional 形态的 GraalVM Reachability Metadata,以提交到 GraalVM Reachability Metadata 中央仓库,对此的早期测试位于 Initialize the generateStandardMetadata Maven Profile of all module[45]。ShardingSphere 需要的基础的第三方库的 GraalVM Reachability Metadata 和对应的 nativeTest 已被处理的进展如下。
对于 ShardingSphere 依赖树上的依赖的 GraalVM Reachability Metadata,这不一定是个短期内能彻底解决的问题。如对于 com.alibaba:transmittable-thread-local:2.14.2
,为其单独提交 Conditional 形态的 GraalVM Reachability Metadata 意味着必须把所有 Kotlin 相关的单元测试重构为 Java 的单元测试。
根据[46] 的调查,io.kotest:kotest-runner-junit5-jvm:5.5.4
无法在 GraalVM Native Image 下使用。同时对于 Kotlin,Kotest 不允许通过 uniqueId 运行测试 ( Kotest does not allow running tests via uniqueId )。
GraalVM Truffle Espresso 并非万能,通过此手段改动所有相关的算法类是不合理的,这是徒劳的熵增,由于在 classpath 的 resources 包含了额外的 JAR,这会导致项目的编译产物的体积的增大。
应该使用 UPL LICENSE 的 GraalVM JS 的 Maven 依赖来提供基于 JavaScript Shell 的替代品算法,来在 GraalVM Native Image 下取代对 Groovy Shell 的调用。更为核心的是,着手解决[47],当前 ShardingSphere 的 ActualDataNode 只能使用 Apache Groovy 编写,不提供 SPI 的前提下,这为 ShardingSphere Proxy Native 带来了限制。
GraalVM Native Build Tools 0.9.20 为 Maven Plugin 提供了 Gradle Plugin 上的 Agent Mode 功能,新的 native:metadata-copy goal 使得可能不需要 shell 脚本来转化 maven target 文件夹中的 GraalVM Reachability Metadata 到某个特定文件夹当中。但显然,在[48] 的发现临时中断了这个过程。
笔者会在未来的文章中,讨论如何直接采集 GraalVM Reachability Metadata 来使用和分析 Apache ShardingSphere Proxy Native。
References 🔗
Ling Hengqian: Make ShardingSphere Proxy in GraalVM Native Image form available,
https://github.com/apache/shardingsphere/issues/21347
Douglas Simon: Call for Discussion: New Project: Galahad, https://mail.openjdk.org/pipermail/discuss/2022-December/006164.html
GraalVM: GraalVM JDK Downloader, https://github.com/graalvm/graalvm-jdk-downloader
GraalVM: Getting Started: Prerequisites, https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites
korandoru: TruffleBF, https://github.com/korandoru/trufflebf
Tuyen: Graalvm native image cause with Groovy 4, https://github.com/apache/shardingsphere/issues/17779
Paul King: CLONE - Consolidation of VMPlugin didn't account for API calls in the Groovy runtime, https://issues.apache.org/jira/browse/GROOVY-10643
Open Source at Oracle: GroovySubstitutions.java, https://github.com/oracle/graal/blob/vm-ce-22.3.1/substratevm/src/com.oracle.svm.polyglot/src/com/oracle/svm/polyglot/groovy/GroovySubstitutions.java
Jean-Noël Rouvignac: GroovySubstitutions fails with groovy 4, https://github.com/oracle/graal/issues/4492
InfoQ: GraalVM 22.2 添加库配置仓库功能
GraalVM: Native Image Basics, https://www.graalvm.org/22.3/reference-manual/native-image/basics/
GraalVM: Collect Metadata with the Tracing Agent, https://www.graalvm.org/22.3/reference-manual/native-image/metadata/AutomaticMetadataCollection/
The Apache Software Foundation: JobInstance.java, https://github.com/apache/shardingsphere-elasticjob/blob/3.0.2/elasticjob-infra/elasticjob-infra-common/src/main/java/org/apache/shardingsphere/elasticjob/infra/handler/sharding/JobInstance.java
Matt Raible: Use GitHub Actions to Build GraalVM Native Images, https://developer.okta.com/blog/2022/04/22/github-actions-graalvm
Jonathan Grenda: Native Minecraft Servers with GraalVM Native Image, https://medium.com/graalvm/native-minecraft-servers-with-graalvm-native-image-1a3f6a92eb48
Fabio Niephaus: We're actually working on compression for @GraalVM Native Image, https://twitter.com/fniephaus/status/1596884943564836864
Ling Hengqian: Build GraalVM Native Image nightly for ShardingSphere Proxy, https://github.com/apache/shardingsphere/pull/21109
Ling Hengqian: Using Groovy classes under native-image results in UnsupportedFeatureError, https://github.com/oracle/graal/issues/5522
The Apache Software Foundation: shardingsphere-proxy-native, https://github.com/apache/shardingsphere/pkgs/container/shardingsphere-proxy-native
Rafael Winterhalter: Support subclass mocks on Graal VM., https://github.com/mockito/mockito/pull/2613
Julian Hyde: Immutable beans, powered by reflection, https://issues.apache.org/jira/browse/CALCITE-3328
Immutables: Immutables, https://immutables.github.io/
Open Source at Oracle: graalvm-reachability-metadata/metadata/com.h2database/h2
/2.1.210, https://github.com/oracle/graalvm-reachability-metadata/tree/0.2.6/metadata/com.h2database/h2/2.1.210
Sam Brannen: Provide built-in support for GraalVM native images, https://github.com/junit-team/junit5/issues/3040
Ling Hengqian: Add support for io.etcd:jetcd-core:0.7.5, https://github.com/oracle/graalvm-reachability-metadata/pull/170
Janne Valkealahti: Support testing macos/windows, https://github.com/oracle/graalvm-reachability-metadata/issues/24
Open Source at Oracle: graalvm-reachability-metadata/metadata/ch.qos.logback
/logback-classic, https://github.com/oracle/graalvm-reachability-metadata/tree/0.2.6/metadata/ch.qos.logback/logback-classic
Sébastien Deleuze: Introduce a default-for attribute, https://github.com/oracle/graalvm-reachability-metadata/issues/62
Vojin Jovanovic: Remove index.json from the metadata folder, https://github.com/oracle/graalvm-reachability-metadata/issues/4
Ling Hengqian: Add support for org.opengauss:opengauss-jdbc:3.1.0-og, https://github.com/oracle/graalvm-reachability-metadata/pull/168
吴伟杰: Remove Vert.x from ShardingSphere-Proxy, https://github.com/apache/shardingsphere/pull/22982
Alan Bateman: QuorumPeer overrides Thread.getId with different semantics, https://issues.apache.org/jira/browse/ZOOKEEPER-4460
Ling Hengqian: Add support for org.apache.curator:curator-client:5.4.0 and org.apache.curator:curator-framework:5.4.0, https://github.com/oracle/graalvm-reachability-metadata/pull/219
Ling Hengqian: Add support for org.apache.shardingsphere.elasticjob:elasticjob-lite-core:3.0.2, https://github.com/oracle/graalvm-reachability-metadata/pull/208
GraalVM on GitHub: espresso-jshell, https://github.com/graalvm/graalvm-demos/tree/317ae53b070ba5742098a1ba6e40232daf1cd4e4/espresso-jshell
The Apache Software Foundation: InlineExpressionParser.java, https://github.com/apache/shardingsphere/blob/5.3.1/infra/util/src/main/java/org/apache/shardingsphere/infra/util/expr/InlineExpressionParser.java
Ling Hengqian: Truffle Espresso and Truffle JS behave differently under Hotspot, https://github.com/oracle/graal/issues/5850
Ling Hengqian: Introduce Truffle Espresso to make GroovyShell available under GraalVM Native Image, https://github.com/apache/shardingsphere/pull/23873
Alexander Cramb: Native image + espresso, espressoHome not defined & internal GraalVM error, https://github.com/oracle/graal/issues/4555
Quarkiverse Hub: Quarkus - Shardingsphere JDBC Extension, https://github.com/quarkiverse/quarkus-shardingsphere-jdbc
The Netty Project: Netty Project, https://github.com/netty/netty
GraalVM: Mandrel, https://github.com/graalvm/mandrel
Max Rydahl Andersen: Investigate graalvm-reachability-metadata tools, https://github.com/apache/camel-quarkus/issues/4326#issuecomment-1341633222
Microsoft: Windows Package Manager Community Repository, https://github.com/microsoft/winget-pkgs
Ling Hengqian: Initialize the generateStandardMetadata Maven Profile of all module, https://github.com/apache/shardingsphere/pull/24271
Ling Hengqian: Add support for com.alibaba:transmittable-thread-local:2.14.2, https://github.com/oracle/graalvm-reachability-metadata/issues/201
Ling Hengqian: Make actualDataNodes expose SPI that can define expression with custom rules and add GraalVM Truffle implementation, https://github.com/apache/shardingsphere/issues/22899
Ling Hengqian: For Maven Plugin 0.9.20, the native:metadata-copy task cannot be executed normally, https://github.com/graalvm/native-build-tools/issues/403
文章转载自公众号: ShardingSphere官微