Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

Benchmark ฐานข้อมูล Serverless: เปรียบเทียบ Latency ของ DynamoDB, FaunaDB และ Upstash (Redis)

ในบทความนี้ จะเปรียบเทียบ Latency ของ 3 Serverless Databases: DynamoDB, FaunaDB, Upstash (Redis) สำหรับ Common Web Use Case

สร้าง Sample News Website และบันทึก Database-related Latency ทุก Request ไปยังเว็บไซต์ ดู เว็บไซต์ และ Source Code

Insert NY Times Articles 7,001 บทความลงแต่ละฐานข้อมูล บทความรวบรวมจาก New York Times Archive API (ทุกบทความ มกราคม 2021) ให้คะแนนสุ่มแต่ละบทความ ทุก Page Request จะ Query Top 10 Articles ภายใต้ Section World จากแต่ละฐานข้อมูล

ใช้ Serverless Functions (AWS Lambda) โหลด Articles จากแต่ละฐานข้อมูล Response Time ของการดึง 10 Articles ถูกบันทึกเป็น Latency ภายใน Lambda Function หมายเหตุ: Latency ที่บันทึกเป็นแค่ระหว่าง Lambda Function และ Database ไม่ใช่ Latency ระหว่าง Browser และ Server

หลัง Read Request ทุกครั้ง จะ Update Scores แบบสุ่มเพื่อจำลอง Dynamic Data แต่ไม่นำส่วนนี้มาคำนวณ Latency

ก่อนจะดู Application แล้วค่อยดู Results:

AWS Lambda Setup

Region: US-West-1

Memory: 1024Mb

Runtime: nodejs14.x

DynamoDB Setup

สร้าง DynamoDB Table ใน US-West-1 Read/Write Capacity 50 (Default 5)

Index เป็น GSI Partition Key section (String) Sort Key view_count (Number)

Benchmark ฐานข้อมูล Serverless: เปรียบเทียบ Latency ของ DynamoDB, FaunaDB และ Upstash (Redis)

FaunaDB Setup

FaunaDB เป็น Globally Replicated Database ไม่มีทางเลือก Region (ที่รู้) ใช้ FQL สมมติ GraphQL API อาจมี Overhead

สร้าง Index ด้วย Terms section และ Values ref ด้านล่าง ทำ Non-serialized หวังปรับปรุงประสิทธิภาพ

CreateIndex({
  name: "section_by_view_count",
  unique: false,
  serialized: false,
  source: Collection("news"),
  terms: [{ field: ["data", "section"] }],
  values: [
    { field: ["data", "view_count"], reverse: true },
    { field: ["ref"] }
  ]
})

Redis Setup

สร้าง Standard Type Database ใน US-West-1 Region ใน Upstash ใช้ Sorted Set ต่อ News Category ทุก World Articles จะอยู่ใน Sorted Set Key World

Initialize Databases

ดาวน์โหลด 7,001 News Articles เป็น JSON จาก NYTimes API Site แล้วสร้าง NodeJS Script ต่อ Database อ่าน JSON และ Insert News Record เข้า Database ดูไฟล์: initDynamo.js, initFauna.js, initRedis.js

Query DynamoDB

ใช้ AWS SDK เชื่อมต่อ DynamoDB เพื่อลด Latency เก็บ DynamoDB Connection Alive ใช้ Library perf_hooks วัด Response Time บันทึกเวลาก่อน Query DynamoDB สำหรับ Top 10 Articles คำนวณ Latency ทันทีที่ได้ Response จาก DynamoDB จากนั้น Random Score Articles และ Insert Latency Number ไป Redis Sorted Set แต่ส่วนเหล่านี้อยู่นอก Latency Calculation ดูโค้ด:

var AWS = require("aws-sdk");
AWS.config.update({ region: "us-west-1" });
const https = require("https");
const agent = new https.Agent({ keepAlive: true, maxSockets: Infinity });
AWS.config.update({ httpOptions: { agent } });
const Redis = require("ioredis");
const { performance } = require("perf_hooks");
const tableName = "news";
var params = {
  TableName: tableName,
  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 = (event, context, callback) => {
  let start = performance.now();
  docClient.query(params, (err, result) => {
    if (err) {
      console.error("Unable to scan the table. Error JSON:", JSON.stringify(err, null, 2));
    } else {
      let latency = performance.now() - start;
      let response = {
        statusCode: 200,
        headers: {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Credentials": true,
        },
        body: JSON.stringify({ latency: latency, data: result }),
      };
      result.Items.forEach((item) => {
        let view_count = Math.floor(Math.random() * 1000);
        var params2 = {
          TableName: tableName,
          Key: { id: item.id },
          UpdateExpression: "set view_count = :r",
          ExpressionAttributeValues: { ":r": view_count },
        };
        docClient.update(params2, function (err, data) {
          if (err) console.error("Unable to update item. Error JSON:", JSON.stringify(err, null, 2));
        });
      });
      const client = new Redis(process.env.LATENCY_REDIS_URL);
      client.lpush("histogram-dynamo", latency, (resp) => {
        client.quit();
        callback(null, response);
      });
    }
  });
};

Query FaunaDB

ใช้ Library faunadb เชื่อมต่อและ Query FaunaDB ส่วนที่เหลือคล้าย DynamoDB Code เก็บ Connection Alive ใช้ perf_hooks วัดเวลา บันทึกเวลาก่อน Query FaunaDB คำนวณ Latency ทันทีได้ Response จากนั้น Random Score และส่ง Latency ไป Redis Sorted Set ส่วนนี้นอก Calculation ดูโค้ด:

const faunadb = require("faunadb");
const Redis = require("ioredis");
const { performance } = require("perf_hooks");
const q = faunadb.query;
const client = new faunadb.Client({ secret: process.env.FAUNA_SECRET, keepAlive: true });
const section = process.env.SECTION;
module.exports.load = async (event) => {
  let start = performance.now();
  let ret = await client.query(
    q.Map(
      q.Paginate(q.Match(q.Index("section_by_view_count"), section), { size: 10 }),
      q.Lambda(["view_count", "X"], q.Get(q.Var("X")))
    )
  ).catch((err) => console.error("Error: %s", err));
  console.log(ret);
  let latency = performance.now() - start;
  const rclient = new Redis(process.env.LATENCY_REDIS_URL);
  await rclient.lpush("histogram-fauna", latency);
  await rclient.quit();
  let result = [];
  for (let i = 0; i < ret.data.length; i++) {
    result.push(ret.data[i].data);
  }
  ret.data.forEach((item) => {
    let view_count = Math.floor(Math.random() * 1000);
    client.query(
      q.Update(q.Ref(q.Collection("news"), item["ref"].id), { data: { view_count } })
    ).catch((err) => console.error("Error: %s", err));
  });
  return {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({ latency: latency, data: { Items: result } }),
  };
};

Query Redis

ใช้ ioredis เชื่อมต่อและอ่านจาก Redis ใน Upstash ใช้คำสั่ง ZREVRANGE โหลดข้อมูลจาก Sorted Set เก็บ Connection ไว้ reuse โดยสร้าง Redis Client นอก Function คล้าย DynamoDB และ FaunaDB กำลัง Update Scores และส่ง Latency ไป Redis DB อื่นสำหรับ Histogram ดูโค้ด:

const Redis = require("ioredis");
const { performance } = require("perf_hooks");
const client = new Redis(process.env.REDIS_URL);
module.exports.load = async (event) => {
  let section = process.env.SECTION;
  let start = performance.now();
  let data = await client.zrevrange(section, 0, 9);
  let items = [];
  for (let i = 0; i < data.length; i++) {
    items.push(JSON.parse(data[i]));
  }
  let latency = performance.now() - start;
  for (let i = 0; i < data.length; i++) {
    let view_count = Math.floor(Math.random() * 1000);
    await client.zadd(section, view_count, data[i]);
  }
  const client2 = new Redis(process.env.LATENCY_REDIS_URL);
  await client2.lpush("histogram-redis", latency);
  await client2.quit();
  return {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({ latency: latency, data: { Items: items } }),
  };
};

Histogram Calculation

ใช้ Library hdr-histogram-js คำนวณ Histogram นี่คือ JS Implementation ของ Gil Tene's hdr-histogram Library ดูโค้ด Lambda Function ที่รับ Latency Numbers และคำนวณ Histogram:

const Redis = require("ioredis");
const hdr = require("hdr-histogram-js");
module.exports.load = async (event) => {
  const client = new Redis(process.env.LATENCY_REDIS_URL);
  let dataRedis = await client.lrange("histogram-redis", 0, 10000);
  let dataDynamo = await client.lrange("histogram-dynamo", 0, 10000);
  let dataFauna = await client.lrange("histogram-fauna", 0, 10000);
  const hredis = hdr.build();
  const hdynamo = hdr.build();
  const hfauna = hdr.build();
  dataRedis.forEach((item) => { hredis.recordValue(item); });
  dataDynamo.forEach((item) => { hdynamo.recordValue(item); });
  dataFauna.forEach((item) => { hfauna.recordValue(item); });
  await client.quit();
  return {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify(
      {
        redis_min: hredis.minNonZeroValue,
        dynamo_min: hdynamo.minNonZeroValue,
        fauna_min: hfauna.minNonZeroValue,
        redis_mean: hredis.mean,
        dynamo_mean: hdynamo.mean,
        fauna_mean: hfauna.mean,
        redis_histogram: hredis,
        dynamo_histogram: hdynamo,
        fauna_histogram: hfauna,
      },
      null,
      2
    ),
  };
};

The Results (ผลลัพธ์)

ดู เว็บไซต์สำหรับผลลัพธ์ล่าสุด เข้าถึง Histogram Data ล่าสุด ได้ด้วย ตราบใดที่เว็บยัง Up เราจะเก็บข้อมูลและอัปเดต Histogram ต่อไป ผลลัพธ์ ณ วันที่ 12 เมษายน 2021 แสดงว่า Upstash มี Latency ต่ำสุด (~50ms ที่ 99th Percentile) FaunaDB สูงสุด (~900ms ที่ 99th Percentile) DynamoDB (~200ms ที่ 99th Percentile)

Benchmark ฐานข้อมูล Serverless: เปรียบเทียบ Latency ของ DynamoDB, FaunaDB และ Upstash (Redis)

Cold Start Effect (ผลของ Cold Start)

แม้เราวัด Latency แค่ส่วน Query Cold Start ยังมีผล เรา Optimize Code ด้วย Reusing Client Connections ได้ประโยชน์ตราบใดที่ Lambda Container Hot และ Running เมื่อ AWS Kill Container (Cold Start) Code จะ Re-create Client นี่คือ Overhead ใน เว็บไซต์แอปพลิเคชัน หาก Refresh หน้า จะเห็น Latency ลดลงเหลือ ~1ms สำหรับ Upstash และ ~7ms สำหรับ DynamoDB

ทำไม FaunaDB ช้า (ใน Benchmark นี้)?

ใน FaunaDB Status Page จะเห็น Latency Numbers เป็นร้อยๆ ผมสมมติ Config ของผมไม่มีข้อบกพร่องใหญ่ สาเหตุ 2 ประการที่เป็นไปได้:

Strong Consistency: โดย Default ทั้ง DynamoDB และ Upstash ให้ Eventual Consistency สำหรับ Reads FaunaDB ให้ Calvin-based Strong Consistency และ Isolation Strong Consistency มาพร้อม Performance Overhead

Global Replication: สำหรับ Upstash และ DynamoDB เรา Config Database และ Lambda ให้อยู่ใน AWS Region เดียวกันได้ ใน FaunaDB ข้อมูลถูกจำลองทั่วโลก ไม่มี Option เลือก Region หาก Database Clients อยู่ทั่วโลก นี่อาจเป็นข้อได้เปรียบ แต่ถ้า Deploy Backend ไป Region เฉพาะ จะก่อให้เกิด Extra Latency

Redis ให้ Sub-millisecond Latency ทำไมที่นี่ไม่ใช่?

การสร้าง Redis Connection ใหม่ใน AWS Lambda Function ก่อให้เกิด Overhead ที่โดดเด่น เนื่องจากแอปไม่ได้ Traffic Stable AWS Lambda Re-create Connection (Cold Start) ส่วนใหญ่ ดังนั้น Latency Numbers ส่วนใหญ่ใน Histogram รวม Connection Creation Time เรา Run Job Fetch เว็บไซต์ทุก 15 วินาที พบว่า Latency สำหรับ Upstash ลดเหลือ ~1ms หาก Refresh หน้า จะเห็นผลลัพธ์คล้ายกัน ดู บล็อกโพสต์ของเรา สำหรับวิธี Optimize Serverless Apps สำหรับ Low Latency

Coming Soon

Upstash จะเปิดตัว Premium Product เร็วๆ นี้ ที่ Data ถูกจำลองไป Multiple Availability Zones จะเพิ่มเข้าไปดู Effect ของ Zone Replication

แจ้ง Feedback ที่ Twitter หรือ Discord

Update

มีการถกเถียงกระทบใน HackerNews เกี่ยวกับ Benchmark และ Performance ของ Fauna นำไปปรับปรุงและ Restart FaunaDB Application นั่นคือเหตุผลที่จำนวน FaunaDB Records ใน Histogram น้อยกว่าอื่นๆ