🔍

[DynamoDB] put, batchWrite, transactWriteの速度比較

2022/08/06に公開

Amazon DynamoDBの書き込み操作 put batchWrite transactWrite はそれぞれどれくらい速度に差があるのか検証してみました(この中で transactWrite は速度より一貫性を重視している操作ではありますが、それはそれで他の操作と比べてどれくらい差があるのか気になったため)。

オンデマンドモードのテーブルを用意し、それぞれの操作で1万件投入するスクリプトを書いてみます。

環境

  • M1 Mac
  • node 16.15.0
  • typescript 4.7.4
  • @aws-sdk/{client,lib}-dynamodb 3.121.0
  • lodash 4.17.21
  • @faker-js/faker 7.3.0
  • uuid 8.3.2
  • esbuild 0.14.48
  • esbuild-register 3.3.3

テーブル作成

  • テーブル名: testItems
  • オンデマンドモード
  • 主キー:
    • ハッシュキー: id: S
    • ソートキー: なし

としてテーブル作成します。

検証補助スクリプト

検証作業の補助用途として次のようなスクリプトを用意しています。

全件数確認スクリプト

テーブルの全件数を確認するスクリプトです。各操作後にきっちり1万件入ったかどうか確認するのに使います。

scan-test-items.ts
import { DynamoDB } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"

const main = async () => {
  // DynamoDB Document Client初期化
  const ddbDoc = DynamoDBDocument.from(
    new DynamoDB({ region: "ap-northeast-1" })
  )

  let exclusiveStartKey: any | null | undefined = null
  let count = 0
  while (exclusiveStartKey !== undefined) {
    const result = await ddbDoc.scan({
      TableName: "testItems",
      ExclusiveStartKey: exclusiveStartKey ?? undefined,
    })
    exclusiveStartKey = result.LastEvaluatedKey
    count += result.Items?.length ?? 0
  }
  console.log({ count })
}

main()

全件削除スクリプト

テーブルの全アイテムをスキャンしながら削除するスクリプトです。条件を揃えるため、各操作前にテーブルを0件にします。

テーブル自体を削除して再作成する手もありますが、今回はこちらを選択しました。

delete-all-items.ts
import { DynamoDB } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
import { chunk } from "lodash"

const main = async () => {
  const ddbDoc = DynamoDBDocument.from(
    new DynamoDB({ region: "ap-northeast-1" })
  )

  let items =
    (
      await ddbDoc.scan({
        TableName: "testItems",
      })
    ).Items ?? []

  while (items.length > 0) {
    const chunkedItems = chunk(items, 25)
    for (const items of chunkedItems) {
      await ddbDoc.batchWrite({
        RequestItems: {
          testItems: items.map((item) => ({
            DeleteRequest: {
              Key: {
                id: item.id,
              },
            },
          })),
        },
      })
    }

    items =
      (
        await ddbDoc.scan({
          TableName: "testItems",
        })
      ).Items ?? []
  }
}

main()

比較

計測方法

/usr/bin/time -l を使い、下記のように実行して計測します。

/usr/bin/time -l node -r esbuild-register "path/to/file.ts"

put

まずは put です。直列に1万件投入します。

put-items.ts
import { DynamoDB } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
import * as uuid from "uuid"
import { faker } from "@faker-js/faker"

const main = async () => {
  const items = Array.from({ length: 10000 }).map(() => ({
    id: uuid.v4(),
    name: faker.name.findName(),
    age: Number(faker.random.numeric(2)),
    content: faker.lorem.sentences(),
    createdAt: faker.date.recent().toISOString(),
    updatedAt: faker.date.recent().toISOString(),
  }))

  const ddbDoc = DynamoDBDocument.from(
    new DynamoDB({ region: "ap-northeast-1" })
  )

  for (const item of items) {
    await ddbDoc.put({
      TableName: "testItems",
      Item: item,
    })
  }
}

main()
結果
      293.38 real        20.25 user         1.99 sys
           151207936  maximum resident set size

293.38秒かかるようです。

batchWrite

続いて batchWrite で25件ずつ投入してみます。

batch-write-items.ts
import { DynamoDB } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
import * as uuid from "uuid"
import { faker } from "@faker-js/faker"
import { chunk } from "lodash"

const main = async () => {
  const allItems = Array.from({ length: 10000 }).map(() => ({
    id: uuid.v4(),
    name: faker.name.findName(),
    age: Number(faker.random.numeric(2)),
    content: faker.lorem.sentences(),
    createdAt: faker.date.recent().toISOString(),
    updatedAt: faker.date.recent().toISOString(),
  }))

  const ddbDoc = DynamoDBDocument.from(
    new DynamoDB({ region: "ap-northeast-1" })
  )

  // 10000件のデータを作成
  const chunkedAllItems = chunk(allItems, 25)
  for (const items of chunkedAllItems) {
    const result = await ddbDoc.batchWrite({
      RequestItems: {
        testItems: items.map((item) => ({
          PutRequest: {
            Item: item,
          },
        })),
      },
    })
  }
}

main()
       14.45 real         2.91 user         0.31 sys
           155615232  maximum resident set size

14.45秒でした。これだけ見ると相当速くなる印象です。

put + Promise.all

put でも並列にリクエストを投げれば batchWrite に近い速度が出るのでは?ということで Promise.all で25件ずつ投入します。

put-items-parallel.ts
import { DynamoDB } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
import * as uuid from "uuid"
import { faker } from "@faker-js/faker"
import { chunk } from "lodash"

const main = async () => {
  const allItems = Array.from({ length: 10000 }).map(() => ({
    id: uuid.v4(),
    name: faker.name.findName(),
    age: Number(faker.random.numeric(2)),
    content: faker.lorem.sentences(),
    createdAt: faker.date.recent().toISOString(),
    updatedAt: faker.date.recent().toISOString(),
  }))

  const ddbDoc = DynamoDBDocument.from(
    new DynamoDB({ region: "ap-northeast-1" })
  )

  // 10000件のデータを作成
  const chunkedAllItems = chunk(allItems, 25)
  for (const items of chunkedAllItems) {
    await Promise.all(
      items.map((item) =>
        ddbDoc.put({
          TableName: "testItems",
          Item: item,
        })
      )
    )
  }
}

main()
結果
       17.52 real         6.59 user         0.65 sys
           205717504  maximum resident set size

17.52秒でした。 batchWrite にかなり迫ってますが、メモリ使用量が batchWrite よりやや増えてる点は気になります。

transactWrite

transactWrite は速度より一貫性を重視している操作ですが、比較としてやってみます。

transact-write-items.ts
import { DynamoDB } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
import * as uuid from "uuid"
import { faker } from "@faker-js/faker"
import { chunk } from "lodash"

const main = async () => {
  const allItems = Array.from({ length: 10000 }).map(() => ({
    id: uuid.v4(),
    name: faker.name.findName(),
    age: Number(faker.random.numeric(2)),
    content: faker.lorem.sentences(),
    createdAt: faker.date.recent().toISOString(),
    updatedAt: faker.date.recent().toISOString(),
  }))

  const ddbDoc = DynamoDBDocument.from(
    new DynamoDB({ region: "ap-northeast-1" })
  )

  // 10000件のデータを作成
  const chunkedAllItems = chunk(allItems, 25)
  for (const items of chunkedAllItems) {
    await ddbDoc.transactWrite({
      TransactItems: items.map((item) => ({
        Put: {
          TableName: "testItems",
          Item: item,
        },
      })),
    })
  }
}

main()
       24.78 real         3.12 user         0.37 sys
           152387584  maximum resident set size

24.78秒でした。batchWrite よりは遅いですがそこまでというか、実用上問題は出てこなさそうという感想です。

まとめ

それぞれ1万件投入にかかった時間をまとめます。

ケース 時間
put 293.38秒
batchWrite 14.45秒
put+Promise.all 17.52秒
transactWrite 24.78秒
  • 大量データの保存に put 直列実行を選択するとすごく遅い
  • batchWrite が最速
  • put + Promise.all も速い
  • transactWritebatchWrite よりは遅いが、十分な速度

といった所感でした。

参考

Discussion