Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Blazing fast serverless with Rust

Blazing fast serverless with Rust

© 2022

Avatar for Daniele Frasca

Daniele Frasca

November 28, 2022

More Decks by Daniele Frasca

Other Decks in Technology

Transcript

  1. Lambda Optimisation Time Start new execution environment Download the code

    Bootstrap the runtime Execute initialization code Execute handler code Small packages Use Lambda Execution Context Code Avoid Fat Lambda Cache assets in the /tmp dir Graviton2 Use parallelism when possible @dfrasca80
  2. Rust Designed to be a safe and highly performant language

    Cross-Platform development and support Static-Typing ensures easy maintainable Has an expansive ecosystem @dfrasca80
  3. Ownership is a set of rules that governs how a

    Rust program manages memory. Garbage Collector I declare my variable and the language runtime takes care I don't need to worry about what goes on the stack and heap Move the data itself and give up ownership in the process Create a copy of the data and pass that along Pass a reference to the data and retain ownership, letting the recipient borrow it for a while @dfrasca80
  4. Ownership fn main(){ let s1 = give_up_ownership(); let s2 =

    String::from("hello world"); let s3 = create_a_copy_pass_it_along(s2); let s3_len = pass_reference(&s2); println!("s1 :{} - s3: {} - s3 len: {}", s1, s2, s3_len); } @dfrasca80
  5. Ownership fn main(){ let s1 = give_up_ownership(); let s2 =

    String::from("hello world"); let s3 = create_a_copy_pass_it_along(s2); let s3_len = pass_reference(&s2); println!("s1 :{} - s3: {} - s3 len: {}", s1, s2, s3_len); } fn give_up_ownership() -> String { let something = String::from("ciao"); something } @dfrasca80
  6. Ownership fn main(){ let s1 = give_up_ownership(); let s2 =

    String::from("hello world"); let s3 = create_a_copy_pass_it_along(s2); let s3_len = pass_reference(&s2); println!("s1 :{} - s3: {} - s3 len: {}", s1, s2, s3_len); } fn give_up_ownership() -> String { let something = String::from("ciao"); something } fn create_a_copy_pass_it_along(input: String) -> String { // do something input } @dfrasca80
  7. Ownership fn main(){ let s1 = give_up_ownership(); let s2 =

    String::from("hello world"); let s3 = create_a_copy_pass_it_along(s2); let s3_len = pass_reference(&s2); println!("s1 :{} - s3: {} - s3 len: {}", s1, s2, s3_len); } fn pass_reference(input: &String) -> String { // do something input.len() } @dfrasca80
  8. Ownership fn main(){ let s1 = give_up_ownership(); let s2 =

    String::from("hello world"); let s3 = create_a_copy_pass_it_along(s2); let s3_len = pass_reference(&s2); println!("s1 :{} - s3: {} - s3 len: {}", s1, s2, s3_len); } fn pass_reference(input: &String) -> String { // do something input.len() } @dfrasca80
  9. Ownership fn main(){ let s1 = give_up_ownership(); let s2 =

    String::from("hello world"); let s3 = create_a_copy_pass_it_along(s2); let s3_len = pass_reference(&s3); println!("s1 :{} - s3: {} - s3 len: {}", s1, s2, s3_len); } fn pass_reference(input: &String) -> String { // do something input.len() } @dfrasca80
  10. Ownership fn main(){ let s1 = give_up_ownership(); let s2 =

    String::from("hello world"); let s3 = create_a_copy_pass_it_along(s2); let s3_len = pass_reference(&s3); println!("s1 :{} - s3: {} - s3 len: {}", s1, s2, s3_len); } fn pass_reference(input: &String) -> String { // do something input.len() } @dfrasca80
  11. Struct and Traits A struct, or structure, is a custom

    data type that lets you package together and name multiple related values that make up a meaningful group. A trait abstractly defines shared behaviour. For example, we can use trait bounds to specify that a generic type can be any type that has particular behaviour. pub struct Tweet { pub username: String, pub content: String, } pub trait Summary { fn summarise(&self) -> String; } @dfrasca80
  12. Enum and Pattern Matching Enum allows enumerating all possible variants.

    Match pattern allow you to compare a value against a set of patterns. enum ResponseKind { Good, Bad, } enum Coin { Ten, Twenty, } enum ResponseKind { Good, Bad(String), } fn value_in_cents(coin: &Coin) -> u8 { match coin { Coin::Ten => 10, Coin::Twenty => 20, _ => 0, } @dfrasca80
  13. Option and Result Type Option represents an optional value: every

    Option is either Some and contains a value or None. Type Result can represent either success/Ok or failure/Err. enum Coin { Ten, Twenty, } async fn do() -> Result<MyDto, Error> {} fn value_in_cents(coin: &Coin) -> Option<u8> { match coin { Coin::Ten => Some<10>, Coin::Twenty => Some<20>, _ => None, } @dfrasca80
  14. AWS for Rust The AWS Serverless Application Model (AWS SAM)

    is an open-source framework that you can use to build a serverless application. ServerlessLand patterns to quickly build integrations using AWS SAM, CDK, Terraform and Serverless Framework. @dfrasca80
  15. Handler Anatomy use aws_sdk_dynamodb::{model::AttributeValue, Client}; use lambda_http::{service_fn, Body, Error, IntoResponse,

    Request, RequestExt, Response}; use std::env; #[tokio::main] async fn main() -> Result<(), Error> { let config = aws_config::load_from_env().await; let table_name = env::var("TABLE_NAME").expect("TABLE_NAME must be set"); let dynamodb_client = Client::new(&config); lambda_http::run(service_fn(|request: Request| { put_item(&dynamodb_client, &table_name, request) })) .await?; Ok(()) } async fn put_item(client: &Client, table_name: &str, request: Request) -> Result<T, E> { ... } #[cfg(test)] mod tests {} @dfrasca80
  16. Handler Anatomy use aws_sdk_dynamodb::{model::AttributeValue, Client}; use lambda_http::{service_fn, Body, Error, IntoResponse,

    Request, RequestExt, Response}; use std::env; #[tokio::main] async fn main() -> Result<(), Error> { let config = aws_config::load_from_env().await; let table_name = env::var("TABLE_NAME").expect("TABLE_NAME must be set"); let dynamodb_client = Client::new(&config); lambda_http::run(service_fn(|request: Request| { put_item(&dynamodb_client, &table_name, request) })) .await?; Ok(()) } async fn put_item(client: &Client, table_name: &str, request: Request) -> Result<T, E> { ... } #[cfg(test)] mod tests {} @dfrasca80
  17. Handler Anatomy use aws_sdk_dynamodb::{model::AttributeValue, Client}; use lambda_http::{service_fn, Body, Error, IntoResponse,

    Request, RequestExt, Response}; use std::env; #[tokio::main] async fn main() -> Result<(), Error> { let config = aws_config::load_from_env().await; let table_name = env::var("TABLE_NAME").expect("TABLE_NAME must be set"); let dynamodb_client = Client::new(&config); lambda_http::run(service_fn(|request: Request| { put_item(&dynamodb_client, &table_name, request) })) .await?; Ok(()) } async fn put_item(client: &Client, table_name: &str, request: Request) -> Result<T, E> { ... } #[cfg(test)] mod tests {} @dfrasca80
  18. Handler Anatomy async fn put_item(client: &Client, table_name: &str,request: Request) ->

    Result<impl IntoResponse, Error> { // Extract path parameter from request let path_parameters = request.path_parameters(); let id = match path_parameters.first("id") { Some(id) => id, None => return Ok(Response::builder().status(400).body("id is required")?), }; let body = match request.body() { Body::Empty => "".to_string(), Body::Text(body) => body.clone(), Body::Binary(body) => String::from_utf8_lossy(body).to_string(), }; let res = client .put_item() .table_name(table_name) .item("id", AttributeValue::S(id.to_string())) .item("payload", AttributeValue::S(body)) .send() .await; match res { Ok(_) => Ok(Response::builder().status(200).body("item saved")?), Err(_) => Ok(Response::builder().status(500).body("internal error")?), } } @dfrasca80
  19. Handler Anatomy async fn put_item(client: &Client, table_name: &str,request: Request) ->

    Result<impl IntoResponse, Error> { let path_parameters = request.path_parameters(); let id = match path_parameters.first("id") { Some(id) => id, None => return Ok(Response::builder().status(400).body("id is required")?), }; // Extract body from request let body = match request.body() { Body::Empty => "".to_string(), Body::Text(body) => body.clone(), Body::Binary(body) => String::from_utf8_lossy(body).to_string(), }; let res = client .put_item() .table_name(table_name) .item("id", AttributeValue::S(id.to_string())) .item("payload", AttributeValue::S(body)) .send() .await; match res { Ok(_) => Ok(Response::builder().status(200).body("item saved")?), Err(_) => Ok(Response::builder().status(500).body("internal error")?), } } @dfrasca80
  20. Handler Anatomy async fn put_item(client: &Client, table_name: &str,request: Request) ->

    Result<impl IntoResponse, Error> { let path_parameters = request.path_parameters(); let id = match path_parameters.first("id") { Some(id) => id, None => return Ok(Response::builder().status(400).body("id is required")?), }; let body = match request.body() { Body::Empty => "".to_string(), Body::Text(body) => body.clone(), Body::Binary(body) => String::from_utf8_lossy(body).to_string(), }; // Put the item in the DynamoDB table let res = client .put_item() .table_name(table_name) .item("id", AttributeValue::S(id.to_string())) .item("payload", AttributeValue::S(body)) .send() .await; match res { Ok(_) => Ok(Response::builder().status(200).body("item saved")?), Err(_) => Ok(Response::builder().status(500).body("internal error")?), } } @dfrasca80
  21. Handler Anatomy async fn put_item(client: &Client, table_name: &str,request: Request) ->

    Result<impl IntoResponse, Error> { let path_parameters = request.path_parameters(); let id = match path_parameters.first("id") { Some(id) => id, None => return Ok(Response::builder().status(400).body("id is required")?), }; let body = match request.body() { Body::Empty => "".to_string(), Body::Text(body) => body.clone(), Body::Binary(body) => String::from_utf8_lossy(body).to_string(), }; let res = client .put_item() .table_name(table_name) .item("id", AttributeValue::S(id.to_string())) .item("payload", AttributeValue::S(body)) .send() .await; // Return a response to the end-user match res { Ok(_) => Ok(Response::builder().status(200).body("item saved")?), Err(_) => Ok(Response::builder().status(500).body("internal error")?), } } @dfrasca80
  22. Unit Test Anatomy #[cfg(test)] mod tests { use …libs imports…

    async fn get_mock_config() -> Config { let cfg = aws_config::from_env() .region(Region::new("eu-west-1")) .credentials_provider(Credentials::from_keys("access_key", "privatekey", None)) .load().await; Config::new(&cfg) } fn get_request_builder() -> http::request::Builder { http::Request::builder() .header("content-type", "application/x-amz-json-1.0") .uri(http::uri::Uri::from_static( "https://dynamodb.eu-west-1.amazonaws.com/", )) } #[tokio::test] async fn test_put_item() {} } @dfrasca80
  23. Unit Test Anatomy #[cfg(test)] mod tests { use ...imports... async

    fn get_mock_config() -> Config {} fn get_request_builder() -> http::request::Builder {} #[tokio::test] async fn test_put_item() { // Mock DynamoDB client let conn = TestConnection::new(vec![( get_request_builder() .header("x-amz-target", "DynamoDB_20120810.PutItem") .body(SdkBody::from( r#"{"TableName":"test","Item":{"id":{"S":"1"},"payload":{"S":"test1” }}}"#)).unwrap(), http::Response::builder() .status(200) .body(SdkBody::from( r#"{"Attributes": {"id": {"S": "1"}, "payload": {"S": "test1"}}}"#, )).unwrap(), )]); let client = Client::from_conf_conn( get_mock_config().await, DynConnector::new(conn.clone())); … @dfrasca80
  24. Unit Test Anatomy #[cfg(test)] mod tests { use ...imports... async

    fn get_mock_config() -> Config {} fn get_request_builder() -> http::request::Builder {} #[tokio::test] async fn test_put_item() { // Mock DynamoDB client // Mock API Gateway request let mut path_parameters = HashMap::new(); path_parameters.insert("id".to_string(), vec!["1".to_string()]); let request = http::Request::builder() .method("PUT") .uri("/1") .body(Body::Text("test1".to_string())) .unwrap() .with_path_parameters(path_parameters); … @dfrasca80
  25. Unit Test Anatomy #[cfg(test)] mod tests { use ...imports... async

    fn get_mock_config() -> Config {} fn get_request_builder() -> http::request::Builder {} #[tokio::test] async fn test_put_item() { // Mock DynamoDB client // Mock API Gateway request // ACT let response = put_item(&client, "test_table", request) .await .unwrap() .into_response(); // ASSERT assert_eq!(response.status(), 200); assert_eq!(response.body(), &Body::Text("item saved".to_string())); } } @dfrasca80
  26. Unit Test - MockAll 1 Create a mock struct. Its

    name will be the same with "Mock" prepended. 2 Instantiate the mock struct with its new or default method. 3 Set expectations on the mock struct. @dfrasca80
  27. Rust vs Node cargo npm cargo init npm init cargo

    build npm install Cargo.toml package. json Cargo.lock package-lock. json Cargo build webpack Cargo clippy lint Cargo fmt prettier Cargo test test library like jest https://dfrasca.hashnode.dev/series/rust-for-js-devs @dfrasca80
  28. SQS batch error handling with AWS Lambda pub async fn

    execute(app_client: &dyn WriterAppInitialisation, event: LambdaEvent<SqsEvent>) -> Result<Value, Error> { let failed_message: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new())); let mut tasks = Vec::with_capacity(event.payload.records.len()); for record in event.payload.records.into_iter() { let shared_client = app_client.clone_to_arc(); let message_id = record.message_id.unwrap(); let failed_message = failed_message.clone(); tasks.push(tokio::spawn(async move { if let Some(body) = &record.body { let request = serde_json::from_str::<SetDataForSlug>(&body); if let Ok(request) = request { shared_client.add_slug_query(&request).await .map_or_else(|e| { failed_message.lock().unwrap().push(message_id); }, |_| ()); } else { failed_message.lock().unwrap().push(message_id); } } })); } join_all(tasks).await; let response = BatchItemFailures { batch_item_failures: failed_message.lock().unwrap().clone() …. }; Ok(serde_json::to_value(response).unwrap()) } @dfrasca80
  29. SQS batch error handling with AWS Lambda pub async fn

    execute(app_client: &dyn WriterAppInitialisation, event: LambdaEvent<SqsEvent>) -> Result<Value, Error> {. let failed_message: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new())); let mut tasks = Vec::with_capacity(event.payload.records.len()); for record in event.payload.records.into_iter() { let shared_client = app_client.clone_to_arc(); let message_id = record.message_id.unwrap(); let failed_message = failed_message.clone(); tasks.push(tokio::spawn(async move { if let Some(body) = &record.body { let request = serde_json::from_str::<SetDataForSlug>(&body); if let Ok(request) = request { shared_client.add_slug_query(&request).await .map_or_else(|e| { failed_message.lock().unwrap().push(message_id); }, |_| ()); } else { failed_message.lock().unwrap().push(message_id); } } })); } join_all(tasks).await; let response = BatchItemFailures { batch_item_failures: failed_message.lock().unwrap().clone() …. }; Ok(serde_json::to_value(response).unwrap()) } @dfrasca80
  30. AWS Serverless Demo Test case: • ~2.5B of executions (warm)

    • ~2.5M of executions (cold) • ARM 64 AWS Cloud AWS Region request request Amazon API Gateway AWS Lambda Amazon DynamoDB @dfrasca80
  31. AWS Serverless Demo 70.65ms ms 50 Test case: • ~2.5B

    of executions • ARM 64 • Warm State Node Rust 40.73ms 40 128 MB: $ 795.83 $ 516.67 ~35% cheaper 256 MB: $ 841.67 $ 525.00 ~37% cheaper 30 512 MB: $ 650.00 $ 550.00 ~15% cheaper 20 1024 MB: $ 833.34 $ 633.33 ~24% cheaper 8.40ms 10 4ms 128 9.40ms 2.87ms 3.69ms 3.44ms 256 512 1024 MB @dfrasca80
  32. AWS Serverless Demo ms Test case: • ~2.5M of executions

    • ARM 64 • Cold Start (0.1%) Node Rust 900 800 700 $ 0.77 ~74% cheaper 128 MB: $ 3.07 256 MB: $ 2.93 $ 0.76 ~74% cheaper 500 512 MB: $ 2.77 $ 1.05 ~62% cheaper 400 1024 MB: $ 3.23 $ 1.63 ~50% cheaper 600 615.18ms 291.09ms 300 200 135.55ms 100 64.55ms 30.49ms 32.55ms 128 256 512 81.50ms 33.74ms 1024 MB @dfrasca80
  33. Rust vs Node ms ms 50 50 40 40 30

    30.49ms 81.50ms Node Rust 1024 256 MB MB Cold : 81.50ms 30.49ms ~62% faster Warm State: 9.40ms 30 20 20 10 10 Test case: • ~2.5B of executions (warm) • ~2.5M of executions (cold) • ARM 64 2.87ms ~69% faster Total Cost: $ 836.57 $ 525.76 ~37% cheaper 9.40ms 2.87ms 256 MB 1024 MB @dfrasca80
  34. Concurrent Request Time 1 t1 t2 Init Invocation 2 3

    Init Init 4 t3 t5 t4 t6 Time Concurrency 6 Invocation t1 3 Invocation 7 Invocation t2 5 Invocation 8 Invocation t3 4 t4 6 t5 5 t6 2 Init 5 Invocation Init Invocation 10 Invocation 9 Init Invocation Concurrent requests https://aws.amazon.com/blogs/compute/understanding-aws-lambda-scaling-and-throughput/ @dfrasca80
  35. Transaction per Second Time = 1 second Concurrent requests 1

    Init Invocation 2 Init Invocation 3 Init Invocation 4 Init Invocation 5 Init Invocation 6 Init Invocation 7 Init Invocation 8 Init Invocation 9 Init Invocation 10 Init Invocation If the function duration is 1 second to run 10 invocations happen concurrently Lambda creates 10 runtime environments. Transaction per second = 10 https://aws.amazon.com/blogs/compute/understanding-aws-lambda-scaling-and-throughput/ @dfrasca80
  36. Transaction per Second Time = 1 second 1 Init Invocation

    11 Invocation 2 Init Invocation 12 Invocation Invocation 3 Init Invocation 13 4 Init Invocation 14 Invocation 5 Init Invocation 15 Invocation 6 Init Invocation 16 Invocation 7 Init Invocation 17 Invocation 8 Init Invocation 18 Invocation 9 Init Invocation 19 Invocation 10 Init Invocation 20 Invocation If the function duration is 500 ms to run 10 invocations happen concurrently Lambda creates 10 runtime environments. However, transactions per second are now 20. Transaction per second = 20 https://aws.amazon.com/blogs/compute/understanding-aws-lambda-scaling-and-throughput/ @dfrasca80
  37. Rust vs Node ms ms 50 50 40 30 81.50ms

    40 30.49ms 20 10 10 Rust 1024 MB 256 MB Cold : 81.50ms 30.49ms ~62% faster Warm State: 9.40ms 2.87ms ~69% faster 30 20 Node Total Cost: $ 836.57 $ 525.76 ~37% cheaper TPS : 106 348 3 times more scalable 9.40ms 2.87ms 256 MB 1024 MB @dfrasca80