DynamoDBがどれだけ頑張れるかを実験してみる

# はじめに
初心者向けDynamoDB理解用。

DynamoDBってフルマネージドでスケーラブルでサーバレスには欠かせないぜ！と言われているけど、実際どれくらい頑張ってくれるの？というところを知りたかったので実験してみた。

なお、記事中に記載した料金は目安までに。2020年6月時点のものを書いているので、今後の価格改定の可能性がある。また、記載分以外にも課金要素があるので、正確な料金を知りたければ[公式で料金計算して](https://aws.amazon.com/jp/dynamodb/pricing/provisioned/)ほしい。無料範囲を加味せず記載しているので、実際はもう少し安くなるケースもある。

# テーブル準備
テーブルはあくまでも実験用なので凝らずに作ろう。以下のHCLで```$ terraform apply```だ！
データ抽出の実験をしたいので、グローバルセカンダリインデックスを作っておく。
※プライマリキーのソートキーを使おうとすると、プライマリキー必須になってしまい美味しくないため、GSLを使う。

なお、```read_capacity```と```write_capacity```はこの後変更するので、ひとまずテキトーで良い。

```HCL
resource "aws_dynamodb_table" "from_table" {
  name           = "[テーブル名]"
  billing_mode   = "PROVISIONED"
  read_capacity  = 1
  write_capacity = 1
  hash_key       = "id"
  
  attribute {
    name = "id"
    type = "S"
  }

  attribute {
    name = "name"
    type = "S"
  }

  global_secondary_index {
    name               = "NameIndex"
    hash_key           = "name"
    write_capacity     = 1
    read_capacity      = 1
    projection_type    = "ALL"
  }
}
```

# 書き込み性能
DynamoDBの性能はキャパシティユニットに依存する。
正確には、キャパシティーモードがProvisionedな設定になっているケースにおいては、だ。
要は、どれだけのリソースを用意しておくか＝キャパシティユニット数と考えれば良い。

これを変更しながらDBにPUTしてみる。
なお、PUTにはPythonのboto3モジュールでbatch_writerを使う。
batch_writerがバックエンドでどのような動作をしているかは分からないが、少なくとも呼び出し元アプリケーションの実装はシングルスレッド・シングルプロセスで実行する。

```Python
#!/usr/bin/python3

import pprint
import boto3
import time

name_table = ['Taro', 'Jiro', 'Saburo', 'Shiro']

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('[テーブル名]')

range_from = 1
range_to   = 1000000

start_tm = time.time()

try:
    with table.batch_writer() as batch:
        for cnt in range(range_from, range_to+1):
            batch.put_item(Item={'id': "{:08d}".format(cnt), 'name': name_table[(cnt-1)%4]})
            if cnt % 100 == 0:
                pprint.pprint(str(cnt) + " / " + str(range_to) + " finished. elapsed " + str(time.time()-start_tm))
except Exception as e:
    pprint.pprint(e.response)
```

結果は以下の通り。

|WCU|実行回数|実行時間|秒間あたりの処理件数|月あたりの利用料金|
|:--|:--|:--|:--|:--|
|1|1000|途中でエラーになった|1件|$0.56|
|10|1000|約100秒|10件|$5.57|
|100|100000|約1000秒|100件|$55.65|
|500|1200000|約2100秒|571件|$278.25|

ほぼ割り当てたキャパシティユニットに比例してカタログスペック程度かそれ以上の性能が出ている。
しかし、WCU1だと、そもそもガンガン書き込むことができない。
シングルスレッドでどこまで頑張れるか試してみたいところだが、利用料金が怖いので割愛。

参考までに、キャパシティーモードをオンデマンドに変えて実行してみる。

|実行回数|実行時間|秒間あたりの処理件数|料金|
|:--|:--|:--|:--|
|100000|51秒|1960件|$1.43|

速い！これは圧倒的。シングルスレッドでWCUを増やすとすると、おそらく1900くらいが限界ということだろうか。

ただし、トランザクション量が多い場合にオンデマンドにしていたら、金銭的に死んでしまう。

たとえば、WCU100で済むトランザクションが恒常的に流れるとすると、オンデマンドでは月あたりざっくり$382かかる。一方で、瞬間最大風速で数万件の処理をするのに対してProvisionedにするのはオーバーヘッドが大きすぎる。ちゃんとトランザクションの傾向を知っておくことが重要だ。

ちなみに、セカンダリインデックスを使うと、倍のキャパシティユニットが必要になるので、その点も注意が必要。

# 読み込み性能
読み込み性能も、書き込み同様に、Provisionedモードの場合はキャパシティユニットに依存する。
単価は読み込み性能の方が安い。

これも、キャパシティユニット単位で性能を比較してみる。
なお、セカンダリインデックスを使用した一括抽出を試してみる。

```Python
#!/usr/bin/python3

import pprint
import boto3
import time
from boto3.dynamodb.conditions import Key, Attr

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('[テーブル名]')

start_tm = time.time()

response = table.query(
    IndexName='NameIndex',
    KeyConditionExpression=Key('name').eq('Taro'))
ret_data = response['Items']

while 'LastEvaluatedKey' in response:
    pprint.pprint("Elapsed: " + str(time.time()-start_tm))

    response = table.query(
        IndexName='NameIndex',
        KeyConditionExpression=Key('name').eq('Taro'),
        ExclusiveStartKey=response['LastEvaluatedKey'])
    ret_data += response['Items']

pprint.pprint("Elapsed: " + str(time.time()-start_tm))

pprint.pprint(ret_data)
```


結果は以下の通り。

|RCU|実行回数|実行時間|秒間あたりの処理件数|月あたりの利用料金|
|:--|:--|:--|:--|:--|
|1|250000|69秒|3623件|$0.11|
|10|1000|約8秒|31250件|$1.11|
|100|100000|約8秒|31250件|$11.13|

どうやら、シングルスレッドでの抽出では、通信等のオーバーヘッドで8秒が限界のようだ。
試しに、キャパシティーモードをオンデマンドにしても同様の結果となる。

|抽出件数|実行時間|秒間あたりの処理件数|15円|
|:--|:--|:--|:--|
|250000|8秒|31250件|$0.28|

読み込み性能についてはDynamoDBはRDBなんて比べ物にならないくらい早い(KVSなのだから、当たり前なのだけど)。
今回、データベースの母数を100万件としたので、それ以上については分からないが、scanならまだしも、インデックスを効かせるqueryの特性を考えれば、抽出数が大きく変わらなければそれほど性能影響はないだろう。
※あとは、今回はテーブルサイズが小さいので、それ次第ではオーバーヘッドが変わると思われる。