Savings in AWS Lambda Functions Powered by AWS Graviton2 Processors

Higher throughput with lower cost

Amir Keibi
Slalom Build
7 min readSep 29, 2021

--

AWS Lambda is a serverless compute service that lets you run code without provisioning or managing servers. Until now, you had no control over the CPU architecture that your functions used. The only resource-related property has been the amount of memory allocated to each instance.

This has changed with the introduction of 2nd generation AWS Graviton Processor, a custom processor from AWS with 64-bit Arm Neoverse cores and is targeted for optimizing cloud-native workloads. Given that Slalom is an AWS Premier Consulting Partner, we have invested heavily in serverless technology. As a result, this compute architecture will be beneficial to us and our clients. This post will demonstrate how to utilize it in AWS Lambda functions and highlight performance improvements and cost savings that come with it.

We will build a simple Lambda function with Java (Corretto 11) and deploy it twice through an AWS SAM template, with and without the new architecture. This allows us to invoke them and compare the results. We don’t have to worry about recompiling our code for different architectures since Java code is compiled into bytecode. This is unlike other languages such as Go where the code ought to be recompiled for 64-bit ARM architecture. All the code for this post is available on GitHub, as well.

Although Lambda supports various development languages, only the following support this new architecture at launch:

  • Python 3.8 & 3.9
  • Node 12 & 14
  • .NET Core 3.1
  • Java 8.al2, 11
  • Ruby 2.7
  • C/C++ with custom Runtime (provided.al2)
  • Docker Images (arm64 images, images targeting multi-CPU architecture are not supported)

Code and AWS SAM Template

The Lambda function calculates the number of combinations. This will answer a question such as this: “how many unique permutations can we create from 20 numbers in a way that each permutation contains exactly 3 numbers?” (n=20, k=3) For example, imagine a sequence of numbers from 1 to 20. Some of the combinations are “1,2,3”, “2,3,4”, “1,3,4” and so on and so forth. Note that “1,2,3” is the same as “2,1,3”. The formula to calculate the number of combinations is n!/(k! * (n-k)!).

We can optimize this function by merging the first 2 loops, but I made no such change to keep the code easier to read.

/// Calculates n!/(k! * (n-k)!)
private int calculateCombinations(int n, int k) {
double kFactorial = 1;
for (double i = 1; i <= k; i++)
kFactorial *= i;

double nMinuskFactorial = 1;
for (double i = 1; i <= n - k; i++)
nMinuskFactorial *= i;

double nFactorial = nMinuskFactorial;
for (double i = n - k + 1; i <= n; i++)
nFactorial *= i;

// the result is most definitely an integer
return (int) (nFactorial / (kFactorial * nMinuskFactorial));
}

We will deploy a REST API via API Gateway for each function with two path parameters for the two input parameters in the above function. For good measure, we also validate the input in the Lambda function to ensure we’re receiving integers: nis between 1 and 40 and kis less than or equal to n.

In addition to the above code, an AWS SAM template (template.yml) deploys the entire stack. The following snippet highlights the changes introduced for this feature.

gravitonTesterFunction:
Type: AWS::Lambda::Function
Properties:
--- REMOVED FOR BREVITY ----
Layers:
- !Ref gravitonLibraries
Architectures:
- arm64

gravitonLibraries:
Type: AWS::Lambda::LayerVersion
Properties:
--- REMOVED FOR BREVITY ----
CompatibleArchitectures:
- arm64
- x86_64

Although we’re using “AWS::Lambda::*” namespace (note AWS::Lambda::Function and AWS::Lambda::LayerVersion resource types in previous snippet), “AWS::Serverless::*” also support this new feature as shown in the following snippet:

gravitonTesterFunction:
Type: AWS::Serverless::Function
Properties:
--- REMOVED FOR BREVITY ----
Layers:
- !Ref gravitonLibraries
Architectures:
- arm64

The template file in our code contains a commented-out example.

Building and Deploying the Lambda Function

The steps to build and deploy are:

  1. Build and package the function and its dependency JARs into their own zip files. The dependencies are packaged separately to be deployed as a Lambda Layer. Since we update the function more frequently than its dependencies, this will help reduce the size of the package and consequently expedite the deployment.
  2. Use AWS CLI to upload the zip files into S3. This will also transform the template replacing the path to zip files with S3 URIs.
  3. Use AWS CLI to deploy the stack using the transformed template. The stack, as mentioned earlier, consists of 2 instances of the function and their corresponding API resources.
$  ./gradlew -q clean packageLibs && mv build/distributions/graviton2.zip build/distributions/graviton2-lib.zip && ./gradlew -q build$  aws cloudformation package --template-file template.yml --s3-bucket graviton2-tester-src --output-template-file out.yml$  aws cloudformation deploy --template-file out.yml --stack-name graviton2 --capabilities CAPABILITY_NAMED_IAM --region us-east-1

Please bear in mind the following before deploying the code:

  • Application is built with gradlew. This wrapper script pulls the latest version of gradle the very first time it runs. Thus, one doesn’t need to install it separately.
  • Ensure the CLI is up-to-date. I’m using v2.2.35 to run above commands.
  • Prior to these steps, you have to create a S3 bucket which is referenced in these commands. Always keep that bucket private. Instead of using public buckets, configure the CLI correctly to be able to upload zip files.
  • The first 2 steps are also in a buildspec file in the repo if you choose to use AWS Code Build and AWS Code Pipeline to build and deploy the functions.
  • The documentation suggests following configuration for deploying a function using a zip file:
Function:
Type: AWS::Lambda::Function
Properties:
Runtime: java11
PackageType: Zip
Code:
S3Bucket: bucket-name
S3Key: zip-file-in-the-bucket

However, we’d like CLI’s packge command to upload the zip file with a unique name in order to avoid manual steps. In order to achieve this, simply set the Code property to the zip file’s path on disk and CLI will transform it correctly.

gravitonTesterFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: java11
PackageType: Zip
Code: build/distributions/graviton2.zip
-- becomes --gravitonTesterFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: java11
PackageType: Zip
Code:
S3Bucket:
graviton2-tester-src
S3Key: 6d00f130fd9bd2055370a0c254657184

The same approach works for the Layer.

gravitonLibraries:
Type: AWS::Lambda::LayerVersion
Properties:
Content: build/distributions/graviton2-lib.zip
-- becomes --gravitonLibraries:
Type: AWS::Lambda::LayerVersion
Properties:
Content:
S3Bucket:
graviton2-tester-src
S3Key: b0e37ac39faf4aa50fa9b49381bbdeb9

The end result should look like this:

API Gateway Configuration
Lambda Function’s Runtime settings which now has a new “Architecture” field

Testing and Results

We’ll call these functions through their respective API methods and wait for a while to build enough metrics for comparison. To produce realistic enough result, I use Artillery. Artillery Core is free and easy to use.

npm install -g artillery@latest
artillery run --output report.json artilleryTest.yml

A test file (artilleryTest) is included in the code. Update the target URL before running the test. The following example, runs for 15 minutes and adds 180 calls every second.

config:
target: "https://xxxxxx.amazonaws.com/default" <-- Update this
phases:
- duration: 900
arrivalRate: 180
scenarios:
- flow:
- get:
url: "/graviton-combinations/40/6"
headers:
Content-Type: "application/json"
expect:
- statusCode: 200

Putting together the cost curve from AWS and visual reports from Artillery creates the following narrative:

Result from Lambda/API utilizing Graviton V2
Result from Lambda/API utilizing default architecture

Given similar workloads (Request over time graph), the duration of execution in Lambda functions using Graviton is lower (Duration graph). This is observable throughout the test. Moreover, the cost associated with the new architecture is lower as well. Although, each cost curve only highlights the highest value in each test ($97.96 vs $106.26), the overall cost throughout the test remains lower.

The lower cost is effectively driven by a better throughput. As it can be seen in the next graph, the number of concurrent executions for the same demand is lower using the new architecture (207 vs 241). The first and last tests in this graph are the 2 tests we’ve highlighted in this article.

Lower concurrent executions using arm64 architecture

It’s important to note that:

  1. One might notice small anomalies at the beginning when measuring metrics. These would be associated with Lambda’s cold start. Hence, they’re not taken into account in above graphs.
  2. This is a small test in nature and meant as a starting point. A more comprehensive test with more Lambda functions, longer period of execution and larger number of requests should reveal even bigger savings.
  3. These tests were planned far apart to ensure shared resources were scaled in/down and the conditions under which they ran were similar.

Clean up

Since everything is deployed through CloudFormation (through aws cloudformation deploy), cleanup is as effortless as deleting the stack.

Summary

The new CPU architecture clearly provides a better throughput, costs less and is easy to use. I encourage you to clone the code repository and measure this yourself.

It’d be easy to scale out this test in order to simulate a more realistic example. For example, you could declare the sample function in template.yml a few more times or utilize a cloud native test runner, which is not bound to your local resources for generating requests. However, if you’re already taking advantage of Lambda in your applications, that would be a better place to start. Having said that, a change such as this should be accompanied with metrics. If you’re not measuring performance metrics for your Lambda functions today, start by building a benchmark.

Happy saving!

Glossary

CLI command reference

https://docs.aws.amazon.com/cli/latest/reference/cloudformation/index.html

SAM Template Anatomy

https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy.html

Lambda function metrics

https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics.html

Lambda Layers

https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html

Getting started with AWS graviton

https://github.com/aws/aws-graviton-getting-started

--

--