Introduction
In part 1, we introduced our sample application. In part 2, we measured the performance (cold and warm start times) of the Lambda function without any optimizations. What we observed was quite a large cold start time. We introduced AWS Lambda SnapStart in part 3 as one of the approaches to reduce the cold start times of the Lambda function. We saw that by enabling the SnapStart on the Lambda function, the cold start time goes down. It's especially noticeable when looking at the "last 70" measurements with the snapshot tiered cache effect.
In this part of our article series, we'll introduce how to apply Lambda SnapStart priming techniques, starting with DynamoDB request priming. The goal is to further improve the performance of our Lambda functions.
Sample application with the enabled AWS Lambda SnapStart using DynamoDB request priming
We'll reuse the sample application from part 1 and do exactly the same performance measurement as we described in part 2.
Also, please make sure that we have enabled Lambda SnapStart in template.yaml as shown below:
Globals:
Function:
Handler:
SnapStart:
ApplyOn: PublishedVersions
....
Environment:
Variables:
JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
You can read more about the concepts behind the Lambda SnapStart in part 2.
SnapStart and runtime hooks offer you new possibilities to create your Lambda functions for low startup latency. With the pre-snapshot hook, we can prepare our Java application as much as possible for the first call. We load and initialize as much as possible that our Lambda function needs before the Lambda SnapStart creates the snapshot. The name for this technique is priming.
In this article, I will introduce you to the priming of DynamoDB requests, which we implemented in the extra GetProductByIdWithDynamoDBPrimingHandler class.
public class GetProductByIdWithDynamoDBPrimingHandler implements
RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent>, Resource {
private static final ProductDao productDao = new ProductDao();
private static final ObjectMapper objectMapper = new ObjectMapper();
public GetProductByIdWithDynamoDBPrimingHandler () {
Core.getGlobalContext().register(this);
}
@Override
public void beforeCheckpoint(org.crac.Context<? extends Resource> context) throws Exception {
productDao.getProduct("0");
}
@Override
public void afterRestore(org.crac.Context<? extends Resource> context) throws Exception {
}
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) {
String id = requestEvent.getPathParameters().get("id");
Optional<Product> optionalProduct = productDao.getProduct(id);
return new APIGatewayProxyResponseEvent()
.withStatusCode(HttpStatusCode.OK)
.withBody(objectMapper.writeValueAsString(optionalProduct.get()));
....
}
We use Lambda SnapStart CRaC runtime hooks here. To do this, we need to declare the following dependency in pom.xml:
<dependency>
<groupId>io.github.crac</groupId>
<artifactId>org-crac</artifactId>
</dependency>
GetProductByIdWithDynamoDBPrimingHandler class additionally implements org.crac.Resource interface. The class registers itself as a CRaC resource in the constructor of this class. The priming itself happens in the method where we search for the product with the ID equal to 0 in the DynamoDB table. beforeCheckpoint method is a CRaC runtime hook that is invoked before creating the microVM snapshot. We are not even processing the result of the call to productDao.getProduct("0"). The product with the ID equal to zero might not even exist in the database. But with this invocation, Java lazily loads and instantiates all the classes that it requires for this invocation. GetItemRequest, GetItemResponse, and many others are among such classes. Also, the expensive one-time initialization of the HTTP Client (default is Apache HTTP Client) happens. The same is true for the initialization of the Jackson Marshaller for converting Java objects to JSON and vice versa. As the priming happens during the deployment phase of the Lambda function when SnapStart is activated and before the SnapStart snapshot is created, the snapshot will already contain all of this. After the fast snapshot restore phase during the Lambda invoke, we'll gain a lot in performance in case the cold start happens. We can leave the afterRestore method empty. We don't need to perform any action after the SnapStart snapshot has been restored.
By the way, we could also achieve the same result by sending nearly any other request to DynamoDB, for example, DescribeTable. However, I found it easier to reuse some already existing database operations. And we already implemented the getProduct by ID method in our DynamoDB DAO. This is a read request without any side effects.
Measurements of cold and warm start times of our application with Lambda SnapStart and DynamoDB request priming
We'll measure the performance of the GetProductByIdJava25WithDynamoDBAndDDBPriming Lambda function mapped to the GetProductByIdWithDynamoDBPrimingHandler shown above. We will trigger it by invoking curl -H "X-API-Key: a6ZbcDefQW12BN56WEVDDB25" https://{$API_GATEWAY_URL}/prod/productsWithDynamoDBPriming/1.
We designed the experiment exactly as described in part 2.
I will present the Lambda performance measurements with SnapStart being activated for all approx. 100 cold start times (labelled as all in the table), but also for the last approx. 70 (labelled as last 70 in the table). With that, the effect of the snapshot tiered cache, which we described in part 3, becomes visible to you.
To show the impact of the SnapStart with the DynamoDB request priming, we'll also present the Lambda performance measurements from part 2 and part 3.
I did the measurements with java:25.v19 Amazon Corretto version, and the deployed artifact size of this application was 13.796 KB.
Cold (c) and warm (w) start time with -XX:+TieredCompilation -XX:TieredStopAtLevel=1 compilation in ms:
| Approach | 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| No SnapStart enabled | 3800 | 3967 | 4183 | 4411 | 4495 | 4499 | 5.55 | 6.15 | 7.00 | 12.18 | 56.37 | 4000 |
| SnapStart enabled but no priming applied, all | 2294 | 2366 | 3530 | 3547 | 3548 | 3551 | 5.68 | 6.30 | 7.33 | 13.43 | 44.74 | 2923 |
| SnapStart enabled but no priming applied, last 70 | 2247 | 2324 | 2389 | 2637 | 2637 | 2637 | 5.68 | 6.35 | 7.39 | 13.65 | 44.03 | 2051 |
| SnapStart enabled and DynamoDB request priming applied, all | 778 | 817 | 1544 | 1572 | 1601 | 1602 | 5.50 | 6.10 | 6.99 | 12.01 | 34.74 | 933 |
| SnapStart enabled and DynamoDB request priming applied, last 70 | 752 | 790 | 837 | 988 | 988 | 988 | 5.46 | 6.05 | 6.99 | 11.92 | 42.65 | 412 |
Conclusion
In this part of the series, we introduced how to apply Lambda SnapStart priming techniques and started with DynamoDB request priming. The goal was to even further improve the performance of our Lambda functions. We saw that by doing this kind of priming and writing some additional code, we could significantly further reduce the Lambda cold start times compared to simply activating the SnapStart. It's especially noticeable when looking at the "last 70" measurements with the snapshot tiered cache effect. Moreover, we could significantly reduce the maximal value for the Lambda warm start times by preloading classes (as Java lazily loads classes when they are required for the first time) and doing some preinitialization work (by invoking the method to retrieve the product from the DynamoDB table by its ID). Previously, all this happened once during the first warm execution of the Lambda function.
In the next part of our article series, we'll introduce another Lambda SnapStart priming technique. I call it API Gateway Request Event priming (or full priming). We'll then measure the Lambda performance by applying it and comparing the results with other already introduced approaches.
Please also watch out for another series where I use a relational serverless Amazon Aurora DSQL database and additionally the Hibernate ORM framework instead of DynamoDB to do the same Lambda performance measurements.
If you like my content, please follow me on GitHub and give my repositories a star!
Please also check out my website for more technical content and upcoming public speaking activities.
Top comments (0)