如何在CircleCI上构建支持Graal的JDK8


GraalVM compiler是HotSpot服务器端的替代品JIT compiler,被广泛称为C2 compiler。它是用Java编写的,目标是比C2编译器有更好的性能。新变化,从Java 9,这意味着我们现在可以将自己手写的C2编译器插入到JVM中,这要归功于JVMCI。甲骨文实验室的研究人员和工程师已经创建了一个支持JVMCI的JDK8变体,可以用来构建GraalVM compiler。GraalVM编译器是开源的available on GitHub(随着HotSpot JVMCI sources需要构建GraalVM编译器)。这使我们能够分叉/克隆它,并构建我们自己版本的GraalVM编译器。

在这篇文章中,我们将在CircleCI上用JDK8构建GraalVM编译器。产生的工件将是:

  • JDK8嵌入了GraalVM编译器
  • 包含Graal和Truffle模块/组件的zip存档。

注意:在这篇文章中,我们不涉及如何构建GraalVM套件;那可以在另一篇文章中完成。虽然这些脚本可以用来做到这一点,而且存在一个branch that contains the rest of the steps

为什么要使用配置项工具来构建GraalVM编译器?

持续集成(CI)和持续部署(CD)工具有很多好处。最大的优势之一是检查代码库运行状况的能力。了解构建失败的原因为您提供了更快修复的机会。对于这个项目来说,重要的是我们能够在本地和Docker容器中验证和验证为Linux和macOS构建GraalVM编译器所需的脚本。配置项/光盘工具允许我们添加自动化测试,以确保在合并每个公共关系时,我们从脚本中获得期望的结果。除了确保我们的新代码不会引入突破性的变化,配置项/光盘工具的另一个重要特性是我们可以自动创建二进制文件。这些二进制文件的自动部署使得它们可用于开源分发。

让我们开始吧

在研究过程中CircleCI作为构建GraalVM编译器的配置项/光盘解决方案,我了解到我们可以通过两种不同的方法来运行构建,即:

  • 带有标准Docker容器的CircleCI构建(构建时间更长,配置脚本更长)。
  • 带有预构建和优化的Docker容器的CircleCI构建(构建时间更短,配置脚本更短)。

我们现在将讨论上面提到的两种方法,看看它们的优缺点。

方法一:使用标准码头集装箱

对于这种方法,CircleCI需要一个Docker映像,可在Docker Hub或它有权访问的另一个公共/私有注册表。为了成功构建,我们必须在这个可用的环境中安装必要的依赖项。我们希望构建第一次运行的时间更长,并且,根据缓存的级别,它会加快速度。

为了理解这是如何实现的,我们将逐段查看CircleCI配置文件(存储在。circleci/circle.yml)。看见config.yml in .circleci完整列表;参见提交df28ee7源的变化。

解释配置文件的各个部分

配置文件中的下面几行将确保我们安装的应用程序被缓存(指的是两个特定的目录),这样我们就不必在每次构建时都重新安装依赖项:

    dependencies:
      cache_directories:
        - "vendor/apt"
        - "vendor/apt/archives"


我们将引用Docker图像的全名(如http://hub.docker.com在使用的帐户名下—采用openjdk)。在这种情况下,它是一个包含JDK8的标准docker映像,由Adopt OpenJDK build farm。理论上,我们可以使用任何图像,只要它支持构建过程。它将作为基础层,我们将在其上安装必要的依赖项:

        docker:
          - image: adoptopenjdk/openjdk8:jdk8u152-b16 


接下来,在预安装操作系统依赖项步骤中,我们将恢复缓存;如果它已经存在,这可能看起来有点奇怪,但是对于唯一键标签,下面的实现是recommended by the docs

          - restore_cache:
              keys:
                - os-deps-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
                - os-deps-{{ arch }}-{{ .Branch }}


然后,在安装操作系统依赖项步骤中,我们运行相应的外壳脚本来安装所需的依赖项。如果操作需要两分钟以上才能完成,我们已将此步骤设置为超时。(参见docs for timeout):

          - run:
              name: Install Os dependencies
              command: ./build/x86_64/linux_macos/osDependencies.sh
              timeout: 2m


然后,在安装后操作系统依赖步骤中,我们保存上一步的结果,即上面运行步骤中的层。(键名被格式化以确保唯一性,并包括要保存的特定路径):

          - save_cache:
              key: os-deps-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
              paths:
                - vendor/apt
                - vendor/apt/archives


然后,在预构建和通过脚本安装make步骤中,如果缓存已经存在,我们将恢复缓存:

          - restore_cache:
              keys:
                - make-382-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
                - make-382-{{ arch }}-{{ .Branch }}


然后,在通过脚本构建和安装make步骤中,我们运行Shell脚本来安装make的特定版本;如果步骤完成时间超过一分钟,则设置为超时:

          - run:
              name: Build and install make via script
              command: ./build/x86_64/linux_macos/installMake.sh
              timeout: 1m


然后,在通过脚本进行后期构建和安装的步骤中,我们将上述操作的结果保存到缓存中:

          - save_cache:
              key: make-382-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
              paths:
                - /make-3.82/
                - /usr/bin/make
                - /usr/local/bin/make
                - /usr/share/man/man1/make.1.gz
                - /lib/


然后,我们定义环境变量并更新JAVA_HOMEandPATHat运行时。这里,环境变量的来源是为了让我们在接下来的步骤中记住它们,直到构建过程结束(请记住这一点):

          - run:
              name: Define Environment Variables and update JAVA_HOME and PATH at Runtime
              command: |
                echo '....'     <== a number of echo-es displaying env variable values
                source ${BASH_ENV}


然后,在“显示硬件、软件、运行时环境和依赖版本”步骤中,我们显示特定于环境的信息,并将其记录到日志中以备后用。这在调试过程中出错时也很有用:

          - run:
              name: Display HW, SW, Runtime env. info and versions of dependencies
              command: ./build/x86_64/linux_macos/lib/displayDependencyVersion.sh


然后,我们运行设置MX的步骤。从GraalVM编译器的角度来看,这很重要。(MX是一个专门的构建系统,创建它是为了方便编译和构建Graal/GraalVM及其组件。(

          - run:
              name: Setup MX
              command: ./build/x86_64/linux_macos/lib/setupMX.sh ${BASEDIR}


然后,我们运行构建JDK JVMCI;我们在这里构建了启用了JVMCI的JDK。如果过程超过15分钟而没有任何输出,或者如果过程超过20分钟才完成,我们会超时:

          - run:
              name: Build JDK JVMCI
              command: ./build/x86_64/linux_macos/lib/build_JDK_JVMCI.sh ${BASEDIR} ${MX}
              timeout: 20m
              no_output_timeout: 15m


然后,我们运行“运行JDK JVMCI测试”步骤,该步骤在构建JDK JVMCI后运行测试,作为健全性检查的一部分:

          - run:
              name: Run JDK JVMCI Tests
              command: ./build/x86_64/linux_macos/lib/run_JDK_JVMCI_Tests.sh ${BASEDIR} ${MX}


然后,我们运行“设置环境”和“构建GraalVM编译器”,用必要的环境变量来设置构建环境,这些变量将在以下步骤中使用:

          - run:
              name: Setting up environment and Build GraalVM Compiler
              command: |
                echo ">>>> Currently JAVA_HOME=${JAVA_HOME}"
                JDK8_JVMCI_HOME="$(cd ${BASEDIR}/graal-jvmci-8/ && ${MX} --java-home ${JAVA_HOME} jdkhome)"
                echo "export JVMCI_VERSION_CHECK='ignore'" >> ${BASH_ENV}
                echo "export JAVA_HOME=${JDK8_JVMCI_HOME}" >> ${BASH_ENV}
                source ${BASH_ENV}


然后,我们运行以下步骤,构建GraalVM编译器,并将其嵌入JDK (JDK8,启用JVMCI),如果该过程需要超过7分钟而没有任何输出,或者总共需要超过10分钟才能完成,就会超时:

          - run:
              name: Build the GraalVM Compiler and embed it into the JDK (JDK8 with JVMCI enabled)
              command: |
                echo ">>>> Using JDK8_JVMCI_HOME as JAVA_HOME (${JAVA_HOME})"
                ./build/x86_64/linux_macos/lib/buildGraalCompiler.sh ${BASEDIR} ${MX} ${BUILD_ARTIFACTS_DIR}
              timeout: 10m
              no_output_timeout: 7m


然后,我们运行简单的健全性检查工件步骤来验证一旦构建完成就创建的工件的有效性,就在归档工件之前:

          - run:
              name: Sanity check artifacts
              command: |
                ./build/x86_64/linux_macos/lib/sanityCheckArtifacts.sh ${BASEDIR} ${JDK_GRAAL_FOLDER_NAME}
              timeout: 3m
              no_output_timeout: 2m


然后,我们运行步骤“归档工件”(将最终工件压缩并复制到一个单独的文件夹中),如果该过程需要超过两分钟而没有任何输出,或者总共需要超过三分钟才能完成,则该步骤会超时:

          - run:
              name: Archiving artifacts
              command: |
                ./build/x86_64/linux_macos/lib/archivingArtifacts.sh ${BASEDIR} ${MX} ${JDK_GRAAL_FOLDER_NAME} ${BUILD_ARTIFACTS_DIR}
              timeout: 3m
              no_output_timeout: 2m


出于后续和调试目的,我们从各种文件夹中捕获生成的日志,并将其存档:

          - run:
              name: Collecting and archiving logs (debug and error logs)
              command: |
                ./build/x86_64/linux_macos/lib/archivingLogs.sh ${BASEDIR}
              timeout: 3m
              no_output_timeout: 2m
              when: always
          - store_artifacts:
              name: Uploading logs
              path: logs/


最后,我们将生成的工件存储在一个指定的位置——下面几行将使这个位置在CircleCI接口上可用(我们可以从这里下载工件)。

          - store_artifacts:
              name: Uploading artifacts in jdk8-with-graal-local
              path: jdk8-with-graal-local/


方法二:使用预构建的、优化的码头集装箱

对于第二种方法,我们将使用一个预先构建的docker容器,该容器是在本地创建和构建的,具有所有必要的依赖关系,Docker映像被保存,然后被推送到远程注册表。然后,我们将通过配置文件在CircleCI环境中引用这个Docker映像。这为我们节省了时间和精力来运行所有命令来安装必要的依赖项,从而为这种方法创建必要的环境(请参见上一节中的详细步骤)。

我们希望与之前的构建相比,构建运行的时间更短;这是我们将在“构建预构建码头工人图像的步骤”部分看到的预构建码头工人图像的结果。额外的速度优势来自于CircleCI缓存Docker图像层的事实,这反过来导致构建环境的更快启动。

我们将浏览CircleCI配置文件(存储在。circleci/circle.yml)逐段。有关这种方法,请参见config.yml in .circleci完整列表;参见提交e5916f1源的变化。

解释配置文件的各个部分

同样,我们将引用Docker图像的全名。它是预先构建的Docker映像(neomatrix369/graalvm-suite-jdk8)提供者neomatrix369。它是在CircleCI构建开始之前提前构建并上传到Docker Hub的。它包含将要构建的GraalVM编译器的必要依赖项:

    docker:
          - image: neomatrix369/graal-jdk8:${IMAGE_VERSION:-python-2.7}
        steps:
          - checkout


下面的所有部分都执行与第一种方法完全相同的任务(出于相同的目的)(参见解释配置文件的各个部分部分了解更多详细信息)。

但是,在这种方法中,我们删除了以下部分,因为它们不再是必需的。


    - restore_cache:
              keys:
                - os-deps-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
                - os-deps-{{ arch }}-{{ .Branch }}
          - run:
              name: Install Os dependencies
              command: ./build/x86_64/linux_macos/osDependencies.sh
              timeout: 2m
          - save_cache:
              key: os-deps-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
              paths:
                - vendor/apt
                - vendor/apt/archives
          - restore_cache:
              keys:
                - make-382-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
                - make-382-{{ arch }}-{{ .Branch }}
          - run:
              name: Build and install make via script
              command: ./build/x86_64/linux_macos/installMake.sh
              timeout: 1m
          - save_cache:
              key: make-382-{{ arch }}-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
              paths:
                - /make-3.82/
                - /usr/bin/make
                - /usr/local/bin/make
                - /usr/share/man/man1/make.1.gz


在下一节中,我将介绍如何构建预构建的Docker映像。它将包括运行bash脚本/build/x86 _ 64/Linux _ macos/osdependencies . shand。/build/x86 _ 64/Linux _ macos/InstallMake . sh安装必要的依赖项,作为构建Docker映像的一部分。最后,我们将图像推送到Docker Hub或您选择的任何其他远程注册表。

构建预构建码头工人形象的步骤

运行build-docker-image.sh(参见bash脚本源),这取决于Dockerfile(参见docker脚本源)的存在。Dockerfile执行在容器内部运行依赖关系的所有必要任务(即,它运行bash脚本)。/build/x86 _ 64/Linux _ macos/osdependencies . shand。/build/x86 _ 64/Linux _ macos/InstallMake . sh

$ ./build-docker-image.sh


一旦映像成功构建,在设置了USER_NAME和IMAGE_NAME之后,运行push-graal-docker-IMage-to-hub . sh(参见源代码);否则,它将使用bash脚本中设置的默认值:

    $ USER_NAME="[your docker hub username]" IMAGE_NAME="[any image name]" \
        ./push-graal-docker-image-to-hub.sh

CircleCI配置文件统计:方法一与方法二

感兴趣的领域 方法1 方法2
配置文件(完整的源代码列表) build-on-circleci build-using-prebuilt-docker-image
提交点(sha) df28ee7 e5916f1
代码行(loc) 110行 85行
源行(sloc) 110 sloc 85 sloc
步骤(步骤:部分) 19 15
性能(参见性能部分) 由于缓存,速度有所提高,但比方法2慢 由于预构建的docker映像,以及不同步骤的缓存,速度加快。比方法1更快

确保启用DLC分层(这是一个付费功能


什么不该做

解决一个问题

我遇到了一些最初不起作用的事情,但后来通过更改配置文件或脚本得到了解决:

  • 请确保circleci/config.yml总是在文件夹的根目录中
  • 中使用store_artifacts指令时。circleci/config.yml文件设置,将该值设置为一个固定的文件夹名称(即在我们的例子中是jdk8-with-graal-local)。一旦构建完成,将路径设置为$ { BASEDIR }/project/JDK 8-with-graal不会创建结果工件...因此建议使用固定路径名。
  • 环境变量:使用环境变量时,请记住每个命令都在自己的shell中运行,这样在shell执行环境中设置给环境变量的值在外部是不可见的。遵循本文中使用的方法。设置环境变量,以便所有命令都能看到所需的值。这将使你在每一步结束时避免不良行为或意想不到的结果。
  • 缓存:使用缓存功能。有关CircleCI缓存的更多详细信息,请参考caching docs。看看它在这篇文章中是如何实现的。这将有助于避免混淆,并有助于更好地利用CircleCI提供的功能。

探讨两个问题

  • 缓存:当试图使用Docker Layer Caching(DLC)选项,因为它是付费功能。一旦知道了这一点,关于“为什么CircleCI在每次构建过程中不断下载所有的层”的疑问将得到澄清。有关Docker层缓存的详细信息,请参考docs。它还可以澄清为什么构建仍然没有您希望的在非付费模式下的速度快。

一般说明

  • 轻量级实例:为了避免认为我们可以运行重载构建的陷阱,请检查实例的技术规范文档。如果我们运行标准的Linux命令来探测实例的技术规格,我们可能会被误认为它们是高规格的机器。请参见登记实例的硬件和软件详细信息的步骤(请参见显示硬件、软件、运行时环境。Dependenciessection节的信息和版本)。实例实际上是虚拟机或类似容器的环境,具有类似2CPU/4096MB的资源。这意味着我们不能运行长时间运行或高负荷的构建,比如构建GraalVM套件。也许有另一种方法来处理这种类型的构建,或者这种构建需要被分解成更小的部分。
  • 全局环境变量:因为config.yml中的每一行都在自己的shell上下文中运行,所以由其他执行上下文设置的变量不能访问这些值。为了克服这一点,我们采用了两种方法:
    • 将变量作为参数传递给调用bash/shell脚本,以确保脚本能够访问环境变量中的值。
    • 使用source命令作为运行步骤,使环境变量可以全局访问。

最终结果和总结

在构建成功完成后,我们会看到下面的屏幕(最后一步:更新工件登记,其中工件已被复制):

最终结果


工件现在被放在正确的文件夹中供下载。我们主要关心的是jdk8-with-graal.tar.gz神器。

性能

在写这篇文章之前,我对这两种方法进行了多次测试,并记下了完成构建所需的时间,如下所示:

方法一:标准的CircleCI构建(启用缓存)。

  • 13分28秒。
  • 13分59秒。
  • 14分52秒。
  • 10分38秒。
  • 10分26秒。
  • 10分23秒。

方法二:使用预构建的docker映像(启用缓存,DLC功能不可用)。

  • 13分15秒。
  • 15分16秒。
  • 15分29秒。
  • 15分58秒。
  • 10分20秒。
  • 9分49秒。

注意:当使用付费层时,方法二应该表现出更好的性能,如Docker Layer Caching是该计划的一部分。

理智检查

为了确保通过使用上述两种方法,我们实际上已经用GraalVM编译器构建了一个有效的嵌入式JDK,我们对创建的工件执行了以下步骤:

首先,从CircleCI仪表板上的工件选项卡下下载jdk8-with-graal.tar.gz工件(需要登录):

CircleCI仪表板


然后,解压缩. tar.gz文件,并执行以下操作:

tar xvf jdk8-with-graal.tar.gz


之后,运行以下命令来检查JDK二进制文件是否有效:

cd jdk8-with-graal
./bin/java -version


最后,检查我们是否得到以下输出:

    openjdk version "1.8.0-internal"
    OpenJDK Runtime Environment (build 1.8.0-internal-jenkins_2017_07_27_20_16-b00)
    OpenJDK 64-Bit Graal:compiler_ab426fd70e30026d6988d512d5afcd3cc29cd565:compiler_ab426fd70e30026d6988d512d5afcd3cc29cd565 (build 25.71-b01-internal-jvmci-0.46, mixed mode)


同样,为了确认JRE是否有效并内置了GraalVM编译器,我们这样做:

./bin/jre/java -version


检查我们是否得到与前一个代码块类似的输出。

    openjdk version "1.8.0-internal"
    OpenJDK Runtime Environment (build 1.8.0-internal-jenkins_2017_07_27_20_16-b00)
    OpenJDK 64-Bit Graal:compiler_ab426fd70e30026d6988d512d5afcd3cc29cd565:compiler_ab426fd70e30026d6988d512d5afcd3cc29cd565 (build 25.71-b01-internal-jvmci-0.46, mixed mode)


有了这个,我们成功地构建了内置GraalVM编译器的JDK8。我们还将Graal和Truffle组件捆绑在一个归档文件中,这两个文件都可以通过CircleCI接口下载。

注意:您会注意到,作为构建步骤的一部分,我们确实会在将二进制文件打包到压缩档案之前对它们进行健全性检查(参见配置文件的底部)。

漂亮的徽章

真棒Graal!


我们都喜欢炫耀;我们还想知道我们的构建工作的当前状态。绿色的建筑状态图标是成功的一个很好的标志。

我们可以通过显示在CircleCI上构建的项目的构建状态(分支特定的,即主分支或您创建的另一个分支),非常容易地嵌入这两个状态标记。(参见docs了解更多信息。(

结论

我们探索了使用CircleCI环境构建GraalVM编译器的两种方法。它们是比较两种方法性能的很好的实验。我们还看到了一些需要避免的事情。此外,我们还看到了CircleCI的一些特性是多么有用。

一旦我们了解了CircleCI环境,它就非常容易使用,并且每次运行时都会给我们一致的行为。我们还可以为构建的每个步骤设置构建时间检查,如果完成一个步骤所花费的时间超过了阈值时间段,则中止构建。

使用预先构建的Docker图像的能力,加上Docker Layer Caching在CircleCI上可以大大提高性能,因为它节省了我们在每次构建时重新安装任何必要依赖项所需的构建时间。CircleCI上还提供了额外的性能加速,缓存了构建步骤——如果相同的步骤没有发生变化,就不必重新运行,这再次节省了构建时间。

CircleCI上有很多有用的特性,有大量的文档,社区论坛上的每个人都很有帮助;问题会很快得到回答。

接下来,让我们在另一个构建环境/构建农场上构建相同和更多的东西——提示,提示,你认为和我一样吗?Adopt OpenJDK buildfarm?我们可以试一试!

感谢并归功于Ron Powell来自CircleCI和Oleg Šelajev来自甲骨文实验室的校对和建设性反馈。

请让我知道这是否有所帮助,在下面的评论中写一行或者发推特到@theNeomatrix369。我也欢迎反馈——看看你能做些什么reach me—最重要的是,请查看上面提到的链接。