SQS To DynamoDB Tuning - Setting Up Rust And Typescript Lambdas

Search for a command to run...

No comments yet. Be the first to comment.
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...
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...

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 mod...

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, attempting to optimize each as much as possible to create a fair comparison.
Create a new Rust project:
cargo new sqs_ddb_rust --bin
Initialize Typescript and Webpack:
npm init
npm install -g typescript
npm install -g webpack webpack-cli
npm install --save-dev @tsconfig/recommended
Typescript
Adding Lambda event type definitions:
npm install --save-dev @types/aws-lambda
Add a Webpack config so I can minimize my TS Lambda size (webpack.config.js):
const path = require('path');
module.exports = {
mode: 'production',
target: "node",
entry: './target/js/index.js',
output: {
library: {"name": "blaster", "type": "this"},
filename: 'index.js',
path: path.resolve(__dirname, 'target/js'),
}
};
Add tsconfig.json:
{
"extends": "@tsconfig/node14/tsconfig.json",
"include": ["src/ts/*"],
"compilerOptions": {
"outDir": "target/js"
}
}
Rust
Cargo.toml:
[package]
name = "sqs_to_ddb"
version = "0.1.0"
edition = "2021"
# 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_json = "^1"
Rust
I want to set things up so I can have multiple handlers and have the binaries output under the name of the handler file, so I first:
mkdir src/bin
mv src/main.rs src/bin/blaster_handler.rs
Then in src/bin/blaster_handler.rs:
use lambda_runtime::{handler_fn, Context, Error as LambdaError};
use serde_json::{Value};
#[tokio::main]
async fn main() -> Result<(), LambdaError> {
let func = handler_fn(func);
lambda_runtime::run(func).await?;
Ok(())
}
async fn func(event: Value, _: Context) -> Result<(), LambdaError> {
println!("Hello Event: {}", serde_json::to_string(&event).unwrap());
Ok(())
}
Typescript:
src/ts/index.ts:
import { SQSEvent, SQSHandler } from "aws-lambda";
export const handler: SQSHandler = async (event: SQSEvent) => {
console.log(`Hello Event: ${JSON.stringify(event)}`);
}
I want to be able to build everything with just the 'sam build' command, so I use a Makefile to do this.
Makefile:
build-BlasterLambdaTS:
tsc
webpack
cp ./target/js/index.js $(ARTIFACTS_DIR)
build-BlasterLambdaRust:
docker run --platform linux/arm64 \
--rm --user "$(id -u)":"$(id -g)" \
-v "$(PWD)":/usr/src/myapp -w /usr/src/myapp rust:latest \
cargo build --release --target aarch64-unknown-linux-gnu
cp ./target/aarch64-unknown-linux-gnu/release/blaster_handler $(ARTIFACTS_DIR)/bootstrap
template.yml:
AWSTemplateFormatVersion: "2010-09-09"
Transform:
- "AWS::Serverless-2016-10-31"
Resources:
BlasterLambdaRust:
Type: AWS::Serverless::Function
Properties:
Architectures:
- arm64
Handler: none
Runtime: provided.al2
CodeUri: .
Timeout: 30
MemorySize: 512
Policies:
- AWSLambdaBasicExecutionRole
Metadata:
BuildMethod: makefile
BlasterLambdaTS:
Type: AWS::Serverless::Function
Properties:
Architectures:
- arm64
Handler: index.blaster.handler
Runtime: nodejs14.x
CodeUri: .
Timeout: 30
MemorySize: 512
Policies:
- AWSLambdaBasicExecutionRole
Metadata:
BuildMethod: makefile
One interesting thing to note here is that I'm using the 'arm64' architecture. I've found that Lambdas running on this not only run faster than on x86, but are also less expensive. More information here . I'll perhaps do some comparisons later in the series.
Adding a test-event.json (copied from here ):
{
"Records": [
{
"messageId": "059f36b4-87a3-44ab-83d2-661975830a7d",
"receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...",
"body": "Test message.",
"attributes": {
"ApproximateReceiveCount": "1",
"SentTimestamp": "1545082649183",
"SenderId": "AIDAIENQZJOLO23YVJ4VO",
"ApproximateFirstReceiveTimestamp": "1545082649185"
},
"messageAttributes": {},
"md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
"awsRegion": "us-east-2"
},
{
"messageId": "2e1424d4-f796-459a-8184-9c92662be6da",
"receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...",
"body": "Test message.",
"attributes": {
"ApproximateReceiveCount": "1",
"SentTimestamp": "1545082650636",
"SenderId": "AIDAIENQZJOLO23YVJ4VO",
"ApproximateFirstReceiveTimestamp": "1545082650649"
},
"messageAttributes": {},
"md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
"awsRegion": "us-east-2"
}
]
}
Now I can run SAM Local:
sam build
sam local invoke BlasterLambdaRust -e test-event.json
sam local invoke BlasterLambdaTS -e test-event.json
Both functions succeed and print out the input event. Lets deploy and test:
sam deploy
Testing the TS version gives this the two invocations:
Duration: 3.80 ms Billed Duration: 4 ms Memory Size: 512 MB Max Memory Used: 55 MB Init Duration: 161.25 ms
Duration: 10.60 ms Billed Duration: 11 ms Memory Size: 512 MB Max Memory Used: 56 MB
Testing the Rust version gives this for the first two invocations:
Duration: 0.93 ms Billed Duration: 18 ms Memory Size: 512 MB Max Memory Used: 13 MB Init Duration: 16.90 ms
Duration: 0.71 ms Billed Duration: 1 ms Memory Size: 512 MB Max Memory Used: 13 MB
We'll gather more samples next time to get a better idea of the comparison, but it is remarkable how much lower the init durations, overall durations, and memory usage are with Rust. Another interesting thing is how the node version doesn't include the init duration in the billed duration, I wonder if it's a bug or a bonus for using Node.
Next time we'll also start adding code to read from an S3 object and send it line-by-line into DynamoDB. We'll gather some metrics and do some comparisons between the two runtimes.