Beat Java cold starts in AWS Lambda's with GraalVM

Profile picturePosted by Joery Vreijsen on 19-10-2020

Let’s face it, against all advice from your co-workers, friends, or even family, you still want to build a blazingly fast lambda with Java. Either to prove a point, to win a bet, or because you are just as much a Java geek as me.

With this blogpost I’m going to walk you through pre-compiling your Java lambda with GraalVM [1] to eliminate slow cold-start times.

Cold starts, what, where, and when?

On the first invocation of your lambda, AWS boots up a container with the right runtime before actually calling your code. When using runtimes like NodeJs or Python, the cold start times are between 200-250 ms [2] while with Java it takes atleast 650 ms if not more depending on the specifics of your function code.

Java’s cold start is mainly caused by the JVM, which is started by default when using AWS’ pre-configured Java runtime. Luckily AWS also provides us the option to create our own custom runtime which allows us to use GraalVM to pre-compile our Java code into a binary that can be ran without the need of a JVM.

Let’s get practical

Let’s take a simple Java program like our Lambda Authorizer from the previous blogpost [3], which can be found on Github [4], and start enabling it for GraalVM processing.

AWS Lambda Runtime API

As we are going to build our own custom runtime, we have to interact with the AWS Lambda Runtime API [5] ourselves. On this API there are three endpoints that we want to use:

A simple bootstrap class to handle this polling of the AWS Runtime API can be found on Github [6].

Reflection

One of the downsides of pre-compiling with GraalVM is that it can’t handle reflection very well, which means that we have to create a reflect.json configuration file, where we can point GraalVM to the classes it should prepare for reflective use. Since we use Jackson as our serialization library we should configure all classes that need (de)serialization in the reflect.json file.

Example entry in the reflect.json configuration file:

1
2
3
4
5
6
7
8
[
  ...
  {
    "name": "nl.theguild.lambda.model.DefaultResponse",
    "allPublicMethods" : true
  },
  ...
]

For the complete reflect.json check Github [7]

Compiling the image!

Yes! The interesting part! Now that we prepared our little Java project for native-image compilation let’s see how it is done.

First we need to make sure our jar comes with a proper manifest stating our mainClass, which can be done by adding the following config to our pom.xml.

1
2
3
4
5
6
7
8
9
10
11
<plugin>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.0.2</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>nl.theguild.lambda.Main</mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>

We then can start creating a simple bash script that will preform all the necessary steps to end up with our custom-runtime zip.

Step 1: Create our jar file.

1
mvn clean install;

Step 2: Run GraalVM inside a docker container, and mount our project.

1
docker run --rm --name graal -v $(pwd):/${PATH_TO_PROJECT} oracle/graalvm-ce:19.2.0

Step 3: Install and run GraalVM’s native-image command while including our reflect.json configuration, and enabling http.

1
2
3
4
5
gu install native-image;
native-image \
    -H:EnableURLProtocols=http \
    -H:ReflectionConfigurationFiles=${PATH_TO_PROJECT}/reflect.json \
    -jar ${PATH_TO_JAR};

Step 4: Move our native image to a folder which we can later on zip for usage in the AWS Lambda.

1
2
mkdir /${PATH_TO_PROJECT}/target/custom-runtime;
cp ${BINARY_RESULT} ${PATH_TO_PROJECT}/target/custom-runtime;

Step 5: Create a bootstrap file that instructs AWS on how to run the native image.

AWS defines in their documentation [8] that every custom-runtime should be a zip including a bootstrap shell file that can jump start the function.

In our case that bootstrap file can be a simple one looking something like this:

1
2
3
#!/bin/sh
set -euo pipefail
./${PROJECT_NAME}

Adding all those steps into one single bash script results in the following file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
PROJECT_NAME=aws-enriching-lambda-authorizer
PROJECT_VERSION=1.0.0

# Generate Jar file
mvn clean install;

# Generate Native Image
docker run --rm --name graal -v $(pwd):/${PROJECT_NAME} oracle/graalvm-ce:19.2.0 \
    /bin/bash -c "gu install native-image; \
                  native-image \
		                -H:ReflectionConfigurationFiles=/${PROJECT_NAME}/reflect.json \
                    -jar /${PROJECT_NAME}/target/${PROJECT_NAME}-${PROJECT_VERSION}.jar \
                    ; \
                    mkdir /${PROJECT_NAME}/target/custom-runtime \
                    ; \
                    cp ${PROJECT_NAME}-${PROJECT_VERSION} /${PROJECT_NAME}/target/custom-runtime/${PROJECT_NAME}";

echo -e "#!/bin/sh \n \
set -euo pipefail \n \
./${PROJECT_NAME}" > target/custom-runtime/bootstrap;

# Make bootstrap executable
chmod +x target/custom-runtime/bootstrap;

# Zip
rm $PROJECT_NAME-custom-runtime.zip
cd target/custom-runtime || exit
zip -X -r ../../$PROJECT_NAME-custom-runtime.zip .

HOORAY!

We now have our aws-enriching-lambda-authorizer-custom-runtime.zip that we can use in our AWS Lambda and enjoy our rapid fast cold start timings… WHILE USING JAVA!

Navigate to the AWS Console -> Lambda -> Create function, choose custom-runtime -> use default bootstrap and click create.

Now that we have a lambda, we can go to Actions and upload our zip file, after which we can configure a test-event.

As our authorizer lambda is triggered by the api-gateway we can use the default Amazon API Gateway AWS Proxy event, and simply add an "Authorization": "Bearer 12345" header to the request.

When we now hit test we see our lambda responding in just 283ms, matching the cold-start times of other runtimes, show that to your co-workers, friends, and family!

Source

Github: https://github.com/VR4J/aws-enriching-lambda-authorizer/tree/feature/graal-vm

References

[1] https://www.graalvm.org/docs/introduction/ [2] https://levelup.gitconnected.com
[3] https://www.kabisa.nl/tech/enriching-requests-with-an-aws-lambda-authorizer
[4] https://github.com/VR4J/aws-enriching-lambda-authorizer
[5] https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html
[6] https://github.com/VR4J/aws-enriching-lambda-authorizer/blob/feature/graal-vm/src/main/java/nl/theguild/lambda/Main.java
[7] https://github.com/VR4J/aws-enriching-lambda-authorizer/blob/feature/graal-vm/reflect.json
[8] https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html

Profile picture

Joery Vreijsen

Software developer • Java & AWS Guru • Github: VR4J • Twitter: @JoeryVreijsen

Bij Kabisa staat privacy hoog in het vaandel. Wij vinden het belangrijk dat er zorgvuldig wordt omgegaan met de data die onze bezoekers achterlaten. Zo zult u op onze website geen tracking-cookies vinden van third-parties zoals Facebook, Hotjar of Hubspot. Er worden alleen cookies geplaatst van Google en Vimeo. Deze worden gebruikt voor analyses, om zo de gebruikerservaring van onze websitebezoekers te kunnen verbeteren. Tevens zorgen deze cookies ervoor dat er relevante advertenties worden getoond. Lees meer over het gebruik van cookies in ons privacy statement.