DEV Community

Cover image for AWS Lambda SnapStart - Part 3 Measuring Java 11 Lambda cold starts with Quarkus framework
Vadym Kazulkin for AWS Community Builders

Posted on • Updated on

AWS Lambda SnapStart - Part 3 Measuring Java 11 Lambda cold starts with Quarkus framework

Introduction

In the first and second part of the series we talked about the SnapStart in general and made the first tests to compare the cold start of Lambda written in Plain Java with AWS SDK for Java version 2 and using Micronaut Framework with and without SnapStart enabled. We saw that enabling SnapStart led to a huge decrease in the cold start times in both cases. In this part of the series we are going to measure the performance of SnapStart using Quarkus as another popular Java framework.

Quarkus Framework and its features

Quarkus Framework is Kubernetes Native Java stack (personally I don't understand this term :)) tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards. For us it's important that this framework is very well suited for writing Serverless applications. Like Micronaut, it gives us the possibility to create our application using the launcher or CLI, provides API Gateway integration, GraalVM (Native Image) integation, supports Maven und Gradle and much more. It also supports MicroProfile and Funqy for multi cloud solutions. It also processes annotations at compile time and doesn't use reflection, runtime byte code generation, runtime generated proxies and dynamic class loading.

Writing AWS Lambda with Quarkus

We'll use the same application as in the previous parts of this series, but we'll rewrite it to use Quarkus Framework. The code of this sample application can be found here. It provides AWS API Gateway and 2 Lambda functions: "CreateProduct" and "GetProductById". The products are stored in the Amazon DynamoDB. We'll use AWS Serverless Application Model (AWS SAM) for the infrastructure as a code.

Let's look how to implement it with Quarkus. In the AWS SAM Template (template.yaml) we point the AWS Lambda function handler to the generic QuarkusStreamHandler implementation:

Globals:
  Function:
    Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
Enter fullscreen mode Exit fullscreen mode

which comes from the dependency

       <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-amazon-lambda</artifactId>
        </dependency>
Enter fullscreen mode Exit fullscreen mode

declared in pom.xml.

Quarkus supports Spring Boot Annotations like @RestController, @GetMapping, @PostMapping, @PathParam as well as AWS Java SDK and MicroProfile and Quarkus own annotations. In our example we'll use the latter.

This is how GetProductById Lambda function implementation looks like:

@Named("getProductById")
public class GetProductByIdHandler implements RequestHandler<APIGatewayProxyRequestEvent, Optional<Product>> {
      private final ProductDao productDao = new DynamoProductDao();

      @Override
      public Optional<Product> handleRequest(APIGatewayProxyRequestEvent event, Context context) {
            String id = event.getPathParameters().get("id");
            return productDao.getProduct(id);
}
Enter fullscreen mode Exit fullscreen mode

We also have to declare the Product Bean with @RegisterForReflection and helper class Products with @ApplicationScoped annotation

The mapping between Lambda function definition and the code is done through these 2 following steps:

1) Set the value of QUARKUS_LAMBDA_HANDLER environment variable in the AWS SAM Template (template.yaml)

  GetProductByIdFunction:
    Type: AWS::Serverless::Function
.....
      Environment:
        Variables:
          QUARKUS_LAMBDA_HANDLER: getProductById
Enter fullscreen mode Exit fullscreen mode

2) The value of the QUARKUS_LAMBDA_HANDLER environment variable (in this case getProductById) should match the @Named annotation value in the Lambda function handler.

@Named("getProductById")
public class GetProductByIdHandler implements RequestHandler<APIGatewayProxyRequestEvent, Optional<Product>> {

....
Enter fullscreen mode Exit fullscreen mode

QuarkusStreamHandler wires everything together. With this it's possible to define multiple Lambda functions in the same SAM template.

Measuring the cold starts

Let's give the GetProductById Lambda Function 1024 MB of memory and first measure its the cold start without enabling the SnapStart on it. The CloudWatch Logs Insights Query for the /aws/lambda/GetProductByIdWithQuarkus Log Group for it is

filter @type="REPORT" | fields greatest(@initDuration, 0) + @duration as duration, ispresent(@initDuration) as coldStart| stats count(*) as count,
pct(duration, 50) as p50,
pct(duration, 90) as p90,
pct(duration, 99) as p99,
max(duration) as max by coldStart
Enter fullscreen mode Exit fullscreen mode

Here are the results after experiencing 100 cold starts for the same Lambda version:

p50 3164.19
p90 3262.57
p99 3823.99

If we compare these metrics with AWS Lambda with plain Java (and AWS SDK for Java version 2) and with Micronaut we'll notice that the cold starts using the Quarkus Framework are by far the lowest. I'll explore the reason for it in the separate article, but I can assume that it's because of the very perfomant HTTP client required for the Amazon DynamoDB communication.

Since the version 2.15.0. Quarkus officially supports Lambda SnapStart . This means that now we can implement CRaC (Coordinated Restore at Checkpoint) API in the Lambda functions written in Quarkus to further decrease the cold starts (this will be the topic of the separate article).
Let's enable SnapStart GetProductById Lambda Function like this

Image description or directly in the SAM template.

It's also important to define AutoPublishAlias on this function in the SAM template

  GetProductByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: GetProductByIdWithQuarkus
      AutoPublishAlias: liveVersion
...
Enter fullscreen mode Exit fullscreen mode

as we can use SnapStart only on published function versions and aliases that point to versions. In this case alias always points to the latest version by default. The CloudWatch Logs Insights Query for the /aws/lambda/GetProductByIdWithQuarkus Log Group for it is

filter @type = "REPORT"
  | parse @message /Restore Duration: (?<restoreDuration>.*?) ms/
  | stats
count(*) as invocations,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 50) as p50,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 90) as p90,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 99) as p99
group by function, (ispresent(@initDuration) or ispresent(restoreDuration)) as coldstart
  | sort by coldstart desc
Enter fullscreen mode Exit fullscreen mode

Here are the results after experiencing 100 cold starts for the same Lambda version:

p50 1337.16
p90 1374.76
p99 1473.87

If we compare these metrics with AWS Lambda with plain Java (and AWS SDK for Java version 2) and Micronaut Framework with SnapStart enabled, we'll notice that using the Micronaut Framework the average cold start with Quarkus is quite comparable with the first and better than the latter. If we compile our application with GraalVM Native Image and deploy our Lambda as Custom Runtime (which is beyond the scope if this article), we can further reduce the cold start to between 450 and 550ms, see the measurements for the comparable application.

Conclusions and next steps

In this blog post we looked into the Quarkus Framework and learned how to write AWS Lambda function. We also measured the cold start with and without enabling SnapStart for our scenario: Lambda receives the event from the Amazon API Gateway and reads the item from the Amazon DynamoDB. Quarkus showed the lowest cold starts comparing to the plain AWS SDK Version 2 Lambda function and Micronaut framework without SnapStart. With SnapStart enabled the cold start with Quarkus was better than with Micronaut and only slightly behind the plain AWS SDK Version 2 Lambda function. I personally can recommend using Quarkus. The well known programming model is a huge productivity booster. I'll write a separate article about the memory consumption of the different frameworks and look deeper into how the choice of the different HTTP Clients impacts the cold start times.

In the next part of series we'll explore Spring Boot for writing Lambda functions and measure the cold start with and without enabling of the SnapSart.

Update: you can significantly reduce the cold start times of the Lambda function with SnapStart enabled further by applying the optimization technique called priming. Learn more about it in my article.

Top comments (0)