Search code examples
javalinuxarm64buildxquarkus-native

Quarkus Native Application with DioZero on Raspberry Pi using Docker containers (multi-arch)


Yoooo!

Scope

I am trying to deploy a Quarkus based application to a Raspberry Pi using some fancy technologies, my goal is to figure out an easy way to develop an application with Quarkus framework, subsequently deploy as native executable to a raspberry device with full GPIO pins access. Below I will provide you will see requirements that I set for myself and my environment settings to have a better picture of the problem that I faced.

Acceptance Criteria

  1. Java 17
  2. Build native executable using GraalVM
  3. Execute native executable in a micro image on raspberry's docker
  4. Target platform can vary
  5. Be able to use GPIO, SPI, I2C and etc. interfaces of the raspberry

Environment

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

Prerequisites

Application

source code on github

  1. As for project base I used getting-started application from https://github.com/quarkusio/quarkus-quickstarts
  2. Adding diozero library to pom.xml
        <dependency>
            <groupId>com.diozero</groupId>
            <artifactId>diozero-core</artifactId>
            <version>1.3.3</version>
        </dependency>
    
  3. Creating a simple resource to test GPIO pins
    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"]
```

Building image with native executable

Dockerfile based on quarkus docs, I changed image of the build container to quay.io/quarkus/ubi-quarkus-native-image:22.0.0-java17-arm64 and executor container to registry.access.redhat.com/ubi8/ubi-minimal:8.6-902, both of these are linux/arm64* compliant.

Since I am developing and building in linux/amd64 and I want to target linux/arm64/v8 my executable must be created in a target like environment. I can achieve that with buildx feature which enables cross-arch builds for docker images.

  1. Installing QEMU

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

sudo apt-get install -y binfmt-support

  1. Initializing buildx for linux/arm64/v8 builds

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

  1. Use new driver

sudo docker buildx use arm64-v8

  1. Bootstrap driver

sudo docker buildx inspect --bootstrap

  1. Verify

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

Now looks like we're ready to run the build. I ended up with the following command

sudo docker buildx build --push --progress plain --platform linux/arm64/v8 -f Dockerfile -t nanobreaker/agus:arm64 .
  • --push - since I need to deploy a final image somewhere
  • --platform linux/arm64/v8 - docker requires to define target platform
  • -t nanobreaker/agus:arm64 - my target repository for final image

It took ~16 minutes to complete the build and push the image final image on docker hub

  • target platform is linux/arm64 as needed
  • 59.75 MB image size, good enough already (with micro image I could achieve ~10 MB)

After I connected to raspberry, downloaded image and run it

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

quarkus-first-run-arm64 Pretty nice, let's try to execute a http request to test out gpio pins

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

quarkus-first-run-arm64-test Okey, so I see here that there are permission problems and diozero library is not in java.library.path

We can fix permission problems by adding additional parameter to docker run command

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

quarkus-second-run-arm64

PROBLEM

From this point I do not know how to resolve library load error in a native executable.

I've tried:

UPDATE I

It looks like I have two options here

  • Figure out a way to create configuration for the diozero library so it is properly resolved by GraalVM during native image compilation.
  • Add library to the native image and pass it to the native executable.

UPDATE II

Further reading of quarkus docs landed me here https://quarkus.io/guides/writing-native-applications-tips

By default, when building a native executable, GraalVM will not include any of the resources that are on the classpath into the native executable it creates. Resources that are meant to be part of the native executable need to be configured explicitly. Quarkus automatically includes the resources present in META-INF/resources (the web resources) but, outside this directory, you are on your own.

I reached out @Matt Lewis (creator of diozero) and he was kind to share his configs, which he used to compile into GraalVM. Thank you Matt!

Here’s the documentation on my initial tests: https://www.diozero.com/performance/graalvm.html I stashed the GraalVM config here: https://github.com/mattjlewis/diozero/tree/main/src/main/graalvm/config

So combining the knowledge we can enrich pom.xml with additional setting to tell GraalVM how to process our library

<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>

Also added resource-config.json, reflection-config.json, jni-config.json to the resource folder of the project (src/main/resources)

First, I will try to create a native executable in my native os ./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.

Okey, so it failed, but let's trace object instantiation as recommended, maybe we can do something in configs to get around this. I added --trace-object-instantiation=java.lang.ProcessHandleImpl to the additional build args.

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.

something new at least, let's try to initialize it first at build time with --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

Okey, we're not able to change the initialization kind and looks like it won't give us any effect. I found out that with -H:+PrintClassInitialization we can generate a csv file with class initialization info here we have two lines for 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

So it says that class is marked as RERUN, but isn't this the thing we're looking for? Makes no sense for me right now.

UPDATE III

With the configs for graalvm provided by @Matt I was able to compile a native image, but it fails anyways during runtime due to java.lang.UnsatisfiedLinkError, makes me feel like the library was not injected properly.

So looks like we just need to build a proper configuration file, in order to do this let's build our application without native for now, just run it on raspberry, trigger the code related to diozero, get output configs.

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

Deploying to raspberry, will run with graalvm agent for configs generation (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

Running simple requests to trigger diozero code (I've connected a led to raspberry on gpio 4, and was actually seeing it turn off/on)

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

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

I've published project with output configs

One thing I noticed that "pattern":"\\Qlib/linux-aarch64/libdiozero-system-utils.so\\E" aarch64 library gets pulled while running on py which is correct, but when I build on native OS I should specify 'amd64' platform.

Let's try to build a native with new configs

./mvnw package -Dnative

Successfully compiled, let's run and test

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

And here we have error again

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]

So I finally managed to build native image, but for some reason it didn't resolve JNI for native library.

Any thoughts on how to properly inject diozero library into native executable?

UPDATE IV

With help of @matthew-lewis we managed to build aarch64 native executable on amd64 os. I updated the source project with final configurations, but I must inform you that this is not a final solution and it doesn't cover all the library code, also according to the Matt's comments this might not be the only way to configure the graalvm build.


Solution

  • I've created a very simple Quarkus app that exposes a single REST API to list the available GPIOs. Note that it currently uses the mock provider that will be introduced in v1.3.4 so that I can test and run locally without deploying to a Raspberry Pi.

    Running on a Pi would be as simple as removing the dependency to diozero-provider-mock in the pom.xml - you would also currently need to change the dependency to 1.3.3 until 1.3.4 is released.

    Basically you need to add this to the application.properties file:

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

    These files were generated by running com.diozero.sampleapps.LEDTest with the GraalVM Java executable (with a few minor tweaks), i.e.:

    $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
    

    Note a lot of this was based my prior experiments with GraalVM as documented here and here.

    The ProcessHandlerImpl error appear to be related to the tinylog reflect config that I have edited out.

    Update 1

    In making life easy for users of diozero, the library does a bit of static initialisation for things like detecting the local board. This causes issues when loading the most appropriate native library at most once (see LibraryLoader - you will notice it has a static Map of libraries that have been loaded which prevents it being loaded at runtime). To get around this I recommend adding this build property:

    --initialize-at-run-time=com.diozero.sbc\\,com.diozero.util
    

    Next, I have been unable to resolve the java.lang.ProcessHandleImpl issue, which prevents reenabling the service loader (diozero uses service loader quite a bit to enable flexibility and extensibility). It would be nice to be able to add this flag:

    quarkus.native.auto-service-loader-registration=true
    

    Instead I have specified relevant classes in resource-config.json.