Performance tuning in a serverless environment is the key to keep your back end responsive and your bill low.
This article will go through several performance tests to understand the behavior of an AWS Lambda written in Swift using the framework Swift-Sprinter and compares it to popular languages and different configurations and options.
Lambda is the AWS resource providing Function as a Service (FAAS) computing power. It acts as the building block of a broader category of services, well-known as serverless.
Like the other serverless blocks, it has the following features:
In addition to the common serverless features, Lambda:
The AWS Lambda architecture, based on Firecracker on top of an EC2 VM, has been built to be auto-scalable.
The first time the Lambda is invoked, a new worker instance is prepared by copying, initializing, and running the code.
This process is called cold start and it happens every time the Lambda environment adds a new worker instance to scale up. Once the worker starts, it receives the event, processes it and then executes the request for the next event remaining in a wait status.
Once the Lambda worker has been started, the next execution is pure processing of the upcoming event as there is no more need to set up the worker.
Eventually, after many minutes, the worker doesn’t receive any events, it is being disposed to scale down and free up unused computing resources.
Lambda computing performance depends on the language/runtime used, the code, and the provisioned amount of memory which also determines the power of the underlying CPU.
Memory — The amount of memory available to the function during execution. Choose an amount between 128 MB and 3,008 MB in 64 MB increments.
Lambda allocates CPU power linearly in proportion to the amount of memory configured. At 1,792 MB, a function has the equivalent of one full vCPU (one vCPU-second of credits per second).
To perform performance evaluation under the same conditions, all the Lambdas have been set up with 256 MB of memory. This configuration gives the Lambda 1/7 of vCPU.
To reduce the number of cold starts, it is possible to take advantage of a new feature called provisioned concurrency which keeps some Lambdas always warm.
Lambda performances are really important as the service is sold as “pay-per-use”. Keeping the execution time and the memory amount usage low helps to reduce the on-going costs of AWS Lambda.
Performance of languages on AWS Lambda is defined by:
The execution of a simple “Hello, World” gives a rough idea of how the language performs on a very basic task and it’s quite accurate in evaluating the AWS Lambda runtime for the language.
Heavy computational tasks may dramatically change the results of the “Hello, World” benchmark as, in this case, the test will advantage the language efficiency in dealing with computational complexity rather than the AWS Lambda runtime.
For simplicity, the article will consider only the “Hello, World” benchmark leaving the task of proofing the efficiency for other workloads to the reader.
To compare the performances to Swift, the Hello World example has been implemented in different languages.
In the table above are the configurations used for the test.
The first thing to notice is that AWS provides a runtime for Ruby, Node.js, Python, Java, Go, and .Net Core 2.1. This gives these languages an advantage as they don’t have the overhead of a custom runtime with additional code to load.
The Hello World example is pretty basic, the code size can be totally different as the code adds more dependencies.
Many versions of the Hello World code in Swift have been added to compare the performances of:
benchmark-swift-NIO-hello — SwiftSprinter with NIO AsyncHTTPClient
benchmark-swift-curl-hello — SwiftSprinter with libCURL
benchmark-al-ffett-hello — LambdaSwiftRuntime with NIO
The workload has been prepared as in the articles referenced at the end.
The tool Artillery has been used to test the Lambdas. An API gateway has been configured to publish the Lambdas as HTTPS endpoints.
After the invocation, all the metrics are stored in AWS CloudWatch.
The cold start performances have been gathered using the lumigo-CLI.
Cold Start performances
To show the warm start performances, we used a custom dashboard on CloudWatch:
Swift Built with Amazon Linux 2
C#/NodeJS/Swift built with Ubuntu
The best performance on average is given by C# .Net (~0.26 ms) and Go (~0.44 ms).
With performances of around 1 ms, we have Node.js (~1.06ms), Java (~1.16 ms), Rust (~1.17 ms), Swift with NIO and connection pooling built with Ubuntu (~1.19ms), Python (~1.34), Swift with NIO (~1.56 ms).
Swift was open-sourced in December 2015 by Apple and has grown from mobile development on iOS and Mac to Linux server platforms such as Ubuntu.
Anyone coming from iOS knows that the language is safe, fast, and expressive. Compared to scripting languages such as Node.js and Python, Swift enhances security and performance, avoiding malicious side-effects due to language undefined behavior and the lack of type checking.
Compared to relatively new languages such as Go or Rust, Swift adds the advantage of a large community of mobile developers and enthusiasts.
The investments of Apple on Swift NIO and the availability of the Swift Package Manager have fuelled the development of many server-side projects and libraries including HTTP Client, Postgres Client, Redis Client, AWS SDK Swift, MongoDB Swift Driver, gRPC Swift, … and more.
The Swift-Sprinter repository provides an AWS Lambda custom runtime for Swift and is based on Ubuntu to guarantee the build process and a well-defined pipeline. The repository contains examples of usage and continuous integration.
For anyone wanting to experiment, the repository Swift-Lambda-Runtime provides a version of Swift built on top of Amazon Linux 2.
The availability of Swift on serverless frameworks like AWS, OpenWhisk, and Azure, as well as traditional API/web frameworks such Vapor, Kitura, and Perfect on VMs, Docker, Kubernetes, and on-premise HW has opened the road to full-stack Swift development.
This article doesn’t cover the choice of a specific cloud provider as this would depend on different business reasons rather than technical considerations.
The advantage of using Swift on AWS Lambda rather than containers is effective when the application built with it doesn’t have a constant workload and requires to be scalable while keeping costs low. Serverless and containers are not silver bullets and the choice needs to be evaluated per use case.
The AWS Lambda runtime choice doesn’t depend only on performance considerations.
It’s always important to consider the total cost of ownership of a solution, taking into account not only the bare cost of the serverless but also reliability, maintainability, resilience, reusability, security, platform support, availability of third-party frameworks, developer community maturity, development tools, licenses, availability of development skills, and learning curve regarding the specific use case.