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. We observed quite a large cold start time, especially if we use the Hibernate ORM framework. Using this framework also significantly increases the artifact size.
In part 3, we introduced AWS Lambda SnapStart as one of the approaches to reduce the cold start times of the Lambda function. We observed that by enabling the SnapStart on the Lambda function, the cold start time goes down significantly for both sample applications. It's especially noticeable when looking at the "last 70" measurements with the snapshot tiered cache effect. The biggest impact of just enabling the SnapStart is on the application using the Hibernate.
In this part of our article series, we'll introduce how to apply Lambda SnapStart priming techniques. We'll start with the database (in our case, Aurora DSQL) request priming. The goal is to further improve the performance of our Lambda functions.
Sample application with JDBC and Hikari connection pool and the enabled AWS Lambda SnapStart using Aurora DSQL 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 database (in our case, Aurora DSQL) requests, which we implemented in the extra GetProductByIdWithDSQLPrimingHandler class.
public class GetProductByIdWithDSQLPrimingHandler
implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent>, Resource {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ProductDao productDao= new ProductDao();
public GetProductByIdWithDSQLPrimingHandler () {
Core.getGlobalContext().register(this);
}
@Override
public void beforeCheckpoint(org.crac.Context<? extends Resource> context) throws Exception {
productDao.getProductById(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.getProductById(Integer.valueOf(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>
GetProductByIdWithDSQLPrimingHandler 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 products table of the Aurora DSQL database. 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.getProductById(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. PreparedStatement, ResultSet, and many others are among such classes. We also instantiate everything required to establish the connection to the database via JDBC and process the request and response.
We can leave the afterRestore method empty. This is because we don't need to perform any action after the SnapStart snapshot has been restored.
Measurements of cold and warm start times of the Lambda function of the sample application with JDBC and Hikari connection pool
We'll measure the performance of the GetProductByIdJava25WithDSQLAndDSQLPriming Lambda function mapped to the GetProductByIdWithDSQLPrimingHandler shown above. We will trigger it by invoking curl -H "X-API-Key: a6ZbcDefQW12BN56WEDQ25" https://{$API_GATEWAY_URL}/prod/productsWithAuroraPriming/1.
Please read part 2 for the description of how we designed the experiment.
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 Aurora DSQL database 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 17.150 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 | 2336 | 2453 | 2827 | 3026 | 3131 | 3132 | 4.84 | 5.29 | 5.73 | 8.88 | 195.38 | 531 |
| SnapStart enabled but no priming applied, all | 970 | 1058 | 1705 | 1726 | 1734 | 1735 | 4.92 | 5.33 | 5.86 | 9.84 | 198.52 | 1134 |
| SnapStart enabled but no priming applied, last 70 | 901 | 960 | 1061 | 1212 | 1212 | 1212 | 4.84 | 5.29 | 5.77 | 9.54 | 196.94 | 719 |
| SnapStart enabled and DSQL database request priming applied, all | 879 | 980 | 1499 | 1515 | 1518 | 1518 | 4.84 | 5.25 | 5.77 | 9.54 | 163.96 | 914 |
| SnapStart enabled and DSQL database request priming applied, last 70 | 803 | 912 | 996 | 1056 | 1056 | 1056 | 4.84 | 5.25 | 5.77 | 9.54 | 152.61 | 597 |
Sample application with Hibernate and Hikari connection pool and the enabled AWS Lambda SnapStart using Aurora DSQL request priming
We'll reuse the sample application from part 1 and do exactly the same performance measurement as we described in part 2.
We implemented the priming of the database (in our case, Aurora DSQL) request in the extra GetProductByIdWithDSQLPrimingHandler class.
The explanation of what we would like to achieve and how is exactly the same as in the first sample application above. The main difference is that we use the Hibernate framework on top and also preload and preinitialize its classes and abstractions.
Measurements of cold and warm start times of the Lambda function of the sample application with Hibernate and Hikari connection pool
We'll measure the performance of the GetProductByIdJava25WithHibernateAndDSQLAndDSQLPriming Lambda function mapped to the GetProductByIdWithDSQLPrimingHandler shown above. We will trigger it by invoking curl -H "X-API-Key: a6ZbcDefQW12BN56WEHADQ25" https://{$API_GATEWAY_URL}/prod/productsWithAuroraPriming/1.
Please read part 2 for the description of how we designed the experiment.
To show the impact of the SnapStart with the Aurora DSQL database 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 42.333 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 | 6243 | 6625 | 7056 | 8480 | 8651 | 8658 | 5.46 | 5.96 | 6.50 | 9.77 | 200.10 | 707 |
| SnapStart enabled but no priming applied, all | 1277 | 1360 | 3050 | 3103 | 3200 | 3201 | 5.50 | 6.01 | 6.45 | 10.16 | 196.94 | 2349 |
| SnapStart enabled but no priming applied, last 70 | 1258 | 1320 | 1437 | 1634 | 1634 | 1634 | 5.42 | 5.91 | 6.40 | 10.08 | 195.94 | 1093 |
| SnapStart enabled and DSQL database request priming applied, all | 1030 | 1185 | 2310 | 2341 | 2345 | 2347 | 5.33 | 5.91 | 6.50 | 11.64 | 201.70 | 1607 |
| SnapStart enabled and DSQL database request priming applied, last 70 | 970 | 1076 | 1226 | 1511 | 1511 | 1511 | 5.37 | 5.96 | 6.61 | 12.01 | 203.32 | 670 |
Conclusion
In this part of our article series, we introduced how to apply the Lambda SnapStart priming technique, such as Aurora DSQL 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 additionally 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 also 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 Aurora DSQL products 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 NoSQL serverless Amazon DynamoDB database instead of Aurora DSQL 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)