Amazon Timestream のメジャーのクォータを確認してみた

Amazon Timstream のクォータを確認してみました。最初の図がこの記事の全てです。
2022.02.11

今回は、Amazon Timestream を使うときに抑えておきたいクォータ(制限値)について確認してみました。結論は下記の図のとおりです。
(誤り等ありましたらご指摘いただけると幸いです)

311-Timestream_Quota_for_blog

上記の図がこの記事で伝えたいことの全てになります。それぞれの詳細については、お時間あるときにでも下に書いている検証内容をご覧いただければ幸いです。
なお、他にもクォータは存在するので、全てを確認する場合は公式ドキュメントよりご確認ください。

Amazon Timestream のクォータ(制限値)について

どんなサービスにもクォータ(制限値)は存在しており、利用する上でクォータを確認しておくことは大切です。
下記のページにあるように、Amazon Timestream にもクォータは存在しますが、全てハードリミットになっており上限緩和を申請して制限値を増やすことができません。(2022年2月9日現在)

ドキュメントに上限緩和の有無は記載されていませんが、AWS Service Quotas の画面から確認すると、Amazon Timestream の全ての項目で緩和できないことが分かります。

300-quota-timestream

また、AWS サポートのサポートケースでも上限緩和用のサービスを選択するプルダウンに Amazon Timestream は存在しませんでした。

そのため、Amazon Timestream を使う場合は、これらの各クォータの意味を正確に把握した上で要件に合致しているかどうか確認する必要があります。

特にマルチメジャーレコードを使う場合は、センサーなどから送られてくるデータの項目数が多いと思わぬエラーに遭遇することになります。
シングルメジャーレコードの場合は「1項目のデータに対して1レコード」であり、書き込む際も「Big int、double、string、boolean」のいずれかの項目として書き込まれるのでカラムが 4つ以上に増えることもありません。
そのためシングルメジャーレコードの場合、あまりクォータを意識しなくても使えるケースが多いかと思います。

サンプルスクリプトでクォータを確認する

今回は、今後よく使うことになると思われるマルチメジャーレコードの制限値についてサンプルスクリプトを通じて確認してみました。コードは前回と同じものを使います。

import boto3
from botocore.config import Config
import time
import random

session = boto3.Session()
write_client = session.client('timestream-write', region_name='us-east-1',
                              config=Config(read_timeout=20,    # リクエストタイムアウト(秒)
                              max_pool_connections=5000,        # 最大接続数
                              retries={'max_attempts': 10}))    # 最大試行回数

# Amazon Timestream データベースとテーブル
DatabaseName='mytestdb'
TableName='mytesttbl'

# タイムスタンプの付与
def current_milli_time():
    return round(time.time() * 1000)

# ディメンションの作成
def gen_dimensions():
    municipalities = random.choice(['Shinjuku', 'Toshima', 'Nakano', 'Ota', 'Chiyoda'])
    gateway_id = random.choice(['gateway_1', 'gateway_2', 'gateway_3'])
    device_name = str(municipalities) + '_' + str(gateway_id)
    dimensions = [
        {'Name': 'Location', 'Value': 'Tokyo'},
        {'Name': 'Municipalities', 'Value': municipalities},
        {'Name': 'DeviceName', 'Value': device_name}
    ]
    return dimensions

# 何件のメジャー値(アイテム、RDBのカラムに相当するもの)を作成するか
# 1レコードあたりに入れるアイテムを複数作成する
def gen_item():
    records = []
    MultiMeasureValue = []
    start_item_num = 0 # 任意のアイテム番号からMeasureValueを作成
    end_item_num = 10 # start_item_numから 256を超えない範囲で指定

    for multi_measure_num in range(start_item_num,end_item_num): 
        #ランダムな数値データをメジャー値として作成
        MeasureValue = str(random.uniform(1, 90))
        dummy_multi_measure = 'item_' + str(multi_measure_num)
        myitem = {
            'Name': dummy_multi_measure,
            'Value': MeasureValue,
            'Type': 'DOUBLE'
        },
        records.append(myitem)
        t = records[multi_measure_num - start_item_num]
        MultiMeasureValue.append(t[0]) # 要素だけ抽出して、それを連結して変数に入れる
    return MultiMeasureValue

# レコードの生成
def gen_dummy_record():
    records_X = []
    # 何件のレコードを作成するか ; 100件:(0,100) 最大100
    for record_num in range(0,1):
        dummy_measure = {
            'Dimensions': gen_dimensions(),
            'MeasureName': 'dummy_metrics',
            'MeasureValueType': 'MULTI',
            'MeasureValues': gen_item(),
            'Time': str(current_milli_time()),
            'TimeUnit': 'MILLISECONDS'
        }
        records_X.append(dummy_measure)
        time.sleep(1/1000)
    return records_X

for write_num in range(0,1): # 生成したレコードを何回書き込むか
    print ("start write_records...: " + str(write_num))
    result = write_client.write_records(DatabaseName=DatabaseName,
                                        TableName=TableName,
                                        Records=gen_dummy_record(), CommonAttributes={})

Measures per multi-measure record : 256の検証

検証の目的

ここでは、「1レコードあたりの書込み可能なバリュー名(データ項目)の最大個数が 256」 であることを確認します。

307-Timestream_Quota_for_blog_256

検証手順

最初に 37 〜 38 行目を start_item_num = 0,end_item_num = 256 で実行してメジャーバリューが 256 個書き込めることを確認します。

$ python3 write_multi_measure.py

クエリの結果からも 256 個のバリュー値を書き込めていることが分かります。
item_0item_255を格納したので、最後のitem_255が格納されていれば OK とします。)

302-item255-query

(なお、SQL でカラム数をカウントしてみた所 information_schema が予約された名前でありクエリが通りませんでした。)

次に先程変更した 37 〜 38 行目を start_item_num = 0,end_item_num = 257 で実行してエラーになることを確認します。

$ python3 write_multi_measure.py

下記のようなエラーメッセージが出ます(見やすさのため適宜改行しています)

botocore.errorfactory.ValidationException: 
An error occurred (ValidationException) when calling the WriteRecords operation: 
You have reached the maximum number of Multi measureValue names for a record. 
See Quotas in the developer guide for additional information.

これにより、マルチメジャーレコード 1 行あたり格納できるバリュー名(データ項目)の最大数は 256 個であることが分かりました。
終わったらstart_item_num = 0,end_item_num = 1とかに戻しておきます。

Unique measures across multi-measure records per table : 1024 の検証

検証の目的

ここでは「マルチメジャーレコード全体における (measure_name を除いた)レコードあたりのメジャー(メジャーバリューではない)の最大数が1024」であることを確認します。

310-Timestream_Quota_for_blog_1024

検証手順

最初に 37 〜 38 行目を start_item_num = 0,end_item_num = 256で実行してメジャーバリュー 256 個を書き込んで、対象テーブルのメジャー数を 256 個にします。

先程検証したように、1 つのマルチメジャーレコードに格納できるメジャー最大数は 256 個なので、256 個ずつデータ項目をずらして書き込んでいきます。そのためコードを都度修正して下記のように書き込みを行います。

  • start_item_num = 256,end_item_num = 512で書き込み
    • テーブルのメジャー全体を前回との合計で512個にする
  • start_item_num = 512,end_item_num = 768で書き込み
    • テーブルのメジャー全体を前回との合計で768個にする
  • start_item_num = 768,end_item_num = 1024で書き込み
    • テーブルのメジャー全体を前回との合計で1024個にする

302-item1024-query

最後に start_item_num = 1024,end_item_num = 1025で実行してエラーになることを確認します。今度は RejectedRecordsException というエラーが発生しました。 (見やすさのため適宜改行しています)

botocore.errorfactory.RejectedRecordsException: 
An error occurred (RejectedRecordsException) when calling the WriteRecords operation: 
One or more records have been rejected. See RejectedRecords for details.

RejectedRecords の詳細は下記になります。

このドキュメントによると、RejectedRecords の原因の一つとして、「Timsstream のクォータを超えた 」事が原因と書かれておりクォータに関するページが案内されています。クォータのページを見ると、1024Unique measures across multi-measure records per table の項目のみなので、たしかに「 マルチメジャーレコード全体における レコードあたりのメジャーの最大数は 1024 」であることが分かります。

終わったらstart_item_num = 0,end_item_num = 1とかに戻しておきます。

Measure value size per multi-measure record : 2048 の検証

目的

ここでは「1 つのメジャーバリューに対して書き込めるデータサイズが 2048byteであることを確認します。

308-Timestream_Quota_for_blog_2048

検証手順

最初に、42 行目 を下記のように変更して、バリュー値に 2048 桁の数字( 2048 Byte のデータ)が入るようにします。

MeasureValue = str(random.uniform(1, 90))
↓
digit = 2048 # 桁数
MeasureValue = str(random.randrange(10**(digit-1),10**digit))

次に 47 行目を下記のように修正してデータ型を変更します。

'Type': 'DOUBLE'
↓
'Type': 'VARCHAR'

正しく 2048 Byte のデータが書き込めました。

312-write-2048byte-data

次に digit = 2049 に変えてエラーになることを確認します。実行すると下記のようなエラーが表示されました。 (見やすさのため改行を入れています)

botocore.errorfactory.ValidationException: An error occurred (ValidationException) when calling the WriteRecords operation: 
1 validation error detected: Value '790・・(中略)・・263' at 'records.1.member.measureValues.1.member.value' failed to satisfy constraint: 
Member must have length less than or equal to 2048

日本語データで確認してみる

Amazon Timestream のクォータのドキュメントには Measure value size per multi-measure record の単位が記載されていません。そのためこの数字が、「メジャー値の文字数、桁数」なのか「Byte」なのかよく分かりません。
そのため、このクォータ値についてはもう少し検証を続けてみたいと思います。

次に、日本語をデータとして入れてみたいと思います。
( IoT のユースケースだと日本語のデータが入ることは無いかもしれませんが、センサーによっては「注意状態です」とか「対応が必要です」といった日本語情報を送ってくるものもあるかもしれません。が、実際にそういうデータを MQTT などでやり取りした経験はまだ筆者にはありません…)

一時的に gen_item() の関数を下記のように変更します。8 行目 の str_num = 682 で 682 文字のランダムな「ひらがな」から成るデータを書き込みます。
AWS Cloudshell の文字コード は utf_8 なので 「ひらがな 682 文字で 2046 Byte( 1 文字 = 3Byte )」をバリュー値として書き込むことになります。

def gen_item():
    records = []
    MultiMeasureValue = []
    start_item_num = 0
    end_item_num = 1
    for multi_measure_num in range(start_item_num,end_item_num): 
        jp = ["あ","い","う","え","お","か","き","く","け","こ","さ","し","す","せ","そ","た","ち","つ","て","と","な","に","ぬ","ね","の","は","ひ","ふ","へ","ほ","ま","み","む","め","も","や","ゆ","よ","ら","り","る","れ","ろ","わ","を","ん"]
        str_num = 682
        word = ""
        for i in range(str_num):
            word += jp[random.randint(0,len(jp)-1)]
        MeasureValue = str(word)
        print('MeasureValue :' + str(MeasureValue))

        dummy_multi_measure = 'item_' + str(multi_measure_num)
        myitem = {
            'Name': dummy_multi_measure,
            'Value': MeasureValue,
            'Type': 'VARCHAR'
        },
        records.append(myitem)
        t = records[multi_measure_num - start_item_num]
        MultiMeasureValue.append(t[0])
    return MultiMeasureValue

関数 gen_item() を修正してスクリプトを実行できたらクエリを発行して確認してみます。

313-write-682-japanese-words

次に str_num = 683 に変更して再度スクリプトを実行します。なお、683 文字のひらがなは 2049 Byte です。
すると下記のようなエラーになりました。
(見やすさのため改行を入れています)

botocore.errorfactory.ValidationException: 
An error occurred (ValidationException) when calling the WriteRecords operation: 
The measure name and value of the multi-measure record with measure name dummy_metrics exceeds the maximum supported length for measure names and values. 
See Quotas in the Timestream developer guide for additional information.

これまでの結果より、マルチメジャーレコードにおけるバリュー値の最大長は2048 Byteであるようです。 (解釈に誤りがあるようでしたらご指摘いただけますと幸いです)

終わったら変更箇所をもとに戻しておきます。

Measures per table : 8192 の検証

Measures per table はシングルメジャーレコード、マルチメジャーレコードの両方に係るクォータになります。

目的

ここでは「テーブルあたりの メジャーの数(メジャー名の種類)が 8192 個」であることを確認します。

309-Timestream_Quota_for_blog_8192

検証手順

先程のスクリプトとあまり変わりませんが、少し修正した下記のコードで検証しました。

import boto3
from botocore.config import Config
import time
import random

session = boto3.Session()
write_client = session.client('timestream-write', region_name='us-east-1',
                              config=Config(read_timeout=20,    # リクエストタイムアウト(秒)
                              max_pool_connections=5000,        # 最大接続数
                              retries={'max_attempts': 10}))    # 最大試行回数

DatabaseName='mytestdb'
TableName='mytesttbl'

def current_milli_time():
    return round(time.time() * 1000)

def gen_dimensions():
    municipalities = random.choice(['Shinjuku', 'Toshima', 'Nakano', 'Ota', 'Chiyoda'])
    gateway_id = random.choice(['gateway_1', 'gateway_2', 'gateway_3'])
    device_name = str(municipalities) + '_' + str(gateway_id)
    dimensions = [
        {'Name': 'Location', 'Value': 'Tokyo'},
        {'Name': 'Municipalities', 'Value': municipalities},
        {'Name': 'DeviceName', 'Value': device_name}
    ]
    return dimensions

# 何件のメジャー値(アイテム、RDBのカラムに相当するもの)を作成するか
# 1レコードあたりに入れるアイテムを複数作成する
def gen_item():
    records = []
    MultiMeasureValue = []
    start_item_num = 0 # 任意のアイテム番号からMeasureValueを作成
    end_item_num = 1 # start_item_numから 256を超えない範囲で指定

    for multi_measure_num in range(start_item_num,end_item_num):
        #ランダムな数値データをメジャーバリューとして作成
        MeasureValue = str(random.uniform(1, 90))
        dummy_multi_measure = 'item_' + str(multi_measure_num)
        myitem = {
            'Name': dummy_multi_measure,
            'Value': MeasureValue,
            'Type': 'DOUBLE'
        },
        records.append(myitem)
        t = records[multi_measure_num - start_item_num]
        MultiMeasureValue.append(t[0])
    return MultiMeasureValue

# レコードの生成
def gen_dummy_record(measure_name_num):
    records_X = []
    # 何件のレコードを作成するか ; 100件:(0,100) 最大100
    for record_num in range(0,1):
        dummy_metrics = 'dummy_metrics_' + str(measure_name_num)
        dummy_measure = {
            'Dimensions': gen_dimensions(),
            'MeasureName': dummy_metrics,
            'MeasureValueType': 'MULTI',
            'MeasureValues': gen_item(),
            'Time': str(current_milli_time()),
            'TimeUnit': 'MILLISECONDS'
        }
        records_X.append(dummy_measure)
        time.sleep(1/1000) 
    return records_X

for write_num in range(0,8191): # 生成したレコードを何回書き込むか
    print ("start write_records...: " + str(write_num))
    result = write_client.write_records(DatabaseName=DatabaseName,
                                        TableName=TableName,
                                        Records=gen_dummy_record(write_num), CommonAttributes={})

急ごしらえのため少々効率が悪いですが、レコードを生成するタイミング(for write_num in range(0,8192)の箇所)で、1 レコード毎にメジャー名(measure_name)を変えて格納します。
これによりメジャー名が全て異なる 8192 行のレコードが書き込まれます。
dummy_metrics_0, dummy_metrics_1, …, dummy_metrics_8191 の8192個)

書き込めたら下記のSQLで measure_nameが一意であるレコード数をカウントして、8192 種類のmeasure_nameが登録されていることを確認します。

SELECT count(DISTINCT measure_name) FROM "mytestdb"."mytesttbl"

304-count-8192

格納されたレコードを確認すると、measure_nameの項目がdummy_metrics_8191という 8192 個目のレコードが最後に登録されていることが分かります。 (dummy_metrics_0dummy_metrics_8191の8192 個)

303-write-8192-measure-name

先程のスクリプトで書き込まれたレコードは下記の通り 8192行 なので、measure_nameの項目が全てバラバラなレコードが 8192 行書き込まれたことが確認できました。

304-count-8192

次に、スクリプトを下記のように変更して、8193 個目(8193種類目)のメジャー名(dummy_metrics_8192)を持つレコードを 1 行書き込んでみます。

Records=gen_dummy_measure(write_num), CommonAttributes={})
↓
Records=gen_dummy_measure(8192), CommonAttributes={})
for write_num in range(0,8192)
↓
for write_num in range(0,1)

スクリプトを実行すると下記のようなエラーが発生します。
(見やすさのため適宜開業しています。)

botocore.errorfactory.RejectedRecordsException: 
An error occurred (RejectedRecordsException) when calling the WriteRecords operation: 
One or more records have been rejected. See RejectedRecords for details.

次に、再度下記のようにコードを変更して、登録済みのメジャー名(dummy_metrics_8191)を持つレコードを1行追加で書き込んでみます。

Records=gen_dummy_measure(write_num), CommonAttributes={})
↓
Records=gen_dummy_measure(8191), CommonAttributes={})

登録できたらクエリを発行して、8193 個目のレコードとして追加できていることを確認します。

306-select-8193th-record

全レコード数が 8193 行となり、1レコード追加されたことも確認できました。

305-write-same-measure-name-record-8193th

以上のことから、 「テーブルあたり登録できるメジャーの最大数(measure_name の種類)は 8192 個」 であることが分かりました。

確認結果のまとめ

以上の検証で分かったことをまとめて表形式で図にしてみました。
(誤り等ありましたらご指摘いただけると幸いです)

311-Timestream_Quota_for_blog

最後に

マルチメジャーレコードを中心に Amazon Timestream のメジャーに関するクォータをいくつか確認してみました。
IoT のユースケースだと、無数のデバイスから多種多様なデータを扱いたい場合があると思います。これらのクォータを踏まえて最適な設計をしていきたいと思います。

以上です。