Introduction
In the previous parts of our series we measured the cold starts of the Lambda function with Java 21 Corretto runtime without SnapStart enabled, with SnapStart enabled and also applied DynamoDB invocation priming optimization with different Lambda memory settings, deployment artifact sizes and Java compilation options.
In this article we'll now add another dimension to our measurements : the choice of HTTP Client implementation. This is also interesting, because starting from AWS SDK for Java version 2.22 AWS added support for their own implementation of the synchronous CRT HTTP Client. The asynchronous CRT HTTP client has been generally available since February 2023. In this article we'll explore synchronous HTTP clients first and leave asynchronous ones for the next article.
Measuring cold and warm starts with Java 21 using synchronous HTTP clients
In our experiment we'll re-use the application introduced in part 9 for this which you can find here. There are basically 2 Lambda functions which both respond to the API Gateway requests and retrieve product by id received from the API Gateway from DynamoDB. One Lambda function GetProductByIdWithPureJava21Lambda can be used with and without SnapStart and the second one GetProductByIdWithPureJava21LambdaAndPriming uses SnapStart and DynamoDB request invocation priming. We give both Lambda functions 1024 MB memory.
As we did our measurements for cold and warm start with Java 21 using different Lambda memory settings and cold and warm start with Java 21 using different compilation options among others, we have always used the default HTTP Client implementation which is Apache HTTP Client (we'll use the measurements for the comparison in this article), now we'll explore 2 other options as well.
There are now 3 synchronous HTTP Clients implementations available in the AWS SDK for Java.
- Url Connection
- Apache (Default)
- AWS CRT
This is the order for the look up and set of synchronous HTTP Client in the classpath.
Let's figure out how to configure the HTTP Client. There are 2 places to do it : pom.xml and DynamoProductDao
Let's consider 3 scenarios:
Scenario 1) Url Connection HTTP Client. Its configuration looks like this:
In the pom.xml the only enabled HTTP Client dependency has to be:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</dependency>
In DynamoProductDao the DynamoDBClient should be created like this:
DynamoDbClient.builder()
.credentialsProvider(DefaultCredentialsProvider.create())
.region(Region.EU_CENTRAL_1)
.httpClient(UrlConnectionHttpClient.create())
.overrideConfiguration(ClientOverrideConfiguration.builder()
.build())
.build();
Scenario 2) Apache HTTP Client. Its configuration looks like this:
In the pom.xml the only enabled HTTP Client dependency has to be:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
</dependency>
In DynamoProductDao the DynamoDBClient should be created like this:
DynamoDbClient.builder()
.credentialsProvider(DefaultCredentialsProvider.create())
.region(Region.EU_CENTRAL_1)
.httpClient(ApacheHttpClient.create())
.overrideConfiguration(ClientOverrideConfiguration.builder()
.build())
.build();
Scenario 3) AWS CRT synchronous HTTP Client. Its configuration looks like this:
In the pom.xml the only enabled HTTP Client dependency has to be:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-crt-client</artifactId>
</dependency>
In DynamoProductDao the DynamoDBClient should be created like this:
DynamoDbClient.builder()
.credentialsProvider(DefaultCredentialsProvider.create())
.region(Region.EU_CENTRAL_1)
.httpClient(AwsCrtHttpClient.create())
.overrideConfiguration(ClientOverrideConfiguration.builder()
.build())
.build();
For the sake of simplicity, we create all HTTP Clients with their default settings. Of course, there is the optimization potential there to figure out the right HTTP Client settings.
The results of the experiment below were based on reproducing more than 100 cold and approximately 100.000 warm starts with experiment which ran for approximately 1 hour. For it (and experiments from my previous article) I used the load test tool hey, but you can use whatever tool you want, like Serverless-artillery or Postman. I ran all these experiments for all 3 scenarios using 2 different compilation options in template.yaml each:
- no options (tiered compilation will take place)
- JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" (client compilation without profiling)
We found out in the article Measuring cold and warm starts with Java 21 using different compilation options that with them we've got the lowest cold start times.
Let's look into the results of our measurements.
Cold (c) and warm (m) start time with compilation option "tiered compilation" without SnapStart enabled in ms:
Scenario Number | 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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Url Connection | 2855.28 | 2914.9 | 3010.5 | 3172.39 | 3330.78 | 3499.23 | 6.88 | 7.69 | 9.09 | 23.27 | 87.37 | 1377.37 |
Apache | 3175.4 | 3299.88 | 3524.78 | 4056.3 | 4192.12 | 4365.72 | 6.01 | 6.93 | 8.52 | 22.09 | 101.41 | 1479.68 |
AWS CRT | 2441.6 | 2491.03 | 2628.82 | 3161.73 | 3301.39 | 3472.02 | 5.47 | 6.21 | 7.63 | 19.15 | 71.52 | 979.58 |
Cold (c) and warm (m) start time with compilation option "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" (client compilation without profiling) without SnapStart enabled in ms:
Scenario Number | 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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Url Connection | 2908.79 | 2983.56 | 3079.15 | 3599.43 | 3743.08 | 3904.12 | 6.99 | 7.88 | 9.38 | 23.65 | 88.77 | 1410.92 |
Apache | 3157.6 | 3213.85 | 3270.8 | 3428.2 | 3601.12 | 3725.02 | 5.77 | 6.50 | 7.81 | 20.65 | 90.20 | 1423.63 |
AWS CRT | 2454.83 | 2499.49 | 2533.96 | 3503.48 | 3643.76 | 3809.27 | 5.38 | 6.01 | 7.27 | 20.09 | 72.66 | 940.17 |
Cold (c) and warm (m) start time with compilation option "tiered compilation" with SnapStart enabled without Priming in ms:
Scenario Number | 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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Url Connection | 1620.20 | 1722.06 | 2034.88 | 2253.28 | 2280.46 | 2280.93 | 6.93 | 7.87 | 9.38 | 24.69 | 1045.79 | 1536.59 |
Apache | 1649.61 | 1691.35 | 1772.70 | 1920.27 | 1976.74 | 1978.35 | 5.96 | 6.77 | 8.20 | 22.01 | 100.04 | 1234.03 |
AWS CRT | 1190.89 | 1263.23 | 1774.48 | 1924.11 | 1951.22 | 1951.98 | 5.55 | 6.30 | 7.75 | 21.75 | 659.97 | 1385.24 |
Cold (c) and warm (m) start time with compilation option "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" (client compilation without profiling) with SnapStart enabled without Priming in ms:
Scenario Number | 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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Url Connection | 1656.22 | 1732.42 | 2057.37 | 2271.37 | 2366.38 | 2368.53 | 6.93 | 8.00 | 9.68 | 26.31 | 1062.52 | 1411.81 |
Apache | 1626.69 | 1741.10 | 2040.99 | 2219.75 | 2319.54 | 2321.64 | 5.64 | 6.41 | 7.87 | 21.40 | 99.81 | 1355.09 |
AWS CRT | 1206.47 | 1292.61 | 1718.62 | 1918.35 | 1939.56 | 1941.03 | 5.47 | 6.20 | 7.51 | 21.07 | 629.28 | 1421.73 |
Cold (c) and warm (m) start time with compilation option "tiered compilation" with SnapStart enabled and with DynamoDB invocation Priming in ms:
Scenario Number | 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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Url Connection | 716.02 | 768.68 | 971.23 | 1210.09 | 1243.19 | 1243.58 | 6.66 | 7.57 | 9.16 | 22.37 | 147.83 | 339.71 |
Apache | 686.59 | 734.88 | 821.92 | 921.12 | 938.78 | 938.79 | 5.73 | 6.61 | 8.00 | 21.74 | 130.73 | 260.48 |
AWS CRT | 692.10 | 771.76 | 1131.71 | 1244.44 | 1354.78 | 1355.8 | 5.64 | 6.41 | 7.63 | 21.07 | 163.26 | 881.70 |
Cold (c) and warm start (m) time with compilation option "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" (client compilation without profiling) with SnapStart enabled and with DynamoDB invocation Priming in ms:
Scenario Number | 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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Url Connection | 703.26 | 783.42 | 945.37 | 1124.94 | 1136.24 | 1136.52 | 6.82 | 7.69 | 9.24 | 24.61 | 145.49 | 297.71 |
Apache | 702.55 | 759.52 | 1038.50 | 1169.66 | 1179.05 | 1179.36 | 5.73 | 6.51 | 7.87 | 21.75 | 92.19 | 328.41 |
AWS CRT | 679.09 | 718.89 | 1044.74 | 1170.83 | 1194.47 | 1195.04 | 5.38 | 6.11 | 7.51 | 20.09 | 153.22 | 974.18 |
Let's visualize our measurements for p90 and draw the conclusions. "SLA" is abbreviation for the compilation option "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" and "tiered" stays for the "tiered compilation".
Conclusion
Generally speaking, as we've already figured out in Measuring cold and warm starts with Java 21 using different compilation options, "tiered compilation" is the preferred choice over "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" (client compilation without profiling) in case SnapStart enabled and other way around in case if SnapStart isn't enabled, but the results are really close to each other.
In terms of the HTTP Client choice, AWS CRT HTTP Client is preferred choice in case SnapStart isn't enabled or SnapStart is enabled but no priming is applied. In case of priming of the DynamoDB invocation, the results in terms of the cold starts for all 3 HTTP Clients are close to each other as the initialization of the DynamoDB Client with the HTTP Client and the most expensive first invocation (priming) happens already during the deployment phase of the Lambda function and doesn't impact the further invocations that much. The Apache HTTP Client is probably the most powerful choice but it shows the worst results for SnapStart not being enabled.
The warm execution times are more or less close to each other for all 3 HTTP clients and compilation options.
Can we reduce the cold start a bit further? From our article Measuring cold starts with Java 21 using different deployment artifact sizes we know that smaller deployment artifact sizes lead to the lower cold start times. The usage of AWS CRT HTTP Client adds 18 MB to the deployment artifact size for our sample application (total size 32MB versus 14 MB for URL Connection and Apache HTTP Clients). If we look into the deployment artifact with AWS CRT HTTP Client, we'll discover the following additional packages for each operating system : linux, osx and windows.
If we take a look into those folders, we'll see for example the following content for the linux folder (the same applies for windows and osx folders) :
As we see the content of such folders is natives file for each operating system and processor architecture: for osx it's libaws-crt-jni.dylib file, for windows - aws-crt-jni.dll and for linux - libaws-crt-jni.so. If we already know that we'll run our Lambda only on Linux x86 architecture, we can delete the osx and windows folders completely and subfolders for arm architecture in the linux folder. This will reduce the deployment artifact size from 32 to 19 MB for AWS CRT HTTP Client and further reduce the cold start time a bit.
The choice of HTTP Client is not only about minimizing cold and warm starts. The decision is much more complex end also depends on the functionality of the HTTP Client implementation and its settings, like whether it supports HTTP/2. AWS publshed the decision tree which HTTP client to choose depending on the criteria.
In the next article of the series we'll make the same measurements but for the asynchronous HTTP Clients.
Update on 06.06.2024. For the CRT client we can set classifier (i.e. linux-x86_64) in our POM file to only pick the relevant binary for our platform. See here. Big thanks to Maximilian Schellhorn for the hint!
Top comments (0)