2 minutes read

POSTED Sep, 2021 dot IN Serverless

Benchmark Your Serverless Database with Thundra

Enes Akar

Written by Enes Akar


linkedin-share
 X

Every millisecond costs you in serverless functions. The visible cost is the one that cloud providers charge. The hidden one is when your customers are unhappy with the performance of your application or website. While performance monitoring is a must for modern applications, it may require a high level of effort to implement correct and useful monitoring. Monitoring tools save a life here. We will use Thundra’s APM to measure the latency of the two serverless databases in this article.

The Scenario

We will write two AWS Lambda functions that fetch records from two different Serverless databases: AWS DynamoDB and Upstash Redis. Both databases are loaded with 7001 sample articles and both functions fetch the top 10 articles with a query equivalent to:

select * from news where section = ‘World’ order by view_count desc;

The Setup

We started both databases in the US-West-1 region. To minimize the latency, we run the Lambda functions in the same region. We do not use VPC or any other special network setup. The runtime for the function is nodejs14.x and the memory size is 1024MB.

For DynamoDB, we use the read and write capacity of 50 (the default value was 5). We added an index which is GSI with partition key section (String) and sort key view_count (Number).

For Redis, we use a single zone Upstash database. We used a Sorted Set per each news category.

The Implementation

Here the functions which fetches records from DynamoDB:

var AWS = require("aws-sdk");

const https = require('https');
const agent = new https.Agent({
   keepAlive: true,
   maxSockets: Infinity
});

AWS.config.update({
   region: "us-west-1",
   httpOptions: {
       agent
   }
});

const thundra = require("@thundra/core")(
   {apiKey: process.env.THUNDRA_KEY});

var params = {
   TableName: "news",
   IndexName: "section-view_count-index",
   KeyConditionExpression: "#sect = :section",
   ExpressionAttributeNames: {
       "#sect": "section"
   },
   ExpressionAttributeValues: {
       ":section": process.env.SECTION
   },
   Limit: 10,
   ScanIndexForward: false,
};
const docClient = new AWS.DynamoDB.DocumentClient();

module.exports.load = thundra((event, context, callback) => {
   docClient.query(params, (err, result) => {
       if (err) {
           console.error("Unable to scan the table. Error JSON:", JSON.stringify(err, null, 2));
       } else {
           let response = {
               statusCode: 200,
               headers: {
                   'Access-Control-Allow-Origin': '*',
                   'Access-Control-Allow-Credentials': true,
               },
               body: JSON.stringify(
                   {
                       data: result
                   }
               )
           };
           callback(null, response)
       }
   });
});

As you will notice, above we configure the HTTP agent to keep the connection alive. This improves the performance of the application as it does not require setting up a connection at each invocation.

And here the code for the function which accesses to the Redis:

<const Redis = require("ioredis");
const client = new Redis(process.env.REDIS_URL);
const thundra = require("@thundra/core")(
   { apiKey: process.env.THUNDRA_KEY });

module.exports.load =  thundra(async(event) => {
   let section = process.env.SECTION;
   let data = await client.zrevrange(section, 0, 9)
   let items = []
   for(let i = 0; i <; data.length i++) {
       items.push(JSON.parse(data[i])); 
   }
   return {
       statusCode: 200,
       headers: {
         <  'Access-Control-Allow-Origin': '*',
           'Access-Control-Allow-Credentials': true,
       },
       body: JSON.stringify( {
           data: {
               Items: items
           },
       })
   };
});
;

The Results

We have run Pingdom to invoke both functions every 5 mins. So far there have been more than 1000 calls. DynamoDB duration is 37ms on average and 98ms on 99 percentile. Upstash Redis duration is 3ms on average and 40ms in 99 percentile.

One of the best things about Thundra is that it analyzes your Lambda function and gives you the performance metrics for each different portion of your code. So this helps us discard the cold start and see the latency caused by the database only.

In the below architecture diagram and count & duration charts, you can see the invocation durations when we use the DynamoDB database. As seen, the average invocation duration is 37ms

Architecture diagram of invocation durations - DynamoDB

Architecture diagram of invocation durations - DynamoDB

Count & duration charts of invocation durations - DynamoDB

Count & duration charts of invocation durations - DynamoDB

In the below architecture diagram and count & duration charts, you can see the invocation durations when we use the Upstash Redis database. As seen, the average invocation duration is just 3ms.

Architecture diagram of invocation durations - Upstash Redis

Architecture diagram of invocation durations - Upstash Redis

 

Count & duration charts of invocation durations - Upstash Redis

Count & duration charts of invocation durations - Upstash Redis

You can check the GitHub repository of this benchmarking test and help us if there is anything to improve. Visit Upstash and Thundra websites for more information.