Software engineer with a focus on game development and scalable backend development
Ever since I started making use of Docker I have always been at a turmoil on whether a JVM language was the best solution for the job. You see JVM has a great level of support with loads of libraries a number of different languages and great performance, however this comes at a cost of a large runtime environment and memory requirements (not to mention warm up times). On the other hand there are languages like Go that include everything you need to build a webapp in the standard library, produce a small single binary and dont have large memory footprint or startup times. Go felt like a much leaner language better prepared for this era of microservices and small docker containers, that is until GraalVM came about.
GraalVM is an alternative VM by Oracle to allow developers to use more languages amongst other things, but most importantly for us provides a tool to recompile JVM applications (in our case a “fat jar”) into native applications.
I will be using a Kotlin application using the HTTP4K library in this example, and in particular built using Gradle, however this method should work for any JVM language and any build tool as long as you are able to produce a fat jar containing all dependencies.
The application I am using looks like:
package example
// Imports here
fun main(args: Array<String>) {
val resource = Class::class.java.getResource("/application.conf")
val config = ConfigFactory.parseURL(resource)
fun helloWorld(name: String) = doctype("html") + html {
head {
title("My amazing title") +
script(type = "text/javascript", src = "/static/foo.js") {}
} +
body {
div {
"Hello $name"
}
}
}
val app: HttpHandler = routes(
"/static" bind static(Classpath("/static")),
"/ping" bind Method.GET to { _: Request -> Response(OK).body("pong!") },
"/greet" bind routes(
"/" bind Method.GET to { _: Request -> Response(OK).body(helloWorld("anon!").render()) }
)
)
val portPath = "deployment.port"
val port = if(config.hasPath(portPath)) config.getInt(portPath) else 9000
println("Listening on http://127.0.0.1:$port")
app.asServer(Netty(port)).start()
}
For the most part this is a simple Kotlin application making use of HTTP4K to serve web requests, backed by Netty, celtric/kotlin-html for templates, and some config files in the jar’s resources.
Now first thing to mention is that Graal doesn’t support reflection so anything that makes use of reflection either needs to be accommodated (more on that later) or simply won’t work, this also means that the ClassLoader is a no-no and that for now we use the above Class::class.java.getResource("/application.conf")
syntax (or just Class.class.getResource("/application.conf")
in Java) to load resources rather than going via the class loader.
The application.conf is just a simple json config file detailing the port to use
deployment {
port = 8080
}
And inside a static folder I have a simple javascript file that prints out when loaded.
buildscript {
ext {
kotlin_version = '1.2.50'
http4k_version = '3.31.0'
kotlin_html_version = '0.1.4'
java_target_version = 1.8
config_version = '1.3.3'
}
repositories {
jcenter()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.github.jengelman.gradle.plugins:shadow:2.0.4"
}
}
apply plugin: "application"
mainClassName = "example.ApplicationKt"
apply plugin: "kotlin"
apply plugin: "com.github.johnrengelman.shadow"
sourceCompatibility = java_target_version
targetCompatibility = java_target_version
compileKotlin { kotlinOptions.jvmTarget = "$java_target_version" }
compileTestKotlin { kotlinOptions.jvmTarget = "$java_target_version" }
repositories {
jcenter()
}
dependencies {
implementation files('libs/graalvm-1.0.0-rc2_svm.jar')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.http4k:http4k-core:$http4k_version"
implementation "org.http4k:http4k-server-netty:$http4k_version"
implementation "com.typesafe:config:$config_version"
implementation "org.celtric.kotlin:kotlin-html:$kotlin_html_version"
}
task dockerBuild(type: Exec) {
executable "sh"
args "-c", "docker build -t graal ."
}
dockerBuild.dependsOn(tasks.shadowJar)
For the most part this is a straight forward gradle script, some notable mentions are the use of shadow to build the fat jar and the additional task at the bottom that makes a full jar build and then builds my docker image. It also lists a custom jar as a dependency of our app, this jar includes the required code to configure graal to work with Netty and can be found in the jre/lib/svm/builder folder of the GraalVM release zip. Hopefully this will make its way to maven at some point.
FROM findepi/graalvm:native as builder
# build our application
WORKDIR /builder
ADD ./build/libs/server-all.jar /builder/server.jar
RUN native-image \
--static \
-H:IncludeResources="(.*.conf)|(static/.*)|(META-INF/mime.types)" \
-jar server.jar
RUN rm server.jar
#####
# The actual image to run
#####
FROM alpine:3.7
RUN apk --no-cache add ca-certificates
WORKDIR /app
EXPOSE 8080
COPY --from=builder /builder/server .
CMD ./server
Docker is what is going to build our executable, mostly as the community version of Graal only works for Linux but also because it allows us to have a tidier build environment. Optionally you could build your Java/Kotlin code in the image but for development purposes I am just doing that locally as it lets me reuse my gradle cache and I just upload the jar.
So here we start with an image that has already got graal downloaded and pre-installed, we add our jar as well as a configuration file for graal (more on that later) and then call graals native-image tool to build our executable, this has a number of arguments:
--static
Tells graal to make a statically linked executable as alpine doesn’t come with libc by default.-H:IncludeResources="(.*.conf)|(static/.*)|(META-INF/mime.types)"
By default graal will just process your code, so you need to pass it a regex to inform it if you also want to keep any resources packed in the jar. Here I am keeping all .conf files, anything under static/ and the mime.types file-jar server.jar
Finally the jar we want to process and build the executable from.Finally the executable gets copied onto a fresh alpine image to be ran when needed.
Up to here you should have a fully functioning JVM based web application, it should run fine locally and serve the endpoints listed as expected, however if you tried to build the Docker image as is you will see a number of errors for the native-image step. The issue here is that whilst we have been careful to not use any reflection in our code, Netty makes use of reflection as well as Unsafe pointers internally. The following instructions are mostly inspired by https://medium.com/graalvm/instant-netty-startup-using-graalvm-native-image-generation-ed6f14ff7692
If you are using HTTP4K version 3.31 or later then you can skip this section, however if you are using a different framework build over netty you may need to do some work to circumvent graal’s class stripping.
There are 2 different things you can do depending on whether you have direct access to the code setting up netty or not, for example if you are using Netty directly.
This is really simple, when setting up the ServerBootstrap don’t pass in the class of a server socket channel as this causes Netty to use reflection internally to instantiate new objects of this class, instead pass in a factory that instantiates new objects using the constructor. You can see how HTTP4K does it in more detail here
//Don't do this
// .channel(NioServerSocketChannel::class.java)
//Do this instead
.channelFactory(ChannelFactory<ServerChannel> { NioServerSocketChannel() })
This is ever so slightly more complicated, rather than changing your code to directly use the class we are going to have to tell graal not to strip the NioServerSocketChannel class. To do this we need a new json file containing the following:
[
{
"name": "io.netty.channel.socket.nio.NioServerSocketChannel",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
}
]
This tells graal that we are actually calling the init/constructor function of the NioServerSocketChannel via reflection and that as such it shouldn’t be stripped.
That file will then need to be passed into native-image when calling it via the -H:ReflectionConfigurationFiles=reflectconfig.json
argument.
Alternatively you may be able to create a new NioServerSocketChannel object in your code so that graal sees the class being used and doesn’t strip it, but I haven’t tested this method.
This one is fairly easy to fix, and hopefully fairly easy to understand as well. When the code uses unsafe memory addresses these are computed based on the JDK we are initially compiling the code for, however when recompiling with graal these addresses may need to change, therefore we need to ensure graal does a re-computation of these addresses in order to make sure they still are pointing at the right thing.
Graal provides an easy interface for doing this by telling it to substitute code with other code, in this case specifying that the new code is an unsafe memory address and that it needs to be recomputed.
The com.oracle.svm.core
package is supplied by the jar I mentioned in the gradle section.
package example;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.TargetClass;
@TargetClass(className = "io.netty.util.internal.CleanerJava6")
final class TargetCleanerJava6 {
@Alias
@RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FieldOffset, declClassName = "java.nio.DirectByteBuffer", name = "cleaner")
private static long CLEANER_FIELD_OFFSET;
}
@TargetClass(className = "io.netty.util.internal.PlatformDependent0")
final class TargetPlatformDependent0 {
@Alias
@RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FieldOffset, declClassName = "java.nio.Buffer", name = "address")
private static long ADDRESS_FIELD_OFFSET;
}
@TargetClass(io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess.class)
final class TargetUnsafeRefArrayAccess {
@Alias
@RecomputeFieldValue(kind = RecomputeFieldValue.Kind.ArrayIndexShift, declClass = Object[].class)
public static int REF_ELEMENT_SHIFT;
}
Much like the previous issue, Netty tries to use SLF4J for its logging but this may not be supplied in the jar, instead we can supply a substitution method (making graal rewrite the LoggerFactory method) to supply a standard logger.
package example;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import io.netty.util.internal.logging.InternalLoggerFactory;
import io.netty.util.internal.logging.JdkLoggerFactory;
@TargetClass(io.netty.util.internal.logging.InternalLoggerFactory.class)
final class TargetInternalLoggerFactory {
@Substitute
private static InternalLoggerFactory newDefaultFactory(String name) {
return JdkLoggerFactory.INSTANCE;
}
}
With all of the above done you should now be in a position to build your docker container resulting in a small docker image based off of alpine (or scratch if you want to be really lean) containing your single executable!
On my machine this image is 14mb:
REPOSITORY TAG SIZE
native-alpine latest 14MB
jar-alpine-openjre8 latest 103MB
Running some very simple metrics you can see quite the difference between the jar version and the native:
RUN echo Size comparison; ls -lh; echo; /usr/bin/time -f "Native maxRSS %MkB, real %e, user %U, sys %S" ./server --skip-logs; /usr/bin/time -f "Jar maxRSS %MkB, real %e, user %U, sys %S" java -jar server.jar --skip-logs
---> Running in 0fd240
Size comparison
total 20208
-rwxr-xr-x 1 root root 11.2M Jun 22 10:09 server
-rw-r--r-- 1 root root 8.5M Jun 22 10:08 server.jar
Native maxRSS 47968kB, real 3.11, user 0.01, sys 0.00
Jar maxRSS 187280kB, real 3.53, user 0.56, sys 0.10
For the full example check out this GitHub repo.