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或非特权用户运行的容器。
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. 设置虚拟机
这个目标可以通过基于QEmu的虚拟机技术来实现。QEmu在此任务中起着重要作用,因为它是用于创建模拟目标CPU架构的虚拟机的最佳上游工具。
使用QEmu设置虚拟机
在Linux发行版上,强烈推荐使用VMM(虚拟机管理器)。
$ sudo dnf groupinstall virtualization
$ sudo dnf install qemu-system-aarch64
虚拟机最低要求如下:
作为参考,如果您想将虚拟机用于其他构建和测试,请考虑增加虚拟机磁盘大小。
设置GraalVM环境
在您的虚拟机上,您需要GCC以及glibc和zlib头文件。常见发行版的示例:
$ sudo dnf install gcc glibc-devel zlib-devel libstdc++-static
安装OpenJDK
$ sudo dnf install -y java-11-openjdk
将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发行版上
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个分区:
-
BIOS (U-Boot) - 1.1 Gb
-
Boot - 525 Mb
-
Root - 占用SD卡剩余的未分配空间
在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
...
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规格!),这是非常令人印象深刻的!
结论
全局概览
本文重现了在树莓派CPU上编译和运行Quarkus原生应用程序的步骤。此过程可能适用于ARM v8系列的所有CPU,但由于某些ARM CPU的架构可能与该版本的标准架构有所不同,可能存在一些例外情况。