基于 GraalVM 的 ShardingSphere Proxy Native 探索

作者简介:ShardingSphere Contributor,何其恒,自2021 年开始为项目贡献小的改进。专注于添加现有模块对 GraalVM Native-Image 的第一方支持与 ShardingSphere 的分片算法类改进。目前,他正在为现有模块的依赖树处理所需的 GraalVM 可达性元数据,并完成项目对 GraalVM Native Build Tools 的集成。


前言

笔者以 Make ShardingSphere Proxy in GraalVM Native Image form available[1] 的主要推动者的上下文身份,旨在通过本文,介绍 GraalVM Native Image 形态的 Apache ShardingSphere Proxy 的起源。

为 Apache ShardingSphere Proxy 创建 GraalVM Native Image 的首要目标,是相对于通过 JVM 启动的 Proxy,实现更快速的启动和更低的资源占用。GraalVM 的 native-image tool 支持将 Java 应用程序通过 ahead-of-time(AOT) 编译为 native executables 或 shared libraries。

传统上,Java 代码是在运行时通过 just-in-time(JIT) 编译的,而 AOT 编译有两个主要优点:首先,它缩短了启动时间,因为代码已经预编译成高效的机器代码。其次,它减少了 Java 应用程序的内存占用,因为它无需包含在运行时加载和优化代码的基础结构。还有其他优势,例如更可预测的性能和更少的 CPU 总使用率。

在 Spring,Quarkus,Micronaut 和 Helidon 等 Web Framework 一侧,此工作的最终目标应当允许 ShardingSphere JDBC Core 及已有可选插件的 SPI 实现的相关依赖可在对应的 Framework 的生态下的 GraalVM Native Image 中使用,以在 ShardingSphere 的混合部署架构下提供同一级别的支持。

这同样为在 ShardingSphere 的 SPI 实现中,GraalVM Truffle Framework 的 Language 实现使用提供性能改善。展望未来,与 GraalVM 生态交互有助于为 OpenJDK 社区的 Project Galahad[2] 的成果做准备。在引出 How to make ShardingSphere Proxy in GraalVM Native Image form available 之前,笔者需要引入一系列 GraalVM 的前置定义,以确保上下文的语义一致。

初识 GraalVM

在 Ubuntu 22.04 的最小安装实例中完成进一步阐释。对于一个全新的 Ubuntu 实例,可以通过 GraalVM JDK Downloader[3] 来完成 GraalVM 的安装,但考虑到更常见的场景,使用者通常需要一个标准的 Hotspot JVM 来作为验证,使用 SDKMAN! 实际是预期行为,当前上下文以 Microsoft OpenJDK 17 作为参照物。

通过 Linux 下的 SDKMAN! 安装 GraalVM CE 22.3.1 首先获得的是 GraalVM just-in-time (JIT) compiler 。这是一个完整的 JVM 实现,任何针对 Hotspot JVM 能实现的事件,在现实场景下都能对其进行,包括但不限于JVM Tool Interface,即 JVMTI 形态的 javaagent 等在 native-image 组件(在 GraalVM 保护伞下被称为 substratevm)和 espresso 组件中不可使用的事件。

在 GraalVM 23.0 CE 之前,由 GraalVM 的 native-image 组件并不由 GraalVM CE 本体直接携带,需要单独通过 GraalVM Updater 安装。由于 Ubuntu 的最小安装实例未默认安装 zip 等组件,还需要手动安装 SDKMAN! 的安装过程中需要的中间依赖,即 unzip ,zip,curl 和 sed。

此外需要额外安装与 local toolchain 相关的系统依赖,各操作系统需要的依赖均在 native-image 的 Reference Manual 的 Prerequisites 一节[4] 被指出。在部分 Ubuntu 版本中,libz-dev 实际不会被安装,而是被 zlib1g-dev 覆盖。

cd /tmp/
sudo apt install unzip zip curl sed -y
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 22.3.1.r17-grl
sdk install java 17.0.6-ms
sdk default java 22.3.1.r17-grl
sdk use java 22.3.1.r17-grl
gu install native-image
sudo apt-get install build-essential libz-dev zlib1g-dev -y

GraalVM CE 本体包含被称为 GraalVM Updater 的命令行工具,在执行 SDKMAN! 的 sdk use java 22.3.1.r17-grl的 bash 命令后,这将作为 $JAVA_HOME/bin/gu 暴露使用。

linghengqian@DESKTOP-TEST:~/.sdkman/candidates/java/22.3.1.r17-grl/bin$ pwd
/home/linghengqian/.sdkman/candidates/java/22.3.1.r17-grl/bin
linghengqian@DESKTOP-TEST:~/.sdkman/candidates/java/22.3.1.r17-grl/bin$ ls
gu         java     javap     jdb        jfr     jinfo  jmod      jrunscript  jstat    native-image            rebuild-images
jar        javac    jcmd      jdeprscan  jhsdb   jlink  jpackage  jshell      jstatd   native-image-configure  rmiregistry
jarsigner  javadoc  jconsole  jdeps      jimage  jmap   jps       jstack      keytool  polyglot                serialver

借助于 GraalVM Updater, 使用者能够接触位于 GraalVM Organization 上所有可安装的组件。其还可通过命令行安装其他本地下载的组件,如 TruffleBF[5],这是 GraalVM Truffle 上的一个 Brainfuck 实现。对于 native-image不支持的架构,可通过 native-image-llvm-backend 的 LLVM 后端自行完成 riscv64 等架构的实现。

linghengqian@DESKTOP-TEST:~$ gu available
Downloading: Component catalog from www.graalvm.org
ComponentId              Version             Component name                Stability                     Origin
---------------------------------------------------------------------------------------------------------------------------------
espresso                 22.3.1              Java on Truffle               Supported                     github.com
espresso-llvm            22.3.1              Java on Truffle LLVM Java librSupported                     github.com
js                       22.3.1              Graal.js                      Supported                     github.com
llvm                     22.3.1              LLVM Runtime Core             Supported                     github.com
llvm-toolchain           22.3.1              LLVM.org toolchain            Supported                     github.com
native-image             22.3.1              Native Image                  Early adopter                 github.com
native-image-llvm-backend22.3.1              Native Image LLVM Backend     Early adopter (experimental)  github.com
nodejs                   22.3.1              Graal.nodejs                  Supported                     github.com
python                   22.3.1              GraalVM Python                Experimental                  github.com
R                        22.3.1              FastR                         Experimental                  github.com
ruby                     22.3.1              TruffleRuby                   Experimental                  github.com
visualvm                 22.3.1              VisualVM                      Experimental                  github.com
wasm                     22.3.1              GraalWasm                     Experimental                  github.com

对于 Apache ShardingSphere Proxy 的 Native Image 化处理,与将 ElasticSearch Server, Apache Kafka Server 和 Apache Zookeeper Server 等中间件实现为 GraalVM Native Image 的处理并没有什么相对较大的差别。这要求找到这些中间件的 mainclass 和对应的 main method,作为目标入口点打包成 GraalVM Native Image。对于 main method 的参数,将作为最终产物的命令行参数被传入。考虑到大多数 Java 中间件在非 Docker Image 环境下的通常启动方式,一般共识认为可以通过 GraalVM Native Image,将 Java 中间件的 Server 端以 CLI 的二进制文件分发。

在 Apache ShardingSphere 社区,最早被记录的为 ShardingSphere JDBC Core 构建 GraalVM Native Image 的 issue 是 Tuyen 报告的 Graalvm native image cause with Groovy 4[6],它引出了将 Apache ShardingSphere Proxy 打包成 GraalVM Native Image 的表层问题,Groovy 并不是 GraalVM Native Image 上的一等公民。Tuyen 认为构建 GraalVM Native Image 失败的原因是 ShardingSphere 的主分支将 Groovy 版本从 V3 切换到 V4 。从 Paul King 调查的 GROOVY-10643[7] 的结果上看,Groovy 4.0.3 解决了问题,但与 IndyInterface 的相关弃用类不是 ShardingSphere 需要关注的主要点。这个问题实际上还可以通过更新 GroovySubstitutions 的类内容[8] 来解决。

为了讨论 Apache ShardingSphere Proxy 在构建 GraalVM Native Image 时遇到的问题,必须引入 GraalVM 对 Reachability Metadata 的定义。对于 GROOVY-10643 涉及到的 GroovySubstitutions fails with groovy 4[9],这里面 Paul King 额外配置了 GraalVM Reachability Metadata 的 reflect-config 部分。InfoQ 的译者在GraalVM 22.2 添加库配置仓库功能[10] 把 GraalVM Reachability Metadata 一词翻译为 GraalVM 可达性元数据 ,这不一定是一个容易理解的翻译,因为 reachable 在 GraalVM 的上下文可以和其他词搭配实现更丰富的语义。

Native Image 是用 Java 编写的,将 Java 字节码作为输入来生成 standalone binary( executable 或 shared library )。针对 Native Image 的初级概念,可直接阅读 Native Image Basics[11]。

基于 GraalVM 的 ShardingSphere Proxy Native 探索_第1张图片

JVM 的动态语言特性(包括反射和资源处理)在运行时计算 dynamically-accessed program elements,例如调用的方法或资源 URL。$JAVA_HOME/bin/native-image 工具在构建 native binary 时执行 static analysis 以确定这些动态特征,但它不能总是详尽地预测所有用途。

为确保将这些 elements 包含到 native binary 中,应该向 native-image builder 提供 reachability metadata( 在 GraalVM 文档中往往简称为 metadata )。为 builder 提供 reachability metadata 还可以确保在 runtime 与 third-party libraries 的无缝兼容。metadata 可以通过以下方式提供给 native-image builder:

通过构建 native binary 时在代码中计算 metadata 并将所需 elements 存储到 native binary 的 initial heap
通过提供存储在项目目录中的META-INF/native-image//的 JSON 文件

涉及到如何为应用程序自动收集元数据的更多信息,应参阅Collect Metadata with the Tracing Agent[12]。Native Image 接受以下类型的 reachability metadata:

基于 GraalVM 的 ShardingSphere Proxy Native 探索_第2张图片

对于最简单的 GraalVM Reachability Metadata 的形态,以 reflect-config.json 中的 JSON 对象来说,其可表现为如下形态:

{
  "condition":{"typeReachable":"org.apache.shardingsphere.elasticjob.lite.internal.instance.InstanceService"},
  "name":"org.apache.shardingsphere.elasticjob.infra.handler.sharding.JobInstance",
  "allDeclaredFields":true,
  "methods":[
    {"name":"","parameterTypes":[] }, 
    {"name":"setJobInstanceId","parameterTypes":["java.lang.String"] }, 
    {"name":"setServerIp","parameterTypes":["java.lang.String"] }
  ]
},

此 JSON 对象的含义是,当且仅当 org.apache.shardingsphere.elasticjob.lite.internal.instance.InstanceService是 reachable 的时候,为 GraalVM Native Image 注册 org.apache.shardingsphere.elasticjob.infra.handler.sharding.JobInstance的查找的所有 Declared 字段,并且同时注册 org.apache.shardingsphere.elasticjob.infra.handler.sharding.JobInstance 的三个具有特定参数的 method。

当拍摄线程快照时,这条元数据实际上指在特定线程上,InstanceService 调用了,或InstanceService 调用到的其他中间类调用到了 JobInstance 的所有 Declared 字段和其中的三个方法。因此,此处的 typeReachable 属性并不是只有 InstanceService一种选择,可以为其他的类,最终根据 typeReachable 的类考虑是否在 GraalVM Native Image 中注册 JobInstance。这形态上可以理解为当且仅当的定语。

此 JSON 对象对应于 ShardingSphere ElasticJob 的 JobInstance 类[13] 的一条 GraalVM Reachability Metadata。注册的方法由 Project Lombok 生成到字节码中。

package org.apache.shardingsphere.elasticjob.infra.handler.sharding;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.shardingsphere.elasticjob.infra.env.IpUtils;

import java.lang.management.ManagementFactory;

@Getter
@Setter
@EqualsAndHashCode(of = "jobInstanceId")
public final class JobInstance {
    
    public static final String DELIMITER = "@-@";
    
    private String jobInstanceId;
    
    private String labels;
    
    private String serverIp;
    
    public JobInstance() {
        this(IpUtils.getIp() + DELIMITER + ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);
    }
    
    public JobInstance(final String jobInstanceId) {
        this(jobInstanceId, null);
    }
    
    public JobInstance(final String jobInstanceId, final String labels) {
        this(jobInstanceId, labels, IpUtils.getIp());
    }
    
    public JobInstance(final String jobInstanceId, final String labels, final String serverIp) {
        this.jobInstanceId = jobInstanceId;
        this.labels = labels;
        this.serverIp = serverIp;
    }
}

探索之后的收获

ShardingSphere Proxy Native 探索 GraalVM 

在 2022 年带来了什么?

在 GraalVM CE 21.3 推出的 Condition Metadata 条件元数据事实上改变了 GraalVM Native Image 的游戏规则。

在那之前,由于不能定义 Condition Metadata,所有在 JSON 文件定义的 GraalVM Reachability Metadata 都必定包含在最终 GraalVM Native Image 当中,这导致如果一个库被设计成被其他库使用,或被其他第三方库使用,往往需要额外在被称为 native-image.properties 的文件中单独配置 native-image 组件的 buildArgs 构建参数,以避开一些错误地被定义在 build time 或 run time 完成初始化的类,或者涉及到的类并不存在,导致无法正常完成 GraalVM Native Image 的编译。

而 GraalVM Tracing Agent 对采集 Conditional 形态的 Metadata 的支持从 GraalVM CE 22.1 推出。这使得建立一个 GraalVM Reachability Metadata 的中央仓库成为可能。

从 2022 年 7 月的 GraalVM CE  22.2 开始,GraalVM Reachability Metadata 的中央仓库现在位于 https://github.com/oracle/graalvm-reachability-metadata 。更核心的特性是构建 Native Image 的内存占用更小,这使得从 GraalVM CE 22.2 开始,能够在几乎什么都不用配置的情况下,在仅有 7 GB 内存的 Github Actions 设备上构建 GraalVM Native Image。

在 GraalVM CE 22.2 之前,在 Github Actions 设备上构建 GraalVM Native Image 也属于 GraalVM 文档定义的 expert feature 之一。Matt Raible 在 Use GitHub Actions to Build GraalVM Native Images[14] 分享过相关流程。

当在 Github Actions 上以 Linux 构建 GraalVM Native Image 时,首先需要使用  pierotofy/set-swap-space 的  GitHub action 组件增加 Github Actions 设备的 Swap Space,将 swap-size-gb 属性设为 10。

当在 Github Actions 上以 Windows 构建 GraalVM Native Image 时,首先需要使用 al-cheb/configure-pagefile-action 的  GitHub action 组件配置 Github Actions 设备 Pagefile 的 size 和 location,将 minimum-size 设置为 10GB,将 maximum-size 设置为 12GB。

习惯上认为可通过 PowerShell 执行 

(Get-CimInstance Win32_PageFileUsage).AllocatedBaseSize 

来将 Pagefile 的设置打印到 Github Actions 的 Log 上来查看是否正常。

如果什么都不做,内存不足在 Linux 上的体现就是 java.lang.OutOfMemoryError: GC overhead limit exceeded。对于 Windows ,则会遇到 The command line is too long.这类特定于 Windows Native 开发环境的问题 -- WSL 不受此影响,而 GraalVM Native Build Tools 是在 Maven Plugin 和 Gradle Plugin 级别解决了这种问题。这一切目前成为了过去时,下游的使用者不需要继续考虑相关因素。

GraalVM Native Image 已被公开证明可用于闭源的超大型应用,[15] 一文演示了如何通过仅 Minecracft Server 的 JAR 和对应的 GraalVM Reachability Metadata 的 JSON 文件,在 Github Actions 上构建 Minecracft Server 所需要的 GraalVM Native Image,并启用 UPX compression 进一步压缩了此 binary 的文件大小。从大众的观点来看,使用 UPX compression 不一定是个好主意,目前 GraalVM CE 正在 23.0 后的里程碑上准备提供内置的 compression 支持[16],而现在 UPX compression 对真正的 startup time 和 peak RSS 存在负面影响,这往往意味着更高的内存占用。

启动流程

ShardingSphere Proxy 的启动流程

作为验证的一部分,在 Build GraalVM Native Image nightly for ShardingSphere Proxy[17] 中已为 GraalVM Native Image 形态的 ShardingSphere Proxy 设置了在 Github Actions 的每夜构建。

这个 PR 的核心是引入了 GraalVM Native Build Tools ,它提供了 Maven Plugin 和 Gradle Plugin,以避免 GraalVM 的下游使用者不得不在 shell 脚本中必须使用 if-else 等语法拼接长度相对较长的 bash 命令才能使用 $JAVA_HOME/bin/native-iamge 等命令行工具。

首先阐释 Apache ShardingSphere Proxy 是如何编译和运行的。当然,下游使用者大多数情况下只需要关注 Linux 环境。以 ShardingSphere 5.3.1 来看,我们可以执行如下命令来 clone source code 观察。

git clone -b 5.3.1 [email protected]:apache/shardingsphere.git
cd ./shardingsphere/

‍‍

通常的流程是,我们可以在已设置 JAVA_HOME 的环境变量的前提下,执行./mvnw clean install -Prelease -T1C -DskipTests -Djacoco.skip=true -Dcheckstyle.skip=true -Drat.skip=true -Dmaven.javadoc.skip=true -B来跳过单元测试,单元测试覆盖率统计,源代码风格检查,发布审计,生成 javadoc 这些与我们目标不相关的流程,并在 ./distribution/proxy/下生成 Apache ShardingSphere Proxy 的产物。

最终产物是一个 .tar.gz 文件,内有一个包含启动类的 jar,并包含启动此 jar 需要的全部第三方库和 LICENSE,这些打包内容由 distribution/proxy/pom.xml的 release profile 定义。

启动 jar 同样需要执行长度不短的 $JAVA_HOME/bin/java 命令。对于 ShardingSphere Proxy,其具有 distribution/proxy/src/main/resources/bin/start.sh的 shell  脚本文件来简化定义的流程。

SERVER_NAME=ShardingSphere-Proxy

DEPLOY_BIN="$(dirname "${BASH_SOURCE-$0}")"
cd "${DEPLOY_BIN}/../" || exit;
DEPLOY_DIR="$(pwd)"

LOGS_DIR=${DEPLOY_DIR}/logs
if [ ! -d "${LOGS_DIR}" ]; then
    mkdir "${LOGS_DIR}"
fi


STDOUT_FILE=${LOGS_DIR}/stdout.log
EXT_LIB=${DEPLOY_DIR}/ext-lib

CLASS_PATH=".:${DEPLOY_DIR}/lib/*:${EXT_LIB}/*"

if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]];  then
    JAVA="$JAVA_HOME/bin/java"
elif type -p java; then
    JAVA="$(which java)"
else
    echo "Error: JAVA_HOME is not set and java could not be found in PATH." 1>&2
    exit 1
fi

is_openjdk=$($JAVA -version 2>&1 | tail -1 | awk '{print ($1 == "OpenJDK") ? "true" : "false"}')
total_version=$($JAVA -version 2>&1 | grep version | sed '1!d' | sed -e 's/"//g' | awk '{print $3}')
int_version=${total_version%%.*}
if [ "$int_version" = '1' ] ; then
    int_version=${total_version%.*}
    int_version=${int_version:2}
fi
echo "we find java version: java${int_version}, full_version=${total_version}, full_path=$JAVA"

case "$OSTYPE" in
*solaris*)
  GREP=/usr/xpg4/bin/grep
  ;;
*)
  GREP=grep
  ;;
esac

VERSION_OPTS=""
if [ "$int_version" = '8' ] ; then
    VERSION_OPTS="-XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70"
elif [ "$int_version" = '11' ] ; then
    VERSION_OPTS="-XX:+SegmentedCodeCache -XX:+AggressiveHeap"
    if $is_openjdk; then
      VERSION_OPTS="$VERSION_OPTS -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler"
    fi
elif [ "$int_version" = '17' ] ; then
    VERSION_OPTS="-XX:+SegmentedCodeCache -XX:+AggressiveHeap"
else
    echo "unadapted java version, please notice..."
fi

DEFAULT_CGROUP_MEM_OPTS=""
if [ "$int_version" = '8' ] ; then
        DEFAULT_CGROUP_MEM_OPTS=" -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:InitialRAMPercentage=80.0 -XX:MinRAMPercentage=80.0 -XX:MaxRAMPercentage=80.0 "
else
        DEFAULT_CGROUP_MEM_OPTS=" -XX:InitialRAMPercentage=80.0 -XX:MinRAMPercentage=80.0 -XX:MaxRAMPercentage=80.0 "
fi

CGROUP_MEM_OPTS="${CGROUP_MEM_OPTS:-${DEFAULT_CGROUP_MEM_OPTS}}"

JAVA_OPTS=" -Djava.awt.headless=true "

DEFAULT_JAVA_MEM_COMMON_OPTS=" -Xmx2g -Xms2g -Xmn1g "
if [ -n "${IS_DOCKER}" ]; then
        JAVA_MEM_COMMON_OPTS="${CGROUP_MEM_OPTS}"
else
        JAVA_MEM_COMMON_OPTS="${JAVA_MEM_COMMON_OPTS:-${DEFAULT_JAVA_MEM_COMMON_OPTS}}"
fi

JAVA_MEM_OPTS=" -server ${JAVA_MEM_COMMON_OPTS} -Xss1m -XX:AutoBoxCacheMax=4096 -XX:+UseNUMA -XX:+DisableExplicitGC -XX:LargePageSizeInBytes=128m ${VERSION_OPTS} -Dio.netty.leakDetection.level=DISABLED "




MAIN_CLASS=org.apache.shardingsphere.proxy.Bootstrap

unset -v PORT
unset -v ADDRESSES
unset -v CONF_PATH
unset -v FORCE

print_usage() {
    echo "usage:"
    echo "start.sh [port] [config_dir]"
    echo "  port: proxy listen port, default is 3307"
    echo "  config_dir: proxy config directory, default is 'conf'"
    echo ""
    echo "start.sh [-a addresses] [-p port] [-c /path/to/conf]"
    echo "The options are unordered."
    echo "-a  Bind addresses, can be IPv4, IPv6, hostname. In"
    echo "    case more than one address is specified in a"
    echo "    comma-separated list. The default value is '0.0.0.0'."
    echo "-p  Bind port, default is '3307', which could be changed in server.yaml"
    echo "-c  Path to config directory of ShardingSphere-Proxy, default is 'conf'"
    echo "-f  Force start ShardingSphere-Proxy"
    exit 0
}

if [ "$1" == "-h" ] || [ "$1" == "--help" ] ; then
    print_usage
fi

print_version() {
    $JAVA ${JAVA_OPTS} ${JAVA_MEM_OPTS} -classpath ${CLASS_PATH} org.apache.shardingsphere.infra.autogen.version.ShardingSphereVersion
    exit 0
}

if [ "$1" == "-v" ] || [ "$1" == "--version" ] ; then
    print_version
fi

if [ $# == 0 ]; then
    CLASS_PATH=${DEPLOY_DIR}/conf:${CLASS_PATH}
fi

if [[ $1 == -a ]] || [[ $1 == -p ]] || [[ $1 == -c ]] || [[ $1 == -f ]] ; then
    while getopts ":a:p:c:f" opt
    do
        case $opt in
        a)
          echo "The address is $OPTARG"
          ADDRESSES=$OPTARG;;
        p)
          echo "The port is $OPTARG"
          PORT=$OPTARG;;
        c)
          echo "The configuration path is $OPTARG"
          CONF_PATH=$OPTARG;;
        f)
          echo "The force param is true"
          FORCE=true;;
        ?)
          print_usage;;
        esac
    done

elif [ $# == 1 ]; then
    PORT=$1
    echo "The port is $1"

elif [ $# == 2 ]; then
    PORT=$1
    CONF_PATH=$2
    echo "The port is $1"
    echo "The configuration path is $2"
fi

if [ -z "$CONF_PATH" ]; then
    CONF_PATH=${DEPLOY_DIR}/conf
fi

if [ -z "$PORT" ]; then
    PORT=-1
fi

if [ -z "$ADDRESSES" ]; then
    ADDRESSES="0.0.0.0"
fi

if [ -z "$FORCE" ]; then
    FORCE=false
fi

CLASS_PATH=${CONF_PATH}:${CLASS_PATH}
MAIN_CLASS="${MAIN_CLASS} ${PORT} ${CONF_PATH} ${ADDRESSES} ${FORCE}"

echo "The classpath is ${CLASS_PATH}"
echo "main class ${MAIN_CLASS}"

if [ -n "${IS_DOCKER}" ]; then
  exec $JAVA ${JAVA_OPTS} ${JAVA_MEM_OPTS} -classpath ${CLASS_PATH} ${MAIN_CLASS}
  exit 0
fi

echo -e "Starting the $SERVER_NAME ...\c"

nohup $JAVA ${JAVA_OPTS} ${JAVA_MEM_OPTS} -classpath ${CLASS_PATH} ${MAIN_CLASS} >> ${STDOUT_FILE} 2>&1 &
if [ $? -eq 0 ]; then
  case "$OSTYPE" in
  *solaris*)
    pid=$(/bin/echo "${!}\\c")
    ;;
  *)
    pid=$(/bin/echo -n $!)
    ;;
  esac
  if [ $? -eq 0 ]; then
      sleep 1;
      if ps -p "${pid}" > /dev/null 2>&1; then
        echo " PID: $pid"
        echo "Please check the STDOUT file: $STDOUT_FILE"
        exit 0
      fi
  else
    echo " FAILED TO GET PID"
  fi
else
  echo " SERVER DID NOT START"
fi
echo "Please check the STDOUT file: $STDOUT_FILE"
exit 1

不讨论用于中止 ShardingSphere Proxy 运行的 distribution/proxy/src/main/resources/bin/stop.sh 的内容。当前,可以首先假设在 JDK17,非 Docker Image 环境,不启用 ShardingSphere Agent ,不指定 ShardingSphere 的配置文件夹,也不指定额外的命令行参数的前提下来粗略定义最终的启动命令如下,当然考虑到$(pwd)得到的当前路径需要另外定义 export DEPLOY_DIR="$(pwd)",这条命令不能直接使用。

nohup $JAVA_HOME/bin/java -Djava.awt.headless=true -server -Xmx2g -Xms2g -Xmn1g -Xss1m -XX:AutoBoxCacheMax=4096 -XX:+UseNUMA -XX:+DisableExplicitGC -XX:LargePageSizeInBytes=128m -XX:+SegmentedCodeCache -XX:+AggressiveHeap -Dio.netty.leakDetection.level=DISABLED -classpath $(pwd)/conf:.:$(pwd)/lib/*:$(pwd)/ext-lib/ org.apache.shardingsphere.proxy.Bootstrap -1 $(pwd)/conf "0.0.0.0" false >> $(pwd)/logs/stdout.log 2>&1 &

额外撇开 JVM 参数 -- 在构建 GraalVM Native Image 的 buildArg 处理繁琐的 JVM 参数不一定合理,而 GraalVM Native Image 的二进制文件虽然能够使用 JVM 参数,但显然没那么多,这条命令可以做简化。

java -classpath $(pwd)/conf:.:$(pwd)/lib/*:$(pwd)/ext-lib/ org.apache.shardingsphere.proxy.Bootstrap -1 $(pwd)/conf "0.0.0.0" false

这实际就是在构建完 GraalVM Native Image 形态的 ShardingSphere Proxy 之后,需要考虑的命令行参数的主要内容, org.apache.shardingsphere.proxy.Bootstrap 是启动类。

我们只需要考虑四个命令行参数,对应为-1的 PORT,对应为$(pwd)/conf的 CONF_PATH,对应为 "0.0.0.0"的 ADDRESSES,对于为false的 FORCE。

org.apache.shardingsphere.proxy.Bootstrap 的内容无需查看, distribution/proxy/src/main/resources/bin/start.sh 为这四个参数标记了说明。PORT 为 ShardingSphere Proxy 的监听端口,CONF_PATH 为 ShardingSphere Proxy 的配置文件所在的绝对路径,ADDRESSES 为 ShardingSphere Proxy 监听的 IP 地址,FORCE 即为 Force Start,如果为 true 则保证 ShardingSphere Proxy Native 无论能否连接都能正常启动。对于 GraalVM Native Image,classpath 的内容不应考虑,GraalVM Native Image 存在 closed-world assumption,构建 GraalVM Native Image 时没使用到的 JAR 之后同样也不应考虑,在下文将引入 GraalVM Truffle Espresso 的讨论。

Apache ShardingSphere Proxy 实际上也是使用 Maven 构建的,其只使用如下的依赖。在 shell 脚本使用到的 org.apache.shardingsphere.proxy.Bootstrap 类也是在

 org.apache.shardingsphere:shardingsphere-proxy-bootstrap 中定义的。

由 release profile 定义中的 maven-assembly-plugin 的 maven execution 将会依赖特定的要求构建出 .tar.gz 的产物。

基于 GraalVM 的 ShardingSphere Proxy Native 探索_第3张图片


早期处理

使 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.Closuregroovy.lang.GroovyShellgroovy.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.(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-elements-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。

基于 GraalVM 的 ShardingSphere Proxy Native 探索_第4张图片

对于 #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。

基于 GraalVM 的 ShardingSphere Proxy Native 探索_第5张图片

#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] 为例。

基于 GraalVM 的 ShardingSphere Proxy Native 探索_第6张图片

对于 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-utilorg.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:curator-* 相关的单元测试涉及到 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。而是将对 InlineExpressionParser 的调用,转移到 Truffle API 的 org.graalvm.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.HotspotInlineExpressionParserEspressoInlineExpressionParser当中时:

会取出在此模块的 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 下,这些文件并不存在等价物。

基于 GraalVM 的 ShardingSphere Proxy Native 探索_第7张图片

这也是目前 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 "[email protected]"

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 已被处理的进展如下。

基于 GraalVM 的 ShardingSphere Proxy Native 探索_第8张图片

对于 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 
[1]Ling Hengqian: Make ShardingSphere Proxy in GraalVM Native Image form available,
https://github.com/apache/shardingsphere/issues/21347
[2]Douglas Simon: Call for Discussion: New Project: Galahad, https://mail.openjdk.org/pipermail/discuss/2022-December/006164.html
[3]GraalVM: GraalVM JDK Downloader, https://github.com/graalvm/graalvm-jdk-downloader
[4]GraalVM: Getting Started: Prerequisites, https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites
[5]korandoru: TruffleBF, https://github.com/korandoru/trufflebf
[6]Tuyen: Graalvm native image cause with Groovy 4, https://github.com/apache/shardingsphere/issues/17779
[7]Paul King: CLONE - Consolidation of VMPlugin didn't account for API calls in the Groovy runtime, https://issues.apache.org/jira/browse/GROOVY-10643
[8]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
[9]Jean-Noël Rouvignac: GroovySubstitutions fails with groovy 4, https://github.com/oracle/graal/issues/4492
[10]InfoQ: GraalVM 22.2 添加库配置仓库功能
[11]GraalVM: Native Image Basics, https://www.graalvm.org/22.3/reference-manual/native-image/basics/
[12]GraalVM: Collect Metadata with the Tracing Agent, https://www.graalvm.org/22.3/reference-manual/native-image/metadata/AutomaticMetadataCollection/
[13]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
[14]Matt Raible: Use GitHub Actions to Build GraalVM Native Images, https://developer.okta.com/blog/2022/04/22/github-actions-graalvm
[15]Jonathan Grenda: Native Minecraft Servers with GraalVM Native Image, https://medium.com/graalvm/native-minecraft-servers-with-graalvm-native-image-1a3f6a92eb48
[16]Fabio Niephaus: We're actually working on compression for @GraalVM Native Image, https://twitter.com/fniephaus/status/1596884943564836864
[17]Ling Hengqian: Build GraalVM Native Image nightly for ShardingSphere Proxy, https://github.com/apache/shardingsphere/pull/21109
[18]Ling Hengqian: Using Groovy classes under native-image results in UnsupportedFeatureError, https://github.com/oracle/graal/issues/5522
[19]The Apache Software Foundation: shardingsphere-proxy-native, https://github.com/apache/shardingsphere/pkgs/container/shardingsphere-proxy-native
[20]Rafael Winterhalter: Support subclass mocks on Graal VM., https://github.com/mockito/mockito/pull/2613
[21]Julian Hyde: Immutable beans, powered by reflection, https://issues.apache.org/jira/browse/CALCITE-3328
[22]Immutables: Immutables, https://immutables.github.io/
[23]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
[24]Sam Brannen: Provide built-in support for GraalVM native images, https://github.com/junit-team/junit5/issues/3040
[25]Ling Hengqian: Add support for io.etcd:jetcd-core:0.7.5, https://github.com/oracle/graalvm-reachability-metadata/pull/170
[26]Janne Valkealahti: Support testing macos/windows, https://github.com/oracle/graalvm-reachability-metadata/issues/24
[27]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
[28]Sébastien Deleuze: Introduce a default-for attribute, https://github.com/oracle/graalvm-reachability-metadata/issues/62
[29]Vojin Jovanovic: Remove index.json from the metadata folder, https://github.com/oracle/graalvm-reachability-metadata/issues/4
[30]Ling Hengqian: Add support for org.opengauss:opengauss-jdbc:3.1.0-og, https://github.com/oracle/graalvm-reachability-metadata/pull/168
[31]吴伟杰: Remove Vert.x from ShardingSphere-Proxy, https://github.com/apache/shardingsphere/pull/22982
[32]Alan Bateman: QuorumPeer overrides Thread.getId with different semantics, https://issues.apache.org/jira/browse/ZOOKEEPER-4460
[33]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
[34]Ling Hengqian: Add support for org.apache.shardingsphere.elasticjob:elasticjob-lite-core:3.0.2, https://github.com/oracle/graalvm-reachability-metadata/pull/208
[35]GraalVM on GitHub: espresso-jshell, https://github.com/graalvm/graalvm-demos/tree/317ae53b070ba5742098a1ba6e40232daf1cd4e4/espresso-jshell
[36]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
[37]Ling Hengqian: Truffle Espresso and Truffle JS behave differently under Hotspot, https://github.com/oracle/graal/issues/5850
[38]Ling Hengqian: Introduce Truffle Espresso to make GroovyShell available under GraalVM Native Image, https://github.com/apache/shardingsphere/pull/23873
[39]Alexander Cramb: Native image + espresso, espressoHome not defined & internal GraalVM error, https://github.com/oracle/graal/issues/4555
[40]Quarkiverse Hub: Quarkus - Shardingsphere JDBC Extension, https://github.com/quarkiverse/quarkus-shardingsphere-jdbc
[41]The Netty Project: Netty Project, https://github.com/netty/netty
[42]GraalVM: Mandrel, https://github.com/graalvm/mandrel
[43]Max Rydahl Andersen: Investigate graalvm-reachability-metadata tools, https://github.com/apache/camel-quarkus/issues/4326#issuecomment-1341633222
[44]Microsoft: Windows Package Manager Community Repository, https://github.com/microsoft/winget-pkgs
[45]Ling Hengqian: Initialize the generateStandardMetadata Maven Profile of all module, https://github.com/apache/shardingsphere/pull/24271
[46]Ling Hengqian: Add support for com.alibaba:transmittable-thread-local:2.14.2, https://github.com/oracle/graalvm-reachability-metadata/issues/201
[47]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
[48]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

你可能感兴趣的:(java,apache,ubuntu,jvm,开发语言)