DEV Community

Cover image for GraalVM: build regular Micronaut Kotlin/Java lambdas, run natively

GraalVM: build regular Micronaut Kotlin/Java lambdas, run natively

bearmug profile image Pavel Fadeev ・4 min read

Finally! It’s time to forget about those cold starts and scheduled warm-up calls. Simply deploy Java (or Kotlin) natively compiled code to AWS Lambda! Or maybe not yet? The post is an attempt to answer this question with an experiment like:

  • deploy natively compiled (by GraalVM native-image tool) Kotlin and Java lambdas to AWS cloud
  • have the same setup with Node.js lambda nearby
  • bridge incoming calls through AWS API Gateway
  • run basic tests to measure cold start timings and see behaviour after warm-up

Project setup

Project boilerplate has been generated with Micronaut tooling and provisioned to AWS infrastructure using Serverless framework. Load testing delivered with K6. Feel free to explore related step-by-step setup guide to deploy the same setup and play with load tests.

Load scenarios

#1 “cold” starts evaluation

  • each lambda deployed 10 times (which guarantees to have init run for the first call after deployment)
  • the deployment followed by 10 seconds load test. The load test is strictly sequential, all requests issued one after another.
  • client-side statistics captured from K6 log output, AWS-side figures taken from CloudWatch Logs Insights with simplest queries.

#2 “warm” runs

  • each lambda deployed once and called to trigger initial iteration
  • deployment followed by 10 iterations, each with the same 10 seconds sequential load test

load test scenarios could be found here: cold-start run, warm run

Measurements and their visualization

Client-side measurements

  • “cold” start outliers are clearly visible. Their value reaches ~7.5 sec with no effort
  • on a bright side, other min/max/percentile figures distributed quite evenly across tested lambdas. JVM/native max numbers leaning a little towards bigger values
  • these slight fluctuations could be caused by the network layer as well. Next AWS-side measurements section might be used for cross-checks.
    client-side latency measurements data
    client-side latency measurements data

AWS-side measurements

  • Node.js min numbers 0.84/0.79 ms(!) are invisible at this log-scale chart
  • alas, JVM-based lambdas show “cold” start numbers up to 5 sec. Add ~2.5 sec init time on top of this. Predictably sad
  • native Kotlin/Java lambdas’ performance looks fairly acceptable. Even the worst iteration kept close to 180 ms
  • it is worth to mention that enterprise GraalVM version features an opportunity to tweak performance with profile-guided optimizations. At the moment GraalVM Enterprise Edition licensed for free testing, evaluation, or for developing non-production applications only. Still, there is a hope to see this tooling within CE version as well…
  • init timings for native images (~330 ms) are much closer to Node.js ones (~160 ms)
    AWS-side latency measurements data (init time is out of scope)
    AWS-side latency measurements data (init time is out of scope)

here you may find exact numbers for client-side and AWS-side runs, including init timings and billing calculations

Summary notes

  • Java/Kotlin JVM lambdas are still experiencing “cold” start issues. These ~5–7 seconds is just a matter of fact today. Further manipulations with artefact size and classpath might help with init timings, but…
  • GraalVM native lambdas have sustainable performance profile. Init time (~330ms) comparable to Node.js reference lambda (~160ms) and way better than for JVM (~2.5 sec)
  • At the same time, GraalVM/Micronaut/Serverless combo instantly leverages from Java/Kotlin existing ecosystem, reach tooling and active community
  • Node.js timings are really good and very stable. Median is barely different from 99%-ile
  • in fact, Java and Kotlin -based native images have the same size, 14.62 MB. JVM images sizing is a little different: 38.58 MB (Java) and 44.37 MB (Kotlin). And Node.js size is just 297 bytes :)
  • Last but not least, this project is intentionally bent from autogenerated Micronaut template. This short journey has proven two things. #1: a lot of issues may arise during migration to *.gradle.kts, restructuring multi-module Gradle project and native-image configuration tweaks. #2: either these issues solved multiple times or solvable with reasonable efforts.

You may run described deployments and tests using project’s github repo.

Discussion (0)

Editor guide