RichyHBM

Software engineer with a focus on scalable backend development

Compiling a Kotlin & Netty WebApp with GraalVM

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.

Setup

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 Program

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.

Gradle file

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.

Dockerfile

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:

Finally the executable gets copied onto a fresh alpine image to be ran when needed.

Netty + GraalVM

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

Reflection

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.

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() })

Using framework built on Netty

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.

Unsafe pointers usage

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

Incomplete classpath

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

Results

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.

Comments