このエントリーをはてなブックマークに追加

DynamoDBのデザインパターン

DynamoDBはフルマネージドのNoSQLデータベースで、うまく活用することで運用とスケーリングの管理作業から開放されます。
弊社で開発中のサービスではサーバレスアーキテクチャーを採用しており、API Gateway, Lambda, DynamoDBを中心にAWSを利用しています。
その中でもDynamoDBの設計には頭を悩ませており、いろいろとデザインパターンについて調べた結果を記録として記事にします。

前提条件: DynamoDBの概要・用語

デザインパターンを説明する前に、DynamoDBの概要を記しておきます。

キャパシティーユニット

RDSやElastiCacheと異なり、料金の算出方法がインスタンスの稼働時間ではなく、ストレージ容量と予約されたスループットになります。
スループットは読み込み用と書き込み用でそれぞれ容量を指定することができ、キャパシティーユニット(CU)と言う単位で計算されます。

  • 書き込みに必要な容量のユニット数(WCUs) = 1 秒あたりの項目書き込み回数 x 項目のサイズ (1 KB ブロック)
  • 読み込みに必要な容量のユニット数(RCUs)* = 1 秒あたりの読み込み回数 x 項目のサイズ (4 KB ブロック)
    ※結果整合性のある読み込みを使用する場合は、1 秒あたりの読み込み回数によってスループットが 2 倍になります。

@Link 読み込み/書き込み容量のユニットとは何ですか - よくある質問 - AWS

テーブル

RDBSのテーブルと同様です。テーブルには複数の項目(Item)が含まれます。各Item内には複数の属性(Attribute)が含まれますが、プライマリーキー以外はスキーマレスです。

属性
(Partition Key)
属性
(Sort Key)
属性属性
項目ID: 1Date: 2010-12-01Name: FredPhone: 333-3333
項目ID: 2Date: 2010-12-10Name: MaryCountry: Japan

プライマリーキーとパーティション

プライマリーキーは2種類あります。プライマリーキーに何をどのように入れるかが非常に重要だということがわかりました。
後にデータ量や読み書きの頻度が増えた場合に大きな影響を与えることになります。

  • パーティションキー - Key-valueパターンでアクセスする場合
  • パーティションキー + ソートキー (複合プライマリーキー)- 柔軟なクエリが必要な場合(ソート、範囲検索など)、または1対Nのリレーションモデルを実現する場合など

DynamoDBのデータはパーティションに保存されます。プライマリーキーがパーティションキーのみの場合は、パーティションキーの値からハッシュを計算しItemが保存されるパーティションが決定されます。利用者側でパーティション数やどのパーティションへデータを保存するか指定することはできません。
複合プライマリーキーの場合も同様にパーティションキーのハッシュ値をもとにパーティションが決定されますが、同じパーティションキーを持つItemはソートキーによって並べ替えられて物理的に近くに配置されます。

パーティション数はテーブルを作成した時点で、スループット設定により次の計算式で決定されます。

初期パーティション数 = ROUNDUP( (RCUs / 3000) + (WCUs / 1000) )

1つのパーティションには約10GBのデータを保存できるため、データ格納後のパーティション数は次の計算式になります。

BY_CAPACITY = ROUNDUP( (RCUs / 3000) + (WCUs / 1000) )
BY_SIZE = ROUNDUP(データ量の合計 / 10GB)
パーティション数 = MAX(BY_CAPACITY, BY_SIZE)

テーブルのパーティションの数が増えると、各パーティションで利用できる読み込みキャパシティーと書き込みキャパシティーユニットの数が減ります。ただし、テーブルのプロビジョニングされたスループットの合計は同じままです。

セカンダリインデックス

DynamoDBではプライマリーキーを使用して、高速にデータへアクセスすることが可能ですが、ほとんどのアプリケーションではそれ以外の属性を使ってデータを取得する必要があります。DynamoDBでは2種類のセカンダリインデックスが用意されており、状況に応じて使い分けます。

  • ローカルセカンダリインデックス(LSI) - 元のテーブルと同じパーティションキーと、異なるソートキーを持つインデックス。パーティションの範囲が、同じパーティションキーの値を持つベーステーブルパーティションに限定されます。サイズは10GBに制限されます。
  • グローバルセカンダリインデックス(GSI) - 元のテーブルと異なるパーティションキーと任意でソートキーを持つインデックス。このインデックスのクエリは全てのパーティションのすべてのデータを対象に実行でき、サイズ制限はありません。

アクセスパターンごとのテーブル設計

できるだけ均一にデータを分散させることでアプリケーションの性能を上げることができるため、テーブル設計時には十分に考慮する必要があります。
パーティションが分割された場合、そのパーティションのCUsも半分になってしまいます。この時データの配置に偏りがあり、1つのパーティションにアクセスが集中すると、全体として十分なキャパシティーユニットを指定したつもりでも、不均一なデータのせいでエラーになってしまうことがあります。

時系列データを扱う

時系列データの場合は、一般的に新しいデータに対して多くの読み書きが発生し(Hot Data)、一方で過去のデータは読み込みのみが発生するか、ほとんどアクセスされない(Cold Data)ことが多いと思います。
この場合はアプリケーションの仕様に応じて、年単位・月単位・週単位などでテーブルを分割することで効率よくスループットを設定できます。 プライマリーキーはパーティションキー + ソートキー(日付)になると思います。
Hot DataとCold Dataを混在させないことが重要です。またほとんどアクセスされないデータはS3に保存したほうがコスト節約になります。

(現在:2017年5月の場合)
Table: 201705Logs: 1000RCUs + 500WCUs (Hot Data)
Table: 201704Logs: 200RCUs + 1WCUs (Cold Data)
Table: 2016Logs: 10RCUs + 1WCUs (Cold Data)

読み込み頻度に偏りがある場合: キャッシュを活用する

例えばEコマースのWebサイトで、商品情報をDynamoDBに保存する場合を考えます。
商品によっては人気があってよくアクセスされたり、あるいはめったにアクセスされないものがあると思いますし、タイムセールなどで突発的に一部の商品にアクセスが集中することも考えられます。
このとき一部のパーティションにアクセスが集中しRCUsが全て消費されてしまうことが考えられますが、回避することはとてもむずかしいと思います。

こういった場合はElastiCacheなどにキャッシュすることでDynamoDBへのアクセスを制御します。
人気商品はキャッシュのヒット率が高くなるはずですので、結果的にDynamoDBでのスループットが安定することになります。
当然、ElastiCacheの料金が発生するので、消費されるRCUsとキャッシュのインスタンス料金を考慮してコストが削減できるかどうか判断する必要があります。

複数の条件でソートする必要がある場合: 複合ソートキー

DynamoDBでは並び替えをソートキーでしか行うことができません。もしソート条件が複数ある場合は一工夫が必要になります。
例としてタスク管理アプリケーションを想定します。タスクには担当者(UserID)、日付(Date)、状態(Status)、内容(Body)のフィールドがあるとします。

ParitionKeySortKeyAttrAttr
38293822015-10-01DONE{"content": "コーヒーを買う"}
28392832015-10-01IN_PROGRESS{"content": "犬の散歩"}
28392832015-10-05IN_PROGRESS{"content": "犬の散歩"}
38293822015-10-06PENDING{"content": "コーヒーを買う"}

普通にテーブルを設計するとこのような形が自然です。このテーブルではタスクを日付順でソートすることができます。
もしこのアプリケーションに「状態ごとに日付で並び替えて表示する」という要件が加わったらどうすればいいでしょうか?

この場合の解決方法は、Sort Keyに日付と状態を含めたセカンダリインデックスを作成することです。

ParitionKeySortKeyAttr
3829382DONE_2015-10-01{"content": "コーヒーを買う"}
2839283IN_PROGRESS_2015-10-01{"content": "犬の散歩"}
2839283IN_PROGRESS_2015-10-05{"content": "犬の散歩"}
3829382PENDING_2015-10-06{"content": "コーヒーを買う"}

参照資料

https://www.slideshare.net/AmazonWebServices/design-patterns-using-amazon-dynamodb
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GuidelinesForTables.html#GuidelinesForTables.Partitions
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.ProvisionedThroughput.html#HowItWorks.ProvisionedThroughput.Reads
https://aws.amazon.com/jp/dynamodb/faqs/#What_is_a_readwrite_capacity_unit