This post provides some experiences from making a simple Java app (well, the actual app does something this is just a demo) that connects to Bitfinex to  get the funding rates run as a GraalVM native image.

xChange is one of the most famous Java libraries for integrating with a large number of crypto exchanges. It provides a simple API that  behind the scenes uses reflection heavily, so it seemed like an interesting challenge. The challenge? Create a simple application that gets the last three 15-minute candles for funding offers. The Java code is as simple as the following:

final Exchange bitfinex = ExchangeFactory.INSTANCE.createExchange(BitfinexExchange.class);
final BitfinexMarketDataServiceRaw bitfinexMarketDataServiceRaw = (BitfinexMarketDataServiceRaw) bitfinex.getMarketDataService();
final List<BitfinexCandle> bitfinexCandles = bitfinexMarketDataServiceRaw.getFundingHistoricCandles("15m", "fBTC", 2, 3);
bitfinexCandles.stream().forEach(System.out::println);

 

Iteration 1: Compile as Java and run on GraalVM JVM in Docker

Native images have many restrictions, so the first attempt was to compile it to Java, create a Docker image and run it on AWS Fargate as a scheduled task to run once every 15 minutes. The Fargate resource allocation is 512MB of Task Memory and 0.25 of vCPU.

Using the IntelliJ New Quarkus project wizard also generates a set of Docker files for fast-jar/native docker images. Creating a fast-jar for Quarkus is as simple as the following:

# Before building the container image run:
#
# ./gradlew build -Dquarkus.package.type=fast-jar
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.fast-jar -t quarkus/quarkus-xchange-native-demo-fast-jar .

The initial attempt created a docker image that was 150MB in size, the application took 9.3 seconds to startup and CPU usage spiked to about 100% and memory to 14% .

 

[io.quarkus] (main) demo on JVM (powered by Quarkus 1.11.1.Final) started in 9.307s

 

cpu-usage-jvm

CPU Usage Docker Image (JVM – Version)

 

memory-usage-jvm

Memory Usage Docker Image (JVM – Version)

 

Not bad, but since this is a very short lived application, it consumes rather a lot of resources. Let’s try to use GraalVM native image generation. For this experiment we used GraalVM version: 21.0.0.2.r11.

Iteration 2: Compile as Native Image and Run in Docker

Creating a native image is a rather challenging process when you include third party libraries, especially if they use reflection. It is important that such features of standard JVM are defined in configuration files as mentioned in the GraalVM documentation.

Native images are built ahead of runtime and their build relies on a static analysis of which code will be reachable. However, this analysis cannot always completely predict all usages of the Java Native Interface (JNI), Java Reflection, Dynamic Proxy objects (java.lang.reflect.Proxy), or class path resources (Class.getResource). Undetected usages of these dynamic features need to be provided to the native-image tool in the form of configuration files.

Luckily GraalVM provides an agent with the ability to automatically generate the GraalVM configuration files.

Iteration 2 – Step 1: Use the Agent to automatically generate the GraalVM native generation configuration files

The xChange library uses reflection a lot. In GraalVM you can explicitly specify which classes use reflection. It provides a method to automatically detect reflection items on a simple run:

java -agentlib:native-image-agent=config-output-dir=./agent-lib-config -jar ./build/quarkus-app/quarkus-run.jar

This code creates a set of files in the agent-lib-config directory that we defined above. GraalVM searches for those files in the src/resources/native-image directory, so we copy/paste all the automatically generated files there.

 

Iteration 2 – Step 2: Run the native image generation using the generated files

Trying to run the native image generation:

./gradlew clean build -Dquarkus.package.type=native

 

The first error that we get is:

Could not resolve com.sun.proxy.$Proxy23 for reflection configuration. Reason: java.lang.ClassNotFoundException: com.sun.proxy.$Proxy23

We need to get rid of those erroneous reflection classes in reflect-config.json. Now we try again the native image generation command above. This time the native-image generation succeeds! Now let’s try running the native image:

./build/quarkus-xchange-native-demo-1.0-SNAPSHOT-runner

This time we get this error:

2021-03-08 02:15:44,947 ERROR [io.qua.run.Application] (main) Failed to start application (with profile prod): java.lang.NoSuchMethodException: javax.ws.rs.QueryParam$$ProxyImpl.value()
        at java.lang.Class.getMethod(DynamicHub.java:1089)
        at si.mazi.rescu.AnnotationUtils.getValueOrNull(AnnotationUtils.java:45)

It appears the problem is in the rescu library that xChange is using, as it was using the class of the annotation for reflection. Patching it was relatively easy. The patched release is tagged here: https://github.com/cyrus13/rescu/commit/aeb94695a7d8073b020a65e08df9ca6b2e17076c

Then I created a version of xChange that uses this version of the library: https://github.com/cyrus13/XChange/releases/tag/native-image-friendly

Using those versions seemed to solve the issue. Now let’s try again:

Iteration 2 – Step 3: Results of execution on AWS

We replaced the docker image of the fast-jar with the native image on AWS. It was running on the same AWS task definition as the JVM image (i.e. allocating the same resources). Consequent runs showed that the native application docker image started in 33ms

[io.quarkus] (main) demo on JVM (powered by Quarkus 1.11.1.Final) started in 0.033s

 

Also CPU usage spiked to around 45% with an average spike of 25%, and memory usage spiked to 4.6%!

 

cpu-usage-docker-native
cpu-usage-docker-native
memory-usage-docker-native
memory-usage-docker-native

 

 

 

 

 

 

Comparison Table

MetricGraalVM JVMNative-Image
Docker Image Size158 MB56 MB
Image Start Time9.307 seconds0.033 seconds
CPU Spikes50 - 100 %7 - 35 %
Memory Spikes5 - 13 %0.8 - 4.8 %

 

Please note that fargate has a minimum charge of 1 minute so this whole optimization is useless from a cost saving point of view, it’s just for the nerdiness of it!

A lamda would be a more appropriate solution from a cost saving perspective. Will try that fairly soon.

Source Code

Source code is available on: https://github.com/cyrus13/anastasakis-net-sample-code/tree/master/quarkus-xchange-native-demo

 

Leave a reply

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

required

Page last modified: 01:20 on March 13, 2021 (UTC+2)