Amazon Web Services ブログ

Amazon DynamoDB の大量の時系列データの設計パターン

時系列データは、経時変化のパターンを示しています。たとえば、次のグラフの例に示すように、センサーを通じて環境データを記録するモノのインターネット (IoT) デバイスがあります。このデータには、温度、気圧、湿度、その他の環境変数が含まれる場合があります。各 IoT デバイスは定期的にこれらの値を追跡するため、バックエンドは毎秒最大数百、数千、または数百万のイベントを取り込む必要があります。

センサーデータのグラフ

このブログ記事では、大量の時系列データを扱うシナリオ用に Amazon DynamoDB を最適化する方法について説明します。これを、自動化とサーバーレスコンピューティングを活用した設計パターンを使用して行います。

DynamoDB で大量のイベントを設計する

通常、データ取り込みシステムには以下が必要です。

  • 現在の時間に関連する新しいレコードを取り込むための高い書き込みスループット。
  • 最近のレコード用の低いスループット。
  • 古いレコード用の非常に低いスループット。

このようなイベントをすべて単一の DynamoDB テーブルに格納する場合、ホットパーティション (最新のレコード) とアクセス頻度の低いパーティション (古いレコード) があります。アクセス頻度の低いパーティションは、プロビジョニングされた書き込み容量を、新しいレコードを保存しておきたいホットパーティションから逸らしてしまうため、最適ではありません。さらに、分析ニーズに適した期間を設計したいことがよくあります。たとえば、過去数日間または数か月間のデータを分析するとします。これらの期間の長さを調整することで、分析パフォーマンスとコストの両方を最適化できます。

DynamoDB の一般的な設計原則では、できるだけ少ない数のテーブルを使用することが推奨されていますが、時系列データでは、その設計原則に反して、各期間に複数のテーブルを作成します。この記事では、このようなアンチパターンを DynamoDB に使用する方法を説明しますが、時系列データには最適です。

オンデマンドキャパシティーモードを選択しない限り、すべての DynamoDB アクセスパターンは、読み取り容量単位と書き込み容量単位の異なる割り当てを必要とします。この記事では、レコードの読み書き頻度に基づいて、レコードを次の 3 つの異なるグループに分類します。

  • 毎秒書き込まれる新しいレコード
  • よく読み取られる最近のレコード
  • あまり読み込まれない古いレコード

新たに取り込まれたレコードの書き込みスループットを最大にするには、期間ごとに新しいテーブルを作成し、最大数の書き込み容量単位と最小数の読み取り容量単位を割り当てます。また、各期間の終了前に次の期間のテーブルを事前作成して、現在の期間が終了したときにすべてのトラフィックをそのテーブルに移動できるようにします。古いテーブルへの新しいレコードの書き込みをやめると、その書き込み容量単位を 1 に減らすことができます。短期間の読み取り要件に基づいて、適切な読み取り容量単位もプロビジョニングします。次の期間が終了したら、読み取り容量や書き込み容量をオーバープロビジョニングしたくないので、割り当てられている読み取り容量単位も減らします。

また、新しい期間に切り替える頻度を見積もる際にも、分析上のニーズを考慮する必要があります。たとえば、昨年何が起こったのか分析したいと思った場合、4 つの並列クエリでデータをより効率的に取得して 4 つの結果セットを集計できるように、四半期ごとのテーブルを使用できそうです。

他のユースケースでは、前四半期のデータだけを分析したいと思うかもしれず、また毎月のテーブルを使用しようと考えるかもしれません。これにより、3 つの並列クエリを実行して分析を実行できます (四半期の各月に 1 回ずつ)。一方、この分析で特定の洞察が日単位で必要な場合は、日次テーブルを選択することを考えるかもしれません。

この記事の残りの部分では、日次テーブルを使用した後者のシナリオに焦点を当てます。昨日のデータが分析に適していて、より古いデータは頻繁にアクセスされないとします。真夜中の直前に新しいテーブルを作成し、真夜中の直後に古いテーブルの書き込み容量単位を減らすためにスケジュールされたジョブを設定しました。

このようにして、いつも以下があるようにします。

  • 1,000 の書き込み容量単位と 300 の読み取り容量単位 (最大の書き込み数と一部の読み取り数) を持つ今日のテーブル。
  • 1 書き込み容量単位と 100 読み取り容量単位を持つ昨日のテーブル (最小の書き込み数と一部の読み取り数)。
  • 1 書き込み容量単位と 1 読み取り容量単位を持つ古いテーブル (アクセスすることはほとんどないでしょう)。

また、DynamoDB Auto Scaling を設定して、容量を一切オーバープロビジョニングやアンダープロビジョニングしないようにすることもできます。このようにして、必要に応じて読み取り容量単位を流し、現在のテーブルの最大書き込み容量単位を 1,000 にプロビジョニングできます。

分析に適した日が予測できないシナリオでは、古いテーブルでオンデマンドキャパシティーモードを有効にできます。これにより、容量を気にせずに毎日のデータを読み取ることができます。この記事の残りの部分では引き続き、分析上のニーズは予測可能であり、読み取り容量を完全に制御したいと考えているとします。

AWS Lambda を使用してテーブルを自動的に事前作成して書き込み容量を減らす方法

AWS LambdaAmazon CloudWatch Events を使用することで、毎日深夜 0 時にテーブルの作成とサイズ変更を自動化します。Lambda 関数とそのトリガー、アクセス許可、環境変数に関連する一連の AWS リソースであるサーバーレスアプリケーションを作成します。

この単純なサーバーレスアプリケーションは、データストア、RESTful API、メッセージキュー、または複雑なオーケストレーションを必要としません。Lambda 関数の呼び出しを毎日深夜の前後数分にスケジュールしたいと思います。CloudWatch Events では、cron のような構文を使用してこれを実行できます。

まず、サポートされている言語 (Node.js、Java、Python、Go、C# など) でテーブルの作成とサイズ変更ロジックを実装します。公式の AWS SDK for Python (boto 3) を使用して、Python ですべてを実装しました。

この記事の後半では、次のコードを使用してデプロイしますが、最初に簡単に説明します。

import os
import boto3
import datetime

region = os.environ.get('AWS_DEFAULT_REGION', 'us-west-2')
dynamodb = boto3.client('dynamodb', region_name=region)

class DailyResize(object):
    
    FIRST_DAY_RCU, FIRST_DAY_WCU = 300, 1000
    SECOND_DAY_RCU, SECOND_DAY_WCU = 100, 1
    THIRD_DAY_RCU, THIRD_DAY_WCU = 1, 1

    def __init__(self, table_prefix):
        self.table_prefix = table_prefix
    
    def create_new(self):
        # create new table (300 RCU, 1000 WCU)
        today = datetime.date.today()
        new_table_name = "%s_%s" % (self.table_prefix, self._format_date(today))
        dynamodb.create_table(
            TableName=new_table_name,
            KeySchema=[       
                { 'AttributeName': "pk", 'KeyType': "HASH"},  # Partition key
                { 'AttributeName': "sk", 'KeyType': "RANGE" } # Sort key
            ],
            AttributeDefinitions=[       
                { 'AttributeName': "pk", 'AttributeType': "N" },
                { 'AttributeName': "sk", 'AttributeType': "N" }
            ],
            ProvisionedThroughput={       
                'ReadCapacityUnits': self.FIRST_DAY_RCU,
                'WriteCapacityUnits': self.FIRST_DAY_WCU,
            },
        )
    
        print("Table created with name '%s'" % new_table_name)
        return new_table_name
    
    
    def resize_old(self):
        # update yesterday's table (100 RCU, 1 WCU)
        yesterday = datetime.date.today() - datetime.timedelta(1)
        old_table_name = "%s_%s" % (self.table_prefix, self._format_date(yesterday))
        self._update_table(old_table_name, self.SECOND_DAY_RCU, self.SECOND_DAY_WCU)
    
        # update the day before yesterday's table (1 RCU, 1 WCU)
        the_day_before_yesterday = datetime.date.today() - datetime.timedelta(2)
        very_old_table_name = "%s_%s" % (self.table_prefix, self._format_date(the_day_before_yesterday))
        self._update_table(very_old_table_name, self.THIRD_DAY_RCU, self.THIRD_DAY_WCU)
        
        return "OK"
        
    
    def _update_table(self, table_name, RCU, WCU):
        """ Update RCU/WCU of the given table (if exists) """
        print("Updating table with name '%s'" % table_name)
        try:
            dynamodb.update_table(
                TableName=table_name,
                ProvisionedThroughput={
                    'ReadCapacityUnits': RCU,
                    'WriteCapacityUnits': WCU,
                },
            )
        except dynamodb.exceptions.ResourceNotFoundException as ex:
            print("DynamoDB Table %s not found" % table_name)
    
    
    @staticmethod
    def _format_date(d):
        return d.strftime("%Y-%m-%d")

前述のコードの例で示したように、この実装はプラットフォームに依存しません。すべてが DailyResize という Python クラスにカプセル化されており、2 つのパブリックメソッドがあります。create_newresize_oldです。このコードを Lambda 関数ハンドラーに含める resizer.py ファイルに保存します。このようにコードを書くことで、Lambda 自体からメインロジックを切り離すことができます。こうすれば、Amazon Elastic Container ServiceAmazon EC2 のように、さまざまなコンピューティングプラットフォームでこのコードを再利用できます。

ここで Lambda 関数ハンドラーを実装します。次のコードをこの記事の後半で使用してデプロイします。

import os
from resizer import DailyResize

def daily_resize(event, context):
    operation = event['Operation']
    resizer = DailyResize(table_prefix=os.environ['TABLE_NAME'])
    if operation == 'create_new':
        resizer.create_new()
    elif operation == 'resize_old':
        resizer.resize_old()
    else:
        raise ValueError("Invalid operation")

前述の Python コードでわかるように、Lambda ハンドラーはイベントと呼び出しコンテキストを入力として受け取る Python 関数です。コードは着信イベントから Operation パラメータを抽出し、それを使用して実行する操作を決定します。Lambda の詳細からメインロジックを切り離したので、Lambda ハンドラーは最小限で、DailyResize クラスのアダプターとしてのみ機能します

これで、メインロジックと Lambda ハンドラーを実装しました。AWS Serverless Application Model (AWS SAM) を使用して、サーバーレスアプリケーションを構成するすべての AWS リソースを定義できます。ベストプラクティスとして、インフラストラクチャのガイドラインにコードとして従います。そのため、後でアプリケーションをデプロイするために使用する YAML ファイル (このファイルを template.yml と呼びます) でサーバーレスアプリケーションを定義します。

AWS::Serverless::Function リソースと 1 日 2 回の予定された呼び出しをその Events として定義します。CloudWatch Events により、各呼び出しの入力イベントを定義することで、1 日の時間に基づいて実行する異なる Operation を Lambda ハンドラーが受け取れます。

  • CreateNewTableEveryDay: 毎日午後 11 時 45 分に実行され、create_new 操作を行います。
  • ResizeYesterdaysTablesEveryDay: 毎日午前 0 時 15 分に実行され、resize_old 操作を実行します。

サーバーレスアプリケーションの定義に関するもう 2 つの重要な詳細:

  • テンプレートには、TablePrefix という入力パラメーターが必要です。これは、ピリオドサフィックスを追加するために使用します (例えば、timeseries_2018-10-25)。
  • Lambda 関数に細かい権限を付与して、同じアカウントおよび AWS リージョン内のテーブルに対して 2 つの DynamoDB API (dynamodb:CreateTable および dynamodb:UpdateTable) を呼び出すことができるようにします。その名前は、指定された TablePrefix で始まります。

次のコードは、AWS SAM テンプレート全体を示しています。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters: 
  TablePrefix:
    Type: String
Resources:
  TableDailyResize:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler.daily_resize
      Policies:
        - AWSLambdaExecute # Managed Policy
        - Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - dynamodb:CreateTable
                - dynamodb:UpdateTable
              Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TablePrefix}*'      
      Runtime: python2.7
      Timeout: 30
      MemorySize: 256
      Environment:
        Variables:
          TABLE_NAME: !Ref TablePrefix
      Events:
        CreateNewTableEveryDay:
          Type: Schedule
          Properties:
            Input: '{"Operation": "create_new"}'
            Schedule: cron(45 23 * * ? *)  # every day at 11.45PM
        ResizeYesterdaysTableEveryDay:
          Type: Schedule
          Properties:
            Input: '{"Operation": "resize_old"}'
            Schedule: cron(15 0 * * ? *)  # every day at 00.15AM

このサーバーレスアプリケーションを AWS SAM テンプレートにデプロイするには、AWS SAM CLI を使用します。これは、サーバーレスアプリケーションのローカル開発とテスト用のツールです。まず、テンプレートを使って sam package を実行し、コードを Amazon S3 にアップロードしてコンパイル済みバージョンのテンプレートを生成します。次に、コンパイルしたテンプレートで sam deploy を実行して CloudFormation に送信します。これは AWS::Serverless Transform を適用し、すべてのリソースとトリガーをプロビジョニングします。

# install AWS SAM
pip install --user aws-sam-cli

# configure your AWS credentials
aws configure

# package your raw YAML template
sam package
    --template-file template.yml \
    --s3-bucket YOUR_BUCKET \
    --output-template-file compiled.yml

# deploy your compiled YAML template
sam deploy
    --template-file compiled.yml \
    --stack-name YOUR_STACK_NAME \
    --capabilities CAPABILITY_IAM \
    --parameter-overrides TablePrefix=YOUR_PREFIX_

それでおしまいです! この関数が実行されるとすぐに、新しいテーブルは新しいレコードを受け取ることができるようになります。DynamoDB に新しいレコードを書き込んでいるアプリケーションは、正確に真夜中に新しいテーブルに書き込みを開始して、明日のレコードが新しい日次テーブルに書き込まれます。深夜まで待ちたくない場合は、クーロンのようなスケジュールを変更できます。ウェブコンソールまたは API を介して Lambda 関数を呼び出して、最初のテーブル作成をトリガーできます。

次のスクリーンショットは、数日後の DynamoDB コンソールの結果を示しています。私は毎日 1 つのテーブルを持っています。今日のテーブルには、1,000 書き込み容量単位と 300 読み取り容量単位があります。昨日のテーブルには 1 書き込み容量単位と 100 読み取り容量単位があり、古いテーブルには 1 書き込み容量単位と 1 読み取り容量単位があります。

数日後の DynamoDB コンソールでの結果。

設計をさらに拡張して、DynamoDB Streams を使用してコストを削減し、ビッグデータ分析シナリオを可能にすることで、古いテーブルのデータを Amazon S3 にアーカイブできます。

まとめ

時系列データには、DynamoDB のアンチパターンと一般的には考えられている最適化手法が必要です。これらの手法の 1 つは、期間ごとに複数のテーブルを使用することです。この手法は書き込みスループットを最大化し、頻繁にアクセスされないデータと分析クエリの両方のコストを最適化します。

この記事では、Lambda、CloudWatch Events、および AWS SAM を使用して、テーブルの事前構築と書き込み容量の縮小を自動化する方法を説明しました。この記事で実装するアーキテクチャーは、人手による介入、サーバーへのパッチ適用やインフラストラクチャの保守を必要としないため、完全に自動化され、サーバーを必要としません。分析パターンを容易に予測できない場合は、オンデマンドキャパシティーモードを使用すると、提案するソリューションをさらに単純化することができる可能性があることを覚えておいてください。

新しく、高速、スケーラブルで、完全マネージド型の時系列データベースである Amazon Timestream が現在プレビューにあります。Amazon Timestream が利用できても、この記事の設計パターンと考察は、DynamoDB の時系列のユースケースに有効な選択肢です。

このブログの投稿に関するコメントは、以下のコメントセクションからお送りください。または、DynamoDB フォーラムに新しいスレッドを作成してください。


著者について

Alex の写真 Alex Casalboni は、AWS のテクニカルエバンジェリストです。彼の趣味はサックスの演奏、ジョギングや旅行です。