GraalVM allows building native applications from Java and other JVM languages. It includes a new just-in-time compiler called Graal and the Truffle framework for building language interpreters. GraalVM also includes Sulong, which allows running LLVM-based languages on the JVM. The presenter discussed using GraalVM to build a native executable for a Zookeeper CLI tool called zkstat from Java code using techniques like ahead-of-time compilation with SubstrateVM, configuring reflection, and building a Docker image for distribution.
2. Agenda
GraalVM? What’s that?
Building native apps in Java (or other JVM languages)
Case study: a CLI app
Reflections on reflection
Building a Docker image from scratch
Conclusion
3. Who’s this guy?
Sylvain Wallez - @bluxte
Software Engineer, Cloud team at Elastic
Member of the Apache Software Foundation
Toulouse JUG co-founder
- we’re hiring!
Started using Java in 1997!
6. GraalVM: a new compiler
Java HotSpot VM
JVM Compiler Interface (JVMCI - JEP 243)
Graal compiler
GC std lib
Truffle framework
Sulong (LLVM)
7. Replacing the bytecode compiler
Problems with C1 & C2
● Code base is old C++ that is hard to understand
● Evolution stalled. Recent changes: mostly intrinsics provided by Intel
Graal is a bytecode compiler written in Java
● Higher level, cleanly organized
● Better inlining and escape analysis
8. GraalVM ecosystem
Truffle framework:
● Easily build language interpreters from an AST
● Support for JS, Ruby, Python, R & WebAssembly
Sulong:
● An LLVM bitcode interpreter
● Run any LLVM language on the JVM: C, C++, Rust, etc.
10. Legal & Pricing
Community Edition
● GPL + Classpath Exception (can safely link with it)
Enterprise edition
● Long term support
● More code optimisations, profiling-guided optimizer
● 25$ (Java SE) + 18$ (GraalVM) per month per processor
14. Why SubstrateVM?
AOT: ahead of time compilation
Produces native executables
● Instant start: no bytecode compilation
→ Great for CLIs and Cloud functions
● Low memory footprint: no metaspace
→ Great for micro services
16. Why not SubstrateVM?
Dynamic Class Loading / Unloading Not supported
Reflection Supported (Requires Configuration)
Dynamic Proxy Supported (Requires Configuration)
InvokeDynamic and Method Handles Not supported
Finalizers Not supported
Serialization Not supported
References Mostly supported
Security Manager Not supported
JVMTI, JMX, other native VM interfaces Not supported
Profiling-based optimization Use GraalVM EE
19. zkstat
A CLI utility to collect statistics on the data stored in ZooKeeper
● Elastic Cloud has 27 ZooKeeper clusters
● Each contains several million nodes
Features
● Dump list of nodes for ingestion in Elasticsearch
● Aggregate statistics by node pattern (count, size, stdev, versions, etc)
● Uses Picocli to parse CLI arguments
20. zkstat: main & picocli
public class ZkStatCLIApp {
public static void main(String[] args) throws Exception {
new ZkStatCLIApp().run(args);
}
public void run(String... args) throws Exception {
CommandLine commandLine = new CommandLine(new TopLevelCommand());
commandLine.addSubcommand("logs-stats", new LogStatsCommand());
commandLine.addSubcommand("node-stats", new NodeStatsCommand());
commandLine.addSubcommand("nodetype-stats", new NodeTypeStatsCommand());
commandLine.addSubcommand("nodetype-stats-csv", new NodeTypeCSVStatsCommand());
System.exit(commandLine.execute(args));
}
...
21. zkstat: main & picocli
@CommandLine.Command(name = "zkstat", mixinStandardHelpOptions = true,
description = "A tool to dump ZooKeeper statistics from ZooKeeper storage folder"
)
class TopLevelCommand implements Callable<Integer> {
public Integer call() throws Exception { return 0; }
}
@CommandLine.Command(name = "logs-stats", description = "Collects transaction log changes into JSON")
class LogStatsCommand implements Callable<Integer> {
@CommandLine.Parameters(index = "0", description = "ZooKeeper datadir")
private String dataDir;
@CommandLine.Parameters(index = "1", description = "Output filename ('-' for stdout)", defaultValue = "-")
private String outputFilename;
@CommandLine.Option(names = {"-t", "--target-index"}, description = "Target index", defaultValue = "zktranlog")
private String targetIndexName;
@Override
public Integer call() throws Exception {
// Do something useful here
}
}
22. zkstat: picocli in action
$ zkstat logs-stats
Missing required parameters: <dataDir>, <outputFilename>
Usage: zkstat logs-stats [-bc] [-t=<targetIndexName>] <dataDir> <outputFilename>
Collects transaction log changes into JSON
<dataDir> ZooKeeper datadir
<outputFilename> Output filename ('-' for stdout)
-b, --bulk Format JSON as a bulk request to ingest into ElasticSearch
-c, --compress Gzipped output
-t, --target-index=<targetIndexName>
Target index
23. zkstat: Gradle build (Kotlin edition)
plugins {
id("java")
}
group = "co.elastic.cloud.zookeeper.stats"
version = "1.0.0"
val appName = "zkstat"
val mainClass = "co.elastic.cloud.zookeeper.stats.ZkStatCLIApp"
repositories {
mavenCentral()
}
dependencies {
implementation("org.apache.zookeeper:zookeeper:3.5.3")
implementation("org.slf4j:slf4j-simple:1.7.25")
implementation("info.picocli:picocli:4.0.4")
testImplementation("junit:junit:4.12")
}
24. From fat jar to native image
tasks.register<Jar>("fatJar") {
archiveBaseName.set("$appName-full")
manifest {
attributes["Main-Class"] = mainClass
}
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
with(tasks.jar.get())
}
$ gradlew fatJar
...
$ ls -lh build/libs/zkstat-full-1.0.0.jar
-rw-r--r-- 1 sylvain staff 2.7M Dec 25 13:37 build/libs/zkstat-full-1.0.0.jar
$ java -jar build/lib/zkstat-full-1.0.0.jar
Usage: zkstat [-hV] [COMMAND]
...
25. From fat jar to native image
plugins {
id("org.mikeneck.graalvm-native-image") version "0.1.1"
}
nativeImage {
setGraalVmHome(System.getProperty("java.home"))
setMainClass(mainClass)
setExecutableName(appName)
if (System.getProperty("os.name") == "Linux") arguments("--static") // To allow "FROM scratch"
}
$ gradlew nativeImage
...
26. From fat jar to native image
$ gradlew nativeImage
> Task :nativeImage
Shutdown Server(pid: 30312, port: 49557)
Build on Server(pid: 36613, port: 53328)*
[zkstat:36613] classlist: 2,303.05 ms
....
[zkstat:36613] universe: 928.73 ms
Warning: Reflection method java.lang.Class.forName invoked at picocli.CommandLine$BuiltIn$ClassConverter.convert(Co
Warning: Reflection method java.lang.Class.newInstance invoked at picocli.CommandLine$DefaultFactory.create(Command
Warning: Reflection method java.lang.Class.getMethods invoked at picocli.CommandLine.getCommandMethods(CommandLine.
Warning: Reflection method java.lang.Class.getDeclaredMethods invoked at picocli.CommandLine.getCommandMethods(Comm
Warning: Reflection method java.lang.Class.getDeclaredMethods invoked at picocli.CommandLine$Model$CommandReflectio
Warning: Reflection method java.lang.Class.getDeclaredConstructor invoked at picocli.CommandLine$DefaultFactory.cre
Warning: Reflection method java.lang.Class.getDeclaredConstructor invoked at picocli.CommandLine$DefaultFactory.cre
Warning: Reflection method java.lang.Class.getDeclaredFields invoked at picocli.CommandLine$Model$CommandReflection
Warning: Aborting stand-alone image build due to reflection use without configuration.
Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Build on Server(pid: 36613, port: 53328)
[zkstat:36613] classlist: 210.49 ms
...
[zkstat:36613] [total]: 22,315.84 ms
Warning: Image 'zkstat' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallba
27. Let’s try with this --no-fallback parameter
From fat jar to native image
$ build/native-image/zkstat logs-stats src dest
Exception in thread "main" picocli.CommandLine$InitializationException:
picocli.CommandLine$AutoHelpMixin is not a command: it has no @Command, @Option, @Parameters or
@Unmatched annotations
at picocli.CommandLine$Model$CommandReflection.validateCommandSpec(CommandLine.java:9731)
at picocli.CommandLine$Model$CommandReflection.extractCommandSpec(CommandLine.java:9566)
at picocli.CommandLine$Model$CommandSpec.forAnnotatedObject(CommandLine.java:5116)
at picocli.CommandLine$Model$CommandSpec.mixinStandardHelpOptions(CommandLine.java:5858)
at picocli.CommandLine$Model$CommandReflection.extractCommandSpec(CommandLine.java:9549)
at picocli.CommandLine$Model$CommandSpec.forAnnotatedObject(CommandLine.java:5116)
at picocli.CommandLine.<init>(CommandLine.java:223)
at picocli.CommandLine.<init>(CommandLine.java:196)
at co.elastic.cloud.zookeeper.stats.ZkStatCLIApp.run(ZkStatCLIApp.java:44)
at co.elastic.cloud.zookeeper.stats.ZkStatCLIApp.main(ZkStatCLIApp.java:40)
28. Configuring reflection
We have to tell native-image:
● what classes are used with reflection
● what classes are proxied
● what resources are loaded from the classpath
● what libraries are load with JNI
Some good builtin heuristics, but cannot guess everything
→ use the tracing agent to create the configs!
$ java -agentlib:native-image-agent=config-output-dir=./graal-config -jar
build/libs/zkstat-full-1.0.0.jar
...
$ ls graal-config
jni-config.json proxy-config.json reflect-config.json resource-config.json
30. Configuring reflection
Picocli comes with an annotation processor that does the job for us!
dependencies {
...
implementation("info.picocli:picocli:4.0.4")
annotationProcessor("info.picocli:picocli-codegen:4.0.4")
}
$ gradlew build
...
$ ls build/classes/java/main/META-INF/native-image/picocli-generated
proxy-config.json reflect-config.json resource-config.json
Configs in META-INF are automatically used by native-image!
32. From fat jar to native image
$ gradlew nativeImage
> Task :nativeImage
Build on Server(pid: 36613, port: 53328)
[zkstat:36613] classlist: 1,004.58 ms
...
[zkstat:36613] write: 370.40 ms
[zkstat:36613] [total]: 27,443.07 ms
BUILD SUCCESSFUL in 32s
4 actionable tasks: 2 executed, 2 up-to-date
$ build/native-image/zkstat
Usage: zkstat [-hV] [COMMAND]
...
$ ls -lh build/native-image/zkstat
-rwxr-xr-x 1 sylvain staff 13M Dec 25 13:37 build/native-image/zkstat*
a yummy standalone executable!
33. Fat jar edition
Startup time - essential for a CLI!
$ time java -jar build/libs/zkstat-full-1.0.0.jar
Usage: zkstat [-hV] [COMMAND]
A tool to dump ZooKeeper statistics from ZooKeeper storage folder
-h, --help Show this help message and exit.
-V, --version Print version information and exit.
Commands:
logs-stats Collects transaction logs changes into JSON
node-stats Collects paths and stats objects from a ZK data dir as CSV
nodetype-stats Computes and outputs as CSV statistics by path type from
a ZK data dir
nodetype-stats-csv Computes and outputs as CSV statistics by path type from
CSV
real 0m0.339s
user 0m0.617s
sys 0m0.080s
RSS 48032 kB
34. Native image edition
Startup time - essential for a CLI!
$ time build/native-image/zkstat
Usage: zkstat [-hV] [COMMAND]
A tool to dump ZooKeeper statistics from ZooKeeper storage folder
-h, --help Show this help message and exit.
-V, --version Print version information and exit.
Commands:
logs-stats Collects transaction logs changes into JSON
node-stats Collects paths and stats objects from a ZK data dir as CSV
nodetype-stats Computes and outputs as CSV statistics by path type from
a ZK data dir
nodetype-stats-csv Computes and outputs as CSV statistics by path type from
CSV
real 0m0.013s
user 0m0.006s
sys 0m0.004s
RSS 5664 kB
real 0m0.339s
user 0m0.617s
sys 0m0.080s
RSS 48032 kB
35. Docker build
FROM oracle/graalvm-ce:19.3.0-java11 as build
RUN gu install native-image
WORKDIR /project
# Download and cache Gradle
COPY gradlew .
COPY gradle ./gradle
RUN ./gradlew
# Download dependencies and cache them separately from the main source code
COPY build.gradle.kts .
RUN ./gradlew downloadDependencies
# Compile and build native image
COPY src ./src
RUN ./gradlew nativeImage
#------------------------------------------------------------------------------
FROM scratch
COPY --from=build /project/build/native-image/zkstat .
CMD ["/zkstat"]
tasks.register("downloadDependencies") {
println("Downloading dependencies")
configurations.testRuntimeClasspath.get().files
}
36. Docker build
$ docker build -t zkstat .
Sending build context to Docker daemon 140.8kB
Step 1/13 : FROM oracle/graalvm-ce:19.3.0-java11 as build
---> bc6f2b723104
Step 2/13 : RUN gu install native-image
---> Using cache
---> a296590b05e6
...
Step 13/13 : CMD ["/zkstat"]
---> Running in 7109549122e8
Removing intermediate container 7109549122e8
---> 3894ce2f74ad
Successfully built 3894ce2f74ad
Successfully tagged zkstat:latest
$ docker image ls zkstat
REPOSITORY TAG IMAGE ID CREATED SIZE
zkstat latest 6547de4b0068 3 hours ago 14.2MB
37. Conclusion
Native-image pros
● Small executable
● Small RAM footprint
● Instant start time
Native-image cons
● May require some setup (reflection)
● We lose a lot of management features
● May not provide optimal performance - but is your code optimal?