Quarkus 原生应用程序在 Raspberry Pi 上运行

简介

当我于2018年12月首次接触Quarkus时,我突然觉得Quarkus运行时有潜力覆盖大约95%的软件用例。我的朋友Sanne在运行时仍处于alpha阶段时,在他的笔记本电脑上演示了Quarkus,远非如今大家所熟知的庞大、扩展且成熟的框架。即便如此,我还是体验到了一种思考、实现和部署Java应用的新方式。

我最近为Red Hat EMEA合作伙伴参加了一个Hackfest,旨在推广Quarkus框架的潜力和性能。我的主要技术目标是展示Quarkus可以在任何地方运行,并且由于其原生编译过程带来的性能优势,它绝对是物联网设备(又称远边缘)的首选运行时。

为了实现上述目标,我选择了一台集成ARM兼容中央处理器的单板计算机,并在基于Linux的操作系统上通过容器运行了一个Quarkus原生应用。以下是具体操作方法:

先决条件

  • 单板计算机:单板计算机中集成的ARM兼容CPU必须支持64位模式。

  • 操作系统

    • 要使Quarkus原生容器镜像在容器中运行,需要一个64位Linux操作系统;

    • 操作系统必须支持aarch64 CPU,这就要求该操作系统提供aarch64基础发行版;

    • 最好:目标操作系统应完全支持目标单板计算机。

  • 容器技术:不出所料!要在Linux上运行容器,必须使用OCI标准容器引擎。

  • 构建工具:鉴于GraalVM原生编译Quarkus所需的资源量巨大,我们还需要一台ARM服务器/虚拟机来实现这一目标。该服务器应满足以下最低要求:

    • CPU:与目标单板计算机中嵌入的ARM型号相同

    • 核心数:4

    • 内存:8GB

    • 磁盘空间:20GB

  • 容器注册表:为了能够在单板计算机上部署包含在ARM服务器上构建的Quarkus原生应用的容器镜像,需要一个远程容器注册表账户。

技术

单传感器板

选择目标单板计算机型号是一项相当简单的任务:我已经拥有一台符合上述要求的树莓派。

  • 型号:树莓派3 B+

  • SOC:博通BCM2837B0,Cortex-A53 (ARMv8) 64位SoC

  • CPU:1.4GHz 64位四核ARM Cortex-A53 CPU

  • 内存:1GB LPDDR2 SDRAM

  • WIFI:双频802.11ac无线局域网(2.4GHz和5GHz)及蓝牙4.2

  • 以太网:通过USB 2.0的千兆以太网(最高300 Mbps)。支持以太网供电(需额外PoE HAT)。改进的PXE网络和USB大容量存储启动。

  • 散热管理:

  • 视频:是 – VideoCore IV 3D。全尺寸HDMI

  • 音频:

  • USB 2.0:4个端口

  • GPIO:40针

  • 电源:5V/2.5A直流电源输入

  • 操作系统支持:Linux和Unix

操作系统

我尝试了几种符合上述要求的操作系统。两种可能的解决方案是Fedora物联网版和Gentoo。经过一些测试,我选择了Fedora IoT,原因如下:

  • 对64位架构(aarch64)的完整原生支持

  • 稳定性

  • 易用性

  • 成熟度

  • 生命周期

  • 对RPi 3 B+的全面支持

容器技术

这项工作也很轻松:Podman。Podman是一个无守护进程的容器引擎,用于在Linux系统上开发、管理和运行OCI容器。容器可以以root身份或rootless模式运行。此外,在Fedora IoT基本安装中无需安装额外的组件/层,因为Podman容器引擎已包含在操作系统发行版中。

此外,从更深层次的技术角度来看,由于Fedora中的cgroups现在是v2,使用Docker将迫使您进行多次调整并降级cgroups版本至v1,以避免出现警告甚至失败。

Podman之所以成为首选容器,在其文章中有很好的解释。我引用其中的快速解释:

crun命令是支持cgroup V2的运行时,从Fedora 31开始提供。其他容器系统使用runc运行时。然而,runc仅支持cgroup V1。cgroup内核功能允许您为容器分配CPU时间、网络带宽和系统内存等资源。cgroup版本1仅支持由root运行的容器,而版本2支持由root或非特权用户运行的容器。

— Rūmī

RPi上“podman info”命令关于OCI运行时的输出如下:

...
ociRuntime:
    name: crun
    package: crun-0.14.1-4.fc32.aarch64
    path: /usr/bin/crun
    version: |-
      crun version 0.14.1
      commit: 598ea5e192ca12d4f6378217d3ab1415efeddefa
      spec: 1.0.0
      +SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +YAJL
...

这意味着Fedora IoT 31及更高版本中包含的Podman版本由于使用了“crun”运行时而不是其他Linux发行版和Fedora v31之前的版本中嵌入的经典“runc”运行时,因此完全支持cgroup v2。

构建工具

如今,有多种方法可以提供一台配备足够资源来原生编译Quarkus的aarch64服务器。

  • 花费合理但不小的预算购买一台物理ARM服务器(请勿在家中尝试);

  • 花费非常小的预算在公共云上提供一台ARM虚拟机(最快,如果您不在主机上运行Linux操作系统,建议使用此方式);

  • 使用QEmu在您的主机上提供一台虚拟机(最便宜);

  • 在容器内运行QEmu,模拟quarkus容器镜像(仍在研究和测试中).

由于我的桌面运行的是Fedora Workstation,所以我选择了第三种选项。

容器注册表

首选的远程容器注册表是Quay.io。关于设置在quay.io上创建的账户的更多详细信息将在本文的专门部分分享。

实现

本节将指导您完成以下步骤:

  1. 设置虚拟机

  2. 设置RPi和Fedora IoT

  3. 将容器引擎连接到quay.io账户

  4. 在Podman上编译和部署您的Quarkus原生应用

  5. 结果

1. 设置虚拟机

这个目标可以通过基于QEmu的虚拟机技术来实现。QEmu在此任务中起着重要作用,因为它是用于创建模拟目标CPU架构的虚拟机的最佳上游工具。

使用QEmu设置虚拟机

在Linux发行版上,强烈推荐使用VMM(虚拟机管理器)。

$ sudo dnf groupinstall virtualization
$ sudo dnf install qemu-system-aarch64

虚拟机最低要求如下:

  • CPU:4核Cortex-A53 (ARMv8) 64位

  • 内存:8 GB

  • 磁盘:10Gb

  • 网络:桥接

  • 操作系统:Fedora Minimal (最新稳定版) - aarch64 (链接)

  • 运行时:GraalVM (最新版) - aarch64 (链接)

vm cpu config

作为参考,如果您想将虚拟机用于其他构建和测试,请考虑增加虚拟机磁盘大小。

设置GraalVM环境

在您的虚拟机上,您需要GCC以及glibc和zlib头文件。常见发行版的示例:

$ sudo dnf install gcc glibc-devel zlib-devel libstdc++-static

安装OpenJDK

$ sudo dnf install -y java-11-openjdk

这里下载您的架构(aarch64)的最新版本graalvm并解压。详细说明请参见这里

将graalvm内容移动到/usr/lib/graalvm,并通过在/etc/profile中添加以下代码片段来设置环境变量。

#JAVA_HOME
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.6.10-0.fc32.aarch64
PATH=$PATH:$HOME/bin:$JAVA_HOME/bin
export JAVA_HOME
export PATH
#GRAALVM_HOME
GRAALVM_HOME=/usr/lib/graalvm/graalvm-ce-java11-19.3.1
PATH=$PATH:$HOME/bin:$GRAALVM_HOME/bin
export GRAALVM_HOME
export PATH

(Optional) Set the JAVA_HOME environment variable to the GraalVM installation directory.
export JAVA_HOME=${GRAALVM_HOME}
(Optional) Add the GraalVM bin directory to the path
export PATH=${GRAALVM_HOME}/bin:$PATH

示例

...

#JAVA_HOME
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.8.10-0.fc32.x86_64
PATH=$PATH:$HOME/bin:$JAVA_HOME/bin
export JAVA_HOME
export PATH
#GRAALVM_HOME
GRAALVM_HOME=/usr/lib/graalvm/graalvm-ce-java11-20.2.0
PATH=$PATH:$HOME/bin:$GRAALVM_HOME/bin
export GRAALVM_HOME
export PATH
export JAVA_HOME=${GRAALVM_HOME}
export PATH=${GRAALVM_HOME}/bin:$PATH

2. 设置RPi和Fedora IoT

下载最新的Fedora IoT发行版

这里下载适用于树莓派的稳定且完全支持的Fedora IoT版本。选择aarch64的raw镜像。

将操作系统刷写到SD卡上

非Linux操作系统

如果您在主机上没有运行任何基于Linux的操作系统,为了能够轻松、安全、快速地将操作系统镜像安装到MicroSD卡上,我强烈建议从这里下载官方的Raspberry Imager工具。Raspberry Pi Imager的教程可以在这里找到。好消息是,我们已经创建了基于ARM的虚拟机,所以您已经有一个Linux发行版可以用来将Fedora IoT OS刷写到SD卡上了!

在Linux发行版上

Fedora OS Linux发行版提供了一个极大地简化此任务的工具:fedora-arm-installer。让我们将该工具添加到操作系统中:

$ dnf install -y fedora-arm-installer

如果您运行的是Fedora,这是设置DS卡的示例:

$ sudo fedora-arm-image-installer -y --image=/home/<<user_home>>/Fedora-IoT-32-20200429.0.aarch64.raw.xz --target=rpi3 --media=/dev/sde --resizefs --addkey=/home/<<user_home>>/.ssh/id_rsa.pub --norootpass --addconsole

有关更多见解,请参阅官方工具指南。该工具的描述在此

作为参考,生成的SD卡将包含3个分区:

  1. BIOS (U-Boot) - 1.1 Gb

  2. Boot - 525 Mb

  3. Root - 占用SD卡剩余的未分配空间

sd card partitions

在RPi CPU上启用64位

刷写操作成功完成后,查看操作系统的配置文件很重要。编辑文件config.txt,并确保**[all]**部分以以下键值对开头:arm_control=0x200, arm_64bit=1。

例如:

...

# Default Fedora configs for all Raspberry Pi Revisions
[all]
# Put the RPi into 64 bit mode
arm_control=0x200
arm_64bit=1

...

首次启动后

如果您对默认键盘布局满意,或者通过LAN线缆连接RPi到网络……这些步骤可能不是必需的。

键盘布局

通过执行以下命令获取的列表中找到您选择的键盘布局:

$ localectl list-keymaps | grep your-locale

并在系统配置中设置它。

$ localectl set-keymap _map_

通过WiFi连接启用互联网访问

获取可用网络列表:

$ nmcli device wifi list

并连接到您选择的Wi-Fi网络。

$ nmcli device wifi connect SSID password PASSWORD

禁用防火墙

仅因为这是演示!!! :-)

$ systemctl disable firewalld
$ systemctl stop firewalld

为设备创建一个新用户

由于使用了fedora-arm-installer工具,我们已将公钥添加到root用户。在Linux系统上使用root用户从来都不是一个好主意,或者说,不是一个好习惯。因此,我们将为我们的操作系统添加一个具有wheel权限的新管理员用户。

$ useradd edge
$ passwd edge
$ usermod edge -a -G wheel

3. 将容器引擎连接到quay.io账户

如果您还没有账户,请在quay.io上创建一个免费账户。

登录Quay.io

要登录Quay.io,请执行podman login quay.io命令。

注意:如果您查看Quay账户设置,可以创建一个加密密码以获得更多安全性。

$ podman login quay.io
Username: myusername
Password: mypassword

4. 在Podman上编译和部署您的Quarkus原生应用

回到虚拟机方面。

构建可执行文件

您需要git来下载源代码并执行它。

$ sudo dnf install -y git

克隆quarkus quickstarts仓库,并选择您想要使用的quarkus quickstart。

$ git clone https://github.com/quarkusio/quarkus-quickstarts.git ~/git/quarkus-quickstarts

进入您选择的quickstart的根目录(例如,getting-started),然后开始构建。

$ ./mvnw package -Pnative

整个Maven过程大约需要40分钟。请注意,如果服务具有更多功能(即导入和使用更多模块),则可能需要更长时间。

[edge@localhost getting-started]$ ./mvnw package -Pnative
...
[INFO] --- quarkus-maven-plugin:1.8.1.Final:native-image (default) @ getting-started ---
[INFO] [org.jboss.threads] JBoss Threads version 3.1.1.Final
[INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building native image source jar: /home/edge/quarkus-quickstarts/getting-started/target/getting-started-1.0-SNAPSHOT-native-image-source-jar/getting-started-1.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Building native image from /home/edge/quarkus-quickstarts/getting-started/target/getting-started-1.0-SNAPSHOT-native-image-source-jar/getting-started-1.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GraalVM Version 20.2.0 (Java Version 11.0.8)
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] /usr/lib/graalvm/graalvm-ce-java11-20.2.0/bin/native-image -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=1 -J-Duser.language=en -J-Dfile.encoding=UTF-8 --initialize-at-build-time= -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime -H:+JNI -jar getting-started-1.0-SNAPSHOT-runner.jar -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:-AddAllCharsets -H:EnableURLProtocols=http -H:NativeLinkerOption=-no-pie --no-server -H:-UseServiceLoaderFeature -H:+StackTrace getting-started-1.0-SNAPSHOT-runner
[getting-started-1.0-SNAPSHOT-runner:2012]    classlist:  82,587.50 ms,  0.96 GB
[getting-started-1.0-SNAPSHOT-runner:2012]        (cap):  25,485.45 ms,  0.96 GB
[getting-started-1.0-SNAPSHOT-runner:2012]        setup:  62,730.91 ms,  0.94 GB
15:15:03,073 INFO  [org.jbo.threads] JBoss Threads version 3.1.1.Final
[getting-started-1.0-SNAPSHOT-runner:2012]     (clinit):   9,087.82 ms,  2.24 GB
[getting-started-1.0-SNAPSHOT-runner:2012]   (typeflow): 392,638.79 ms,  2.24 GB
[getting-started-1.0-SNAPSHOT-runner:2012]    (objects): 300,849.30 ms,  2.24 GB
[getting-started-1.0-SNAPSHOT-runner:2012]   (features):   9,611.94 ms,  2.24 GB
[getting-started-1.0-SNAPSHOT-runner:2012]     analysis: 729,428.50 ms,  2.24 GB
[getting-started-1.0-SNAPSHOT-runner:2012]     universe:  37,544.86 ms,  2.24 GB
[getting-started-1.0-SNAPSHOT-runner:2012]      (parse): 135,120.27 ms,  2.24 GB
[getting-started-1.0-SNAPSHOT-runner:2012]     (inline): 314,244.72 ms,  3.25 GB
[getting-started-1.0-SNAPSHOT-runner:2012]    (compile): 817,352.63 ms,  4.08 GB
[getting-started-1.0-SNAPSHOT-runner:2012]      compile: 1,304,395.09 ms,  4.08 GB
[getting-started-1.0-SNAPSHOT-runner:2012]        image: 102,001.77 ms,  4.10 GB
[getting-started-1.0-SNAPSHOT-runner:2012]        write:  16,216.90 ms,  4.10 GB
[getting-started-1.0-SNAPSHOT-runner:2012]      [total]: 2,342,173.85 ms,  4.10 GB
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Execute [objcopy, --strip-debug, /home/edge/quarkus-quickstarts/getting-started/target/getting-started-1.0-SNAPSHOT-runner]
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 2394526ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  45:25 min
[INFO] Finished at: 2020-09-30T15:46:11+02:00
[INFO] ------------------------------------------------------------------------

必须将原生Quarkus可执行文件打包到容器镜像中,才能在容器运行时上运行它。为了实现这一目标,让我们让您虚拟机中已安装的容器运行时来构建容器镜像。

$ sudo podman build -f src/main/docker/Dockerfile.native -t quay.io/$myusername/quarkus-getting-started:1-aarch64 .

将容器镜像推送到您选择的容器注册表,以便与边缘设备共享。

$ sudo podman push quay.io/$myusername/quarkus-getting-started:1-aarch64

部署可执行文件

在RPi上

$ sudo podman run -it --rm -p 8090:8080 --name quarkus-getting-started quay.io/$myusername/quarkus-getting-started:1-aarch64

结果

本节展示了上述步骤产生的成果。

启动/停止时间

[edge@localhost ~]$ sudo podman run -it --rm -p 8090:8080 --name quarkus-getting-started quay.io/abattagl/quarkus-getting-started:1-aarch64
Trying to pull quay.io/abattagl/quarkus-getting-started:1-aarch64...
Getting image source signatures
Copying blob d44f88e7704f done
Copying blob 8c4861605060 done
Copying blob c5a0fdbc0d7a done
Copying blob 5dd9a2ffef88 done
Copying config f08559ac50 done
Writing manifest to image destination
Storing signatures
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-10-01 08:49:34,566 INFO  [io.quarkus] (main) getting-started 1.0-SNAPSHOT native (powered by Quarkus 1.8.1.Final) started in 0.055s. Listening on: http://0.0.0.0:8080
2020-10-01 08:49:34,566 INFO  [io.quarkus] (main) Profile prod activated.
2020-10-01 08:49:34,566 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]
^C
2020-10-01 08:49:49,061 INFO  [io.quarkus] (Shutdown thread) getting-started stopped in 0.007s

应用程序启动耗时55毫秒,停止耗时7毫秒。考虑到Quarkus原生应用是在RPi3 CPU上运行的(请再次查看本文开头处的CPU规格!),这是非常令人印象深刻的!

内存占用

RPi上“top”命令输出的以下截图显示了在容器技术之上运行Quarkus原生应用程序的成本有多低。

native quarkus getting started on arm8 memory footprint

对于一个提供简单网页和REST API的Java应用程序来说,大约22MB!

连接到服务

以下截图显示了在RPi上安装的Podman中原生运行的Quarkus微服务所提供的网页。

native quarkus getting started on arm8 web page

结论

全局概览

本文重现了在树莓派CPU上编译和运行Quarkus原生应用程序的步骤。此过程可能适用于ARM v8系列的所有CPU,但由于某些ARM CPU的架构可能与该版本的标准架构有所不同,可能存在一些例外情况。

构建工具

对于这个概念验证,我使用了基于QEmu的VM。这促进了可重用性,但在易用性方面仍然很昂贵(QEmu在Linux操作系统上运行得非常好,但在Windows和MacOS上则不然)。正如本文开头所述,目前正在研究一种更灵活、更便携、更可扩展的解决方案,该解决方案仍基于CPU仿真和容器技术。这将有助于构建过程的委托和扩展。

请注意,基于QEmu的工具仍然是一种变通方法。GraalVM缺少交叉编译功能,并且需要一段时间才能发布。

Quarkus无处不在

很明显,Quarkus有可能随处运行。当然,这取决于GraalVM对底层架构的支持程度。

到目前为止,我认为这仅仅是一个开始,未来还将有更多功能和能力。

如果有一个适用于aarch64的Mandrel发行版将会非常好。Mandrel与GraalVM的native-image功能相结合,并提供OpenJDK和Red Hat Enterprise Linux库,以提高可维护性。期待测试它!;-)