DEV Community

Vadym Kazulkin
Vadym Kazulkin

Posted on

Quarkus 3 application on AWS Lambda- Part 9 Measuring Lambda cold and warm starts with GraalVM Native Image and REST API

Introduction

In the part 8 of this article series, we learned how to develop a pure Quarkus REST application and deploy it on AWS Lambda. We also measure the performance (cold and warm start time times) of Lambda function.

In this article series, we'll convert this sample application from part 8 into the GraalVM Native image and deploy it as AWS Lambda Custom Runtime and measure its performance.

Quarkus 3 sample REST API application on AWS Lambda with GraalVM Native Image

In the part 5 of this article, series we've already described the steps to convert the Quarkus 3 application into GraalVM Native Image. We need to perform the similar steps to convert our Qurakus REST application on AWS Lambda into the GraalVM Native (25) Image and deploy it as AWS Lambda Custom Runtime and measure its performance.

The final application is published in the quarkus-3.26-as-graalvm-native-image-rest-api repository.

As far as the source code of the application is concerned, the ReflectionConfig class has been added. In this class, we use @RegisterForReflection annotation to define the classes that are only loaded at runtime.

@RegisterForReflection(targets = {
    APIGatewayProxyRequestEvent.class,
    HashSet.class, 
    APIGatewayProxyRequestEvent.ProxyRequestContext.class, 
    APIGatewayProxyRequestEvent.RequestIdentity.class,
        DateTime.class,
        Product.class,
        Products.class,
})
Enter fullscreen mode Exit fullscreen mode

Since GraalVM uses Native Image Ahead-of-Time compilation, we need to provide such classes in advance, otherwise ClassNotFound errors will be thrown at runtime. This includes custom entity classes like Product and Products, some AWS dependencies to APIGateway Proxy Event Request (from the artifact id aws-lambda-java-events from pom.xml), DateTime class to convert timestamp from JSON to Java object and some other classes. It sometimes takes several attempts running the application first to find all such classes.

There are other ways to register classes for Reflection, which are described in this article Tips for writing native applications.

In the pom.xml there are a few more additional declarations necessary. First, we need to use Amazon DynamoDB Client Quarkus extension from Quarkiverse as follows:

 <dependency>
   <groupId>io.quarkiverse.amazonservices</groupId>
   <artifactId>quarkus-amazon-dynamodb</artifactId>
    <version>3.2.0</version>
 </dependency>
Enter fullscreen mode Exit fullscreen mode

With out it, I ran into different errors when building GraalVM Native Image :

Fatal error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected. If these objects should not be stored in the image heap, you can use 

    '--trace-object-instantiation=java.util.Random'

to find classes that instantiate these objects. Once you found such a class, you can mark it explicitly for run time initialization with 

    '--initialize-at-run-time=<culprit>'

to prevent the instantiation of the object.
The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option --initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Object was reached by
  reading field software.amazon.awssdk.core.retry.backoff.FullJitterBackoffStrategy.random of constant 
    software.amazon.awssdk.core.retry.backoff.FullJitterBackoffStrategy@1121cb59: FullJitterBackoffStrategy(baseDelay=PT0.025S, maxBackoffTime=PT20S)
  scanning root software.amazon.awssdk.core.retry.backoff.FullJitterBackoffStrategy@1121cb59: FullJitterBackoffStrategy(baseDelay=PT0.025S, maxBackoffTime=PT20S) embedded in
    software.amazon.awssdk.services.dynamodb.DynamoDbRetryPolicy.retryPolicyFor(DynamoDbRetryPolicy.java:109)
  parsing method software.amazon.awssdk.services.dynamodb.DynamoDbRetryPolicy.retryPolicyFor(DynamoDbRetryPolicy.java:106) reachable via the parsing context
    at software.amazon.awssdk.services.dynamodb.DynamoDbRetryPolicy.resolveRetryStrategy(DynamoDbRetryPolicy.java:94)
    at software.amazon.awssdk.services.dynamodb.DefaultDynamoDbBaseClientBuilder.finalizeServiceConfiguration(DefaultDynamoDbBaseClientBuilder.java:110)
    at software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder.finalizeChildConfiguration(AwsDefaultClientBuilder.java:171)
    at software.amazon.awssdk.core.client.builder.SdkDefaultClientBuilder.syncClientConfiguration(SdkDefaultClientBuilder.java:202)
Enter fullscreen mode Exit fullscreen mode

Because Quarkus will initialize all classes at build time it means that we can't initialize the value of the private static final String PRODUCT_TABLE_NAME = System.getenv("PRODUCT_TABLE_NAME"); in the static initializer block of the DynamoProductDao, as the value will be null at the build time. But what we can do in our case is to move retrieving table name (which is very cheap) from the environment variable to each method invocation which uses it, for example like this:

 @Override
 public Optional<Product> getProduct(String id) {
  String PRODUCT_TABLE_NAME = System.getenv("PRODUCT_TABLE_NAME");
  GetItemResponse getItemResponse = dynamoDbClient.getItem(GetItemRequest.builder()
      .key(Map.of("PK", AttributeValue.builder().s(id).build()))
      .tableName(PRODUCT_TABLE_NAME)
      .build());
  if (getItemResponse.hasItem()) {
      return Optional.of(ProductMapper.productFromDynamoDB(getItemResponse.item()));
  } else {
    return Optional.empty();
   }
 }
Enter fullscreen mode Exit fullscreen mode

This works well for this type of problem but might not be applicable (or require much more complex code changes) for others. See the full explanation of this issue in the part 5.

Generally speaking, Quarkus 3 currently doesn't work well with GraalVM Native Image due to these described workarounds which I didn't have to do when working with Spring Boot 3 on Lambda.

Measurements of the Lambda cold and warm start times of our application with GraalVM Native Image

In the following, we will measure the performance of our GetProductByIdFunction Lambda function (see AWS SAM template), which we will trigger by invoking curl -H "X-API-Key: a6ZbcDefQW12BN56WES326R" https://{$API_GATEWAY_URL}/prod/products/1. I used GraalVM version 25 in my measurements.

The results of the experiment are based on reproducing more than 100 cold starts and about 100,000 warm starts with the Lambda function GetProductByIdFunction (we ask for the already existing product with ID=1) for the duration of about 1 hour. We give Lambda function 1024 MB memory, which is a good trade-off between performance and cost. We also use (default) x86 Lambda architecture. For the load tests I used the load test tool hey, but you can use whatever tool you want, like Serverless-artillery or Postman.

Cold (c) and warm (w) start time with in ms:

c p50 c p75 c p90 c p99 c p99.9 c max w p50 w p75 w p90 w p99 w p99.9 w max
719 734 751 814 819 819 5.42 5.91 6.66 11.92 28.18 235

Conclusion

In this article series, we converted our sample Quarkus 3 REST application from part 8 into the GraalVM Native image, deployed it as AWS Lambda Custom Runtime and measured its performance.

Compared to the performance of the pure Quarkus 3 application on AWS Lambda converted into the GraalVM Native image and deployed as AWS Lambda Custom Runtime measured in the part 5, we observe much higher cold start times and slightly higher warm start times for the Quarkus 3 REST application.

Top comments (0)