【问题标题】:Quarkus Native Application with DioZero on Raspberry Pi using Docker containers (multi-arch)Quarkus Native Application with DioZero on Raspberry Pi 使用 Docker 容器(多架构)
【发布时间】:2022-08-18 23:38:10
【问题描述】:

哟哟!

范围

我正在尝试使用一些将基于 Quarkus 的应用程序部署到 Raspberry Pi花哨的技术,我的目标是找到一种简单的方法来使用 Quarkus 框架开发应用程序,然后作为本机可执行文件部署到具有完整 GPIO 引脚访问权限的树莓设备。下面我将提供您将看到我为自己和我的环境设置设置的要求,以便更好地了解我所面临的问题。

验收标准

  1. Java 17
  2. 使用 GraalVM 构建本机可执行文件
  3. 在 raspberry 的 docker 上的微镜像中执行本机可执行文件
  4. 目标平台可能不同
  5. 能够使用树莓派的GPIO、SPI、I2C等接口

    环境

    Development PC Raspberry Pi Model 3 B+
    os: Ubuntu 22.04.1 LTS os: DietPy
    platform: x86_64, linux/amd64 platform: aarch64, linux/arm64/v8

    先决条件

    应用

    source code on github

    1. 至于我使用的项目库入门从申请 https://github.com/quarkusio/quarkus-quickstarts
    2. 将 diozero 库添加到 pom.xml
          <dependency>
              <groupId>com.diozero</groupId>
              <artifactId>diozero-core</artifactId>
              <version>1.3.3</version>
          </dependency>
      
    3. 创建一个简单的资源来测试 GPIO 引脚
      package org.acme.getting.started;
      
      import com.diozero.devices.LED;
      
      import javax.ws.rs.Path;
      import javax.ws.rs.QueryParam;
      
      @Path(\"led\")
      public class LedResource {
      
          @Path(\"on\")
          public String turnOn(final @QueryParam(\"gpio\") Integer gpio) {
              try (final LED led = new LED(gpio)) {
                  led.on();
              } catch (final Throwable e) {
                  return e.getMessage();
              }
              return \"turn on led on gpio \" + gpio;
          }
      
          @Path(\"off\")
          public String turnOff(final @QueryParam(\"gpio\") Integer gpio) {
              try (final LED led = new LED(gpio)) {
                  led.off();
              } catch (final Throwable e) {
                  return e.getMessage();
              }
              return \"turn off led on gpio \" + gpio;
          }
      
      }

      4.Dockerfile

      ```
      # Stage 1 : build with maven builder image with native capabilities
      FROM quay.io/quarkus/ubi-quarkus-native-image:22.0.0-java17-arm64 AS build
      COPY --chown=quarkus:quarkus mvnw /code/mvnw
      COPY --chown=quarkus:quarkus .mvn /code/.mvn
      COPY --chown=quarkus:quarkus pom.xml /code/
      USER quarkus
      WORKDIR /code
      RUN ./mvnw -B org.apache.maven.plugins:maven-dependency-plugin:3.1.2:go-offline
      COPY src /code/src
      RUN ./mvnw package -Pnative
      # Stage 2 : create the docker final image
      FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6-902
      WORKDIR /work/
      COPY --from=build /code/target/*-runner /work/application
      # set up permissions for user 1001
      RUN chmod 775 /work /work/application \\
        && chown -R 1001 /work \\
        && chmod -R \"g+rwX\" /work \\
        && chown -R 1001:root /work
      EXPOSE 8080
      USER 1001
      CMD [\"./application\", \"-Dquarkus.http.host=0.0.0.0\"]
      ```
      

      使用本机可执行文件构建映像

      基于quarkus docs的Dockerfile,我将构建容器的镜像更改为quay.io/quarkus/ubi-quarkus-native-image:22.0.0-java17-arm64,将执行容器的镜像更改为registry.access.redhat.com/ubi8/ubi-minimal:8.6-902,这两个都符合linux/arm64*。

      由于我正在linux/amd64 中开发和构建,并且我想以linux/arm64/v8 为目标,因此我的可执行文件必须在类似目标的环境中创建。我可以做到这一点构建x为 docker 镜像启用跨架构构建的功能。

      1. 安装 QEMU

        sudo apt-get install -y qemu-user-static

        sudo apt-get install -y binfmt-support

        1. 为 linux/arm64/v8 构建初始化 buildx

        sudo docker buildx create --platform linux/arm64/v8 --name arm64-v8

        1. 使用新的驱动程序

        sudo docker buildx use arm64-v8

        1. 引导驱动程序

        sudo docker buildx inspect --bootstrap

        1. 验证

        sudo docker buildx inspect

        Name:   arm64-v8
        Driver: docker-container
        Nodes:
        Name:      arm64-v80
        Endpoint:  unix:///var/run/docker.sock
        Status:    running
        Platforms: linux/arm64*, linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

        现在看起来我们已经准备好运行构建了。我结束了以下命令

        sudo docker buildx build --push --progress plain --platform linux/arm64/v8 -f Dockerfile -t nanobreaker/agus:arm64 .
        
        • --push - 因为我需要在某处部署最终映像
        • --platform linux/arm64/v8 - docker 需要定义目标平台
        • -t nanobreaker/agus:arm64 - 我的最终图像的目标存储库

        完成构建并推送映像大约需要 16 分钟

        • 目标平台是linux/arm64,根据需要
        • 59.75 MB 图像大小,已经足够好(使用微图像我可以达到 ~10 MB)

        连接到覆盆子后,下载图像并运行它

        docker run -p 8080:8080 nanobreaker/agus:arm64
        

        很好,让我们尝试执行一个 http 请求来测试 gpio 引脚

        curl 192.168.0.20:8080/led/on?gpio=3
        

        好的,所以我在这里看到存在权限问题,并且 diozero 库不在 java.library.path 中

        我们可以通过向 docker run 命令添加额外的参数来修复权限问题

        docker run --privileged -p 8080:8080 nanobreaker/agus:arm64
        

        问题

        从这一点来看,我不知道如何解决本机可执行文件中的库加载错误。

        我试过了:

        更新我

        看起来我在这里有两个选择

        • 找出一种为 diozero 库创建配置的方法,以便在本机映像编译期间由 GraalVM 正确解析。
        • 将库添加到本机映像并将其传递给本机可执行文件。

        更新二

        进一步阅读 quarkus 文档让我在这里https://quarkus.io/guides/writing-native-applications-tips

        默认情况下,在构建本机可执行文件时,GraalVM 不会将类路径上的任何资源包含到它创建的本机可执行文件中。需要明确配置本机可执行文件的一部分的资源。 Quarkus 自动包含 META-INF/resources(网络资源)中存在的资源,但在此目录之外,您只能靠自己。

        我联系了@Matt Lewis(diozero 的创建者),他很乐意分享他的配置,他曾经将这些配置编译到 GraalVM 中。谢谢马特!

        这是我最初测试的文档:https://www.diozero.com/performance/graalvm.html 我将 GraalVM 配置存放在这里:https://github.com/mattjlewis/diozero/tree/main/src/main/graalvm/config

        所以结合我们可以丰富 pom.xml 的知识和额外的设置来告诉 GraalVM 如何处理我们的库

        <quarkus.native.additional-build-args>
            -H:ResourceConfigurationFiles=resource-config.json,
            -H:ReflectionConfigurationFiles=reflection-config.json,
            -H:JNIConfigurationFiles=jni-config.json,
            -H:+TraceServiceLoaderFeature,
            -H:+ReportExceptionStackTraces
        </quarkus.native.additional-build-args>
        

        还在项目的资源文件夹(src/main/resources)中添加了resource-config.json、reflection-config.json、jni-config.json

        首先,我将尝试在我的原生操作系统./mvnw package -Dnative 中创建一个原生可执行文件

        Fatal error: org.graalvm.compiler.debug.GraalError: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: No instances of java.lang.ProcessHandleImpl are allowed in the image heap as this class should be initialized at image runtime. To see how this object got instantiated use --trace-object-instantiation=java.lang.ProcessHandleImpl.
        

        好吧,所以它失败了,但是让我们按照建议跟踪对象实例化,也许我们可以在配置中做一些事情来解决这个问题。我将--trace-object-instantiation=java.lang.ProcessHandleImpl 添加到附加的构建参数中。

        Fatal error: org.graalvm.compiler.debug.GraalError: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: No instances of java.lang.ProcessHandleImpl are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized by the java.lang.ProcessHandleImpl class initializer with a trace: 
                at java.lang.ProcessHandleImpl.<init>(ProcessHandleImpl.java:227)
                at java.lang.ProcessHandleImpl.<clinit>(ProcessHandleImpl.java:77)
        .  To fix the issue mark java.lang.ProcessHandleImpl for build-time initialization with --initialize-at-build-time=java.lang.ProcessHandleImpl or use the the information from the trace to find the culprit and --initialize-at-run-time=<culprit> to prevent its instantiation.
        

        至少有一些新的东西,让我们尝试在构建时先用--initialize-at-build-time=java.lang.ProcessHandleImpl 初始化它

        Error: Incompatible change of initialization policy for java.lang.ProcessHandleImpl: trying to change BUILD_TIME from command line with \'java.lang.ProcessHandleImpl\' to RERUN for JDK native code support via JNI
        com.oracle.svm.core.util.UserError$UserException: Incompatible change of initialization policy for java.lang.ProcessHandleImpl: trying to change BUILD_TIME from command line with \'java.lang.ProcessHandleImpl\' to RERUN for JDK native code support via JNI
        
        

        好吧,我们无法更改初始化类型,而且看起来它不会给我们带来任何影响。 我发现使用 -H:+PrintClassInitialization 我们可以生成一个带有类初始化信息的 csv 文件 这里我们有两行java.lang.ProcessHandleImpl

        java.lang.ProcessHandleImpl, RERUN, for JDK native code support via JNI
        java.lang.ProcessHandleImpl$Info, RERUN, for JDK native code support via JNI
        

        所以它说这个类被标记为RERUN,但这不是我们要找的东西吗?现在对我来说毫无意义。

        更新三

        使用@Matt 提供的 graalvm 配置,我能够编译本机映像,但由于java.lang.UnsatisfiedLinkError,它在运行时无论如何都会失败,这让我觉得库没有正确注入。

        所以看起来我们只需要构建一个适当的配置文件,为了做到这一点,让我们现在构建我们的应用程序没有原生,只需在 raspberry 上运行它,触发与 diozero 相关的代码,获取输出配置。

        ./mvnw clean package -Dquarkus.package.type=uber-jar

        部署到 raspberry,将使用 graalvm 代理运行以生成配置 (https://www.graalvm.org/22.1/reference-manual/native-image/Agent/)

        /$GRAALVM_HOME/bin/java -agentlib:native-image-agent=config-output-dir=config -jar ags-gateway-1.0.0-SNAPSHOT-runner.jar

        运行简单的请求来触发 diozero 代码(我在 gpio 4 上连接了一个 led 到树莓派,实际上看到它关闭/打开)

        curl -X POST 192.168.0.20:8080/blink/off?gpio=4

        curl -X POST 192.168.0.20:8080/blink/on?gpio=4

        我已经发布了project 和output configs

        一件事我注意到\"pattern\":\"\\\\Qlib/linux-aarch64/libdiozero-system-utils.so\\\\E\" aarch64 库在 py 上运行时被拉取,这是正确的,但是当我在本机操作系统上构建时,我应该指定 \'amd64\' 平台。

        让我们尝试用新的配置构建一个原生

        ./mvnw package -Dnative
        

        编译成功,我们运行测试

        ./target/ags-gateway-1.0.0-SNAPSHOT-runner
        curl -X POST localhost:8080/led/on?gpio=4
        

        在这里我们又遇到了错误

        ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-0) HTTP Request to /led/on?gpio=4 failed, error id: b0ef3f8a-6813-4ea8-886f-83f626eea3b5-1: java.lang.UnsatisfiedLinkError: com.diozero.internal.provider.builtin.gpio.NativeGpioDevice.openChip(Ljava/lang/String;)Lcom/diozero/internal/provider/builtin/gpio/GpioChip; [symbol: Java_com_diozero_internal_provider_builtin_gpio_NativeGpioDevice_openChip or Java_com_diozero_internal_provider_builtin_gpio_NativeGpioDevice_openChip__Ljava_lang_String_2]
        

        所以我终于设法构建了本机映像,但由于某种原因它没有为本机库解析 JNI。

        关于如何将 diozero 库正确注入本机可执行文件的任何想法?

    标签: java linux arm64 buildx quarkus-native


    【解决方案1】:

    我创建了一个非常简单的Quarkus app,它公开了一个 REST API 以列出可用的 GPIO。请注意,它目前使用将在 v1.3.4 中引入的模拟提供程序,因此我可以在本地测试和运行,而无需部署到 Raspberry Pi。

    在 Pi 上运行就像在 pom.xml 中删除对 diozero-provider-mock 的依赖一样简单 - 您目前还需要将依赖更改为 1.3.3,直到 1.3.4 发布。

    基本上你需要将它添加到 application.properties 文件中:

    quarkus.native.additional-build-args=\
        -H:ResourceConfigurationFiles=resource-config.json,\
        -H:JNIConfigurationFiles=jni-config.json,\
        -H:ReflectionConfigurationFiles=reflect-config.json
    

    这些文件是通过运行带有 GraalVM Java 可执行文件的 com.diozero.sampleapps.LEDTest 生成的(有一些小的调整),即:

    $GRAALVM_HOME/bin/java -agentlib:native-image-agent=config-output-dir=config \
      -cp diozero-sampleapps-1.3.4.jar:diozero-core-1.3.4.jar:tinylog-api-2.4.1.jar:tinylog-impl-2.4.1.jar \
      com.diozero.sampleapps.LEDTest 18
    

    请注意,其中很多都是基于我之前对 GraalVM 的实验,如 here 和 here 所述。

    ProcessHandlerImpl 错误似乎与我编辑的 tinylog 反映配置有关。

    【讨论】:

    • 嗨嗨马修!感谢您的加入!您的应用程序在我的机器上成功编译为本机可执行文件,但是一旦我运行它并发出请求 curl -X GET localhost:8080/gpios 它就会给我一个 java.lang.UnsatisfiedLinkError: java.lang.UnsatisfiedLinkError: com.diozero.internal.provider.builtin.gpio.NativeGpioDevice.openChip(Ljava/lang/String;)Lcom/diozero/internal/provider/builtin/gpio/GpioChip;
    • 我将尝试在 Raspberry Pi 上构建 Java 应用程序,使用 agentlib 运行它以获得精确的配置输出,并尝试在 raspberry 上生成本机可执行文件,也许这会成功。
    • 嘿嗨,马特又来了!我将我的解决方案部署到github.com/NanoBreaker/ags-gateway。由于硬件限制,我未能在 raspberry 上编译本机映像,它只有 1GB RAM,我尝试定义 xms 和 xmx,但最终还是失败了。我可以请你试一试吗?
    猜你喜欢
    • 1970-01-01
    • 2020-06-18
    • 2020-12-08
    • 2019-04-01
    • 2015-05-25
    • 1970-01-01
    • 2022-07-12
    • 2020-05-03
    • 2014-10-24
    相关资源
    最近更新 更多