Reducing Clojure Lambda Cold Starts Part 10 - Rust?

Search for a command to run...

No comments yet. Be the first to comment.
Where Lambda cold starts often get worse in other runtimes is when you start adding dependencies, particularly an AWS SDK dependency. Let's see how Rust fares with an S3 client dependency. Updating Cargo.toml: [package] name = "tax_engine_experiments...
If you are using the v2 AWS SDK clients, you can configure them like so: (ns my.s3 (:import (java.net URI) (software.amazon.awssdk.regions Region) (software.amazon.awssdk.services.s3 S3Client) (software.amazon.awssdk.services.s3.mo...
Last time we created our Rust and Typescript Lambdas with basic hello world implementations and did a quick performance comparison. We'll now expand our Rust and Typescript Lambdas from last time into ones that take data from SQS messages and push th...

In this series, I will be investigating throughput tuning for a Lambda that receives SQS events, reads data from S3 object, and blasts the data into DynamoDB. While I'm at it, I'll do a performance shootout between Rust and Typescript versions, attem...

Where Lambda cold starts often get worse in other runtimes is when you start adding dependencies, particularly an AWS SDK dependency. Let's see how Rust fares with an S3 client dependency. Updating Cargo.toml: [package] name = "tax_engine_experiments...

Rust seems to be at the height of the hype cycle right now even among functional programming enthusiasts. Although it's not a true functional programming language, due to not having first-class support for immutable data structures, its ownership model does provide the some of same safety guarantees as immutability. Its core library also comes stock with a lot of the high-level programming features I can't live without, although supposedly still being really fast with its "no cost abstractions". It all seems too good to be true. Let's investigate, with a particular eye towards Lambda.
I create a new project with:
>> cargo new tax_engine_experiments_rust --bin
Modify src/main.rs:
use lambda_runtime::{handler_fn, Context, Error};
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Error> {
let func = handler_fn(func);
lambda_runtime::run(func).await?;
Ok(())
}
async fn func(event: Value, _: Context) -> Result<Value, Error> {
println!("EVENT: {}", event);
Ok(json!({ "message": format!("Hello Event, {}!", event) }))
}
And modify Cargo.toml to look like this:
[package]
name = "tax_engine_experiments_rust"
version = "0.1.0"
edition = "2021"
autobins = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lambda_runtime = "0.4.1"
tokio = { version = "1.0", features = ["macros", "io-util", "sync", "rt-multi-thread"] }
serde = "^1"
serde_json = "^1"
serde_derive = "^1"
[[bin]]
name = "bootstrap"
path = "src/main.rs"
Then follow the instructions here to compile using Docker, but slightly differently:
>> LAMBDA_ARCH="linux/arm64"
>> RUST_TARGET="aarch64-unknown-linux-gnu"
>> RUST_VERSION="latest"
>>docker run \
--platform ${LAMBDA_ARCH} \
--rm --user "$(id -u)":"$(id -g)" \
-v "${PWD}":/usr/src/myapp -w /usr/src/myapp rust:${RUST_VERSION} \
cargo build --release --target ${RUST_TARGET}
Then I create lambda.zip:
cp ./target/aarch64-unknown-linux-gnu/release/bootstrap ./bootstrap && zip lambda.zip bootstrap && rm bootstrap
Add a test event file:
{
"Records": [
{
"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
"receiptHandle": "MessageReceiptHandle",
"body": "{\"bucket\": \"tax-engine-experiments-2-transactionsbucket-78gg1f219mel\", \"key\": \"test.json\"}",
"attributes": {
"ApproximateReceiveCount": "1",
"SentTimestamp": "1523232000000",
"SenderId": "123456789012",
"ApproximateFirstReceiveTimestamp": "1523232000001"
},
"messageAttributes": {},
"md5OfBody": "{{{md5_of_body}}}",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue",
"awsRegion": "us-east-1"
}
]
}
Add a template.yml:
AWSTemplateFormatVersion: "2010-09-09"
Transform:
- "AWS::Serverless-2016-10-31"
Parameters:
TransactionsBucket:
Type: String
Default: tax-engine-experiments-2-transactionsbucket-78gg1f219mel
CalculationsBucket:
Type: String
Default: tax-engine-experiments-2-calculationsbucket-aivptjt1j82w
Resources:
RunRustCalculationsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub "${AWS::StackName}-run-calcs-queue-rust"
VisibilityTimeout: 5400
RunCalculationsRust:
Type: AWS::Serverless::Function
Properties:
Architectures:
- arm64
FunctionName: !Sub "${AWS::StackName}-run-calcs-rust"
Handler: none
Runtime: provided.al2
CodeUri: lambda.zip
Timeout: 900
MemorySize: 512
Policies:
- AWSLambdaBasicExecutionRole
- S3ReadPolicy:
BucketName: !Ref TransactionsBucket
- S3WritePolicy:
BucketName: !Ref CalculationsBucket
Environment:
Variables:
RUST_BACKTRACE: 1
TRANSACTIONS_BUCKET: !Ref TransactionsBucket
CALCULATIONS_BUCKET: !Ref CalculationsBucket
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt RunRustCalculationsQueue.Arn
BatchSize: 1
Note: I tried using serverless-rust as it seemed like a quick way to get up and running with Rust Lambdas, but I use AWS SSO and it doesn't work with Serverless without a bunch of uncomfortable hacks, so I reverted to SAM.
Then invoke with SAM Local:
>> sam local invoke -e test-event.json
Looks good:
Invoking none (provided.al2)
Decompressing /Users/larry/Documents/code/tax_engine_experiments_rust/lambda.zip
Skip pulling image and use local one: public.ecr.aws/sam/emulation-provided.al2:rapid-1.35.0-arm64.
Mounting /private/var/folders/2w/hrc_hrn52nq8n80c7j64tb0c0000gp/T/tmpru54maap as /var/task:ro,delegated inside runtime container
START RequestId: 6706ea25-0135-424f-af83-b8ac0be0eaac Version: $LATEST
EVENT: {"Records":[{"attributes":{"ApproximateFirstReceiveTimestamp":"1523232000001","ApproximateReceiveCount":"1","SenderId":"123456789012","SentTimestamp":"1523232000000"},"awsRegion":"us-east-1","body":"{\"bucket\": \"tax-engine-experiments-2-transactionsbucket-78gg1f219mel\", \"key\": \"test.json\"}","eventSource":"aws:sqs","eventSourceARN":"arn:aws:sqs:us-east-1:123456789012:MyQueue","md5OfBody":"{{{md5_of_body}}}","messageAttributes":{},"messageId":"19dd0b57-b21e-4ac1-bd88-01bbb068cb78","receiptHandle":"MessageReceiptHandle"}]}
END RequestId: 6706ea25-0135-424f-af83-b8ac0be0eaac
REPORT RequestId: 6706ea25-0135-424f-af83-b8ac0be0eaac Init Duration: 1.46 ms Duration: 86.13 ms Billed Duration: 100 ms Memory Size: 512 MB Max Memory Used: 512 MB
{"message":"Hello Event: {\"Records\":[{\"attributes\":{\"ApproximateFirstReceiveTimestamp\":\"1523232000001\",\"ApproximateReceiveCount\":\"1\",\"SenderId\":\"123456789012\",\"SentTimestamp\":\"1523232000000\"},\"awsRegion\":\"us-east-1\",\"body\":\"{\\\"bucket\\\": \\\"tax-engine-experiments-2-transactionsbucket-78gg1f219mel\\\", \\\"key\\\": \\\"test.json\\\"}\",\"eventSource\":\"aws:sqs\",\"eventSourceARN\":\"arn:aws:sqs:us-east-1:123456789012:MyQueue\",\"md5OfBody\":\"{{{md5_of_body}}}\",\"messageAttributes\":{},\"messageId\":\"19dd0b57-b21e-4ac1-bd88-01bbb068cb78\",\"receiptHandle\":\"MessageReceiptHandle\"}]}!"}%
Deploying and running in the Lambda console I get:

It does seem to be very fast:
Duration: 0.88 ms Billed Duration: 17 ms Memory Size: 512 MB Max Memory Used: 13 MB Init Duration: 15.95 ms
We'll see how it does with an S3 dependency and our calculations workload in my next post.