DynamoDBのスループットエラーを意図的に発生させてみた

2020.05.22

こんにちは、CX事業本部の若槻です。

DynamoDBのテーブルにはキャパシティユニットというパラメーターがあり、テーブルに対するデータのWrite/Readスループットがキャパシティユニットを超えると、クライアントに対してProvisionedThroughputExceededExceptionエラーを返却します。

今回は、監視の設定の動作確認をする目的で、DynamoDBでスループットエラーProvisionedThroughputExceededExceptionを意図的に発生させてみました。

スループットエラーを起こすためのチューニング

スループットエラーを意図的に発生させるために、今回はBoto3でテーブルにデータの書き込み処理を行うPythonスクリプトを利用しようと思いますが、その際に効率的にエラーを発生させるために、設定項目のチューニングをいくつか行っていきます。

テーブルはプロビジョニングモードとしWCUを最小とする

DynamoDBのテーブルには2つのキャパシティモードがあります。

プロビジョニングモードの場合はテーブルのキャパシティユニットの上限を指定することができ、最小の設定は1となります。

よって、キャパシティモードはプロビジョニングモード、書き込みキャパシティユニット(WCU)は1としたテーブルを作成することにします。

バーストキャパシティを考慮する

DynamoDBには"バーストキャパシティ"という仕様があり、テーブルに設定されたキャパシティユニットをスループットが超過した際に、過去300秒で利用されなかったキャパシティユニットを消費して充てることができます。

よって、テーブルに設定された1WCU(最大でサイズが 1 KB までの項目について、1 秒あたり 1 回の書き込み)を超過する書き込み処理を行うだけではバーストキャパシティによりスループットエラーが発生しない可能性があるため、Pythonスクリプトでは1KBより十分に大きいサイズのデータを連続で書き込む処理を実装することにします。

ちなみに、同ドキュメントにはバーストキャパシティについて下記のようにもあり、

During an occasional burst of read or write activity, these extra capacity units can be consumed quickly—even faster than the per-second provisioned throughput capacity that you've defined for your table.

DynamoDB can also consume burst capacity for background maintenance and other tasks without prior notice.

「過去300秒で利用されなかったキャパシティユニットを利用可能」という仕様は額面通りには受け取らず、バーストキャパシティは必ずしも期待動作とはならないと考えた方が良さそうです。

Boto3のリトライ回数をゼロにする

下記のドキュメントによると、AWS SDKには実行時のエラーを受け取った際の再試行ロジックが実装されているとのことです。

Boto3の場合は、ソースコードによれば、既定のリトライ回数は5回とのことです。よって、最短でスループットエラーを発生させるためにこのリトライ回数を0回に変更する設定をPythonスクリプト内で行います。

リトライ回数の設定方法については下記の記事が参考となりました。

スループットエラーを起こしてみる

まず、WCU1のDynamoDBテーブルsample-tableを作成します。

$ aws dynamodb create-table \
--table-name sample-table \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--provisioned-throughput  ReadCapacityUnits=1,WriteCapacityUnits=1

データ書き込みを行うPythonスクリプトdata_put.pyは以下のようになります。

import json
from botocore.client import Config
from boto3.session import Session

profile = 'profile'
session = Session(
    profile_name=profile,
    region_name='ap-northeast-1'
)
config = Config(
    retries=dict(
        max_attempts=0
    )
)
dynamodb_resource=session.resource(
    'dynamodb',
    config=config
)

data = open('./large_data').read()
i = 0

while i < 10:
    table = dynamodb_resource.Table('sample-table')
    Item = {
        'id': 'aaa',
        'foo': data
    }
    table.put_item(Item=Item)
    print(i)
    i += 1

テーブルに書き込むためのサイズの大きいデータlarge_data(12KB)を作成しておきます。

$ ls -lh | grep large_data
-rw-rw-r-- 1 ec2-user ec2-user  12K May 22 14:16 large_data

Pythonスクリプトを実行します。バーストキャパシティにより1,2,3回目の書き込みではエラーとならず、4回目の実行でProvisionedThroughputExceededExceptionが発生させられました。

$ python data_put.py
0
1
2
Traceback (most recent call last):
  File "data_put.py", line 29, in <module>
    table.put_item(Item=Item)
  File "/usr/local/lib/python3.6/site-packages/boto3/resources/factory.py", line 520, in do_action
    response = action(self, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/boto3/resources/action.py", line 83, in __call__
    response = getattr(parent.meta.client, operation_name)(**params)
  File "/usr/local/lib/python3.6/site-packages/botocore/client.py", line 272, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python3.6/site-packages/botocore/client.py", line 576, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.ProvisionedThroughputExceededException: An error occurred (ProvisionedThroughputExceededException) when calling the PutItem operation (reached max retries: 0): The level of configured provisioned throughput for the table was exceeded. Consider increasing your provisioning level with the UpdateTable API.

参考までに、sample-tableテーブルのCloudWatchメトリクスを見ると、[書き込みキャパシティー]のモニタリングではPythonスクリプトを実行した時間帯に「消費したキャパシティユニット」が急上昇していることが確認できます。 image.png

[スロットル書き込みリクエスト]のモニタリングでは同時間帯に「PUT」によるスロットルが1回発生していることが確認できます。 image.png

おわりに

最初はバーストキャパシティの仕様をよく知らずスループットエラーを発生させるのに苦労しましたが、今回の取り組みを経て結果的にDynamoDBの仕様理解につながったので良かったです!

参考