AWS DynamoDBのまとめ(AWS DVA対策)

AWS DVA

AWS Certified Developer – Associate(DVA) 対策として、Amazon DynamoDBの特徴をまとめました。

DynamoDBとは

サーバーレス、フルマネージド、key-value型のNoSQLデータベースサービス。

DynamoDBのコアコンポーネント

DynamoDBのコアコンポーネントは、テーブル、項目、属性の3つ。
例えば、以下のUsersテーブルを例に考えてみる。

  • テーブル(赤枠):SQLと同様にDynamoDBもデータをテーブルに保存する
  • 項目(緑枠):テーブルは0以上の項目で構成される。属性の集まり。SQLでいう行。プライマリキー以外、各項目は独自の固有の属性を持つ事ができる。(テーブルはスキーマレス。属性またはデータ型を事前に定義する必要がない)
  • 属性(青枠):各項目は1つ以上の属性で構成される。SQLでいう列。持っている属性は項目によって違っても良い。

このテーブルではプライマリキーがUserID(パーティションキー(詳細は後述))となっている。
また、Name、Age、Email、Tel、Hobbiesなどの属性があるが、項目によってはTel属性がない場合や、Hobbies属性を持っていないこともある。

SQLとDynamoDBの用語比較

SQLDynamoDB
テーブルテーブル
項目
属性
プライマリキープライマリキー
インデックスセカンダリインデックス
参考:NoSQL とは?(NoSQL データベースの解説と SQL との比較)| AWS

プライマリキーとは

テーブルを作成する際、テーブル名に加えて、テーブルのプライマリキーを指定する必要がある。
プライマリキーはテーブルの各項目を一意に識別するための値。つまり、テーブル内に同じプライマリキーを持つ項目はない。

プライマリキーは1つの属性で構成するか2つの属性で構成するかを選択する。

1つの属性をプライマリキーにする(パーティションキー)

  • その属性のことをパーティションキーという
  • この場合、2つの項目が同じパーティションキー値を持つことはない。
  • Usersテーブルで言うと、UserID=001という項目は1つしか存在しない

2つの属性をプライマリキーにする(複合プライマリキー)

  • 1つ目をパーティションキー、2つ目をソートキーと呼ぶ
  • パーティションキー + ソートキーの組み合わせを「複合プライマリキー」と呼ぶ
  • パーティションキーは複数の項目で同じ値を持つことができる。ただし、パーティションキー+ソートキーの組み合わせが同じ項目はない
  • 例えば、以下の例に示したMusicテーブルはパーティションキーがArtistで、ソートキーがSongTitleとなっている。Artist=BUMP OF CHICKENは複数項目取得できるが、Artist=BUMP OF CHICKEN, SongTitle=sailing dayの項目は1つだけ
(例)Musicテーブル

セカンダリインデックス

GSI(グローバルセカンダリインデックス)とLSI(ローカルセカンダリインデックス)の2種類をサポートしている。追加することで検索に柔軟性を持たせられる。

GSI(グローバルセカンダリインデックス)LSI(ローカルセカンダリインデックス)
パーティションキーテーブルと異なる属性を指定できるテーブルと同じ
ソートキー自由に指定できる別の属性を指定できる
追加タイミングテーブル作成後でも追加可能テーブル作成時にしか定義できない
強力な整合性のある読み込み非対応対応
キャパシティGSI専用にRCU/WCUの割り当てが必要追加のRCU/WCU割り当ては不要(ベーステーブルの容量を共有)
クエリ例パーティションキー:Genre
ソートキー:ReleaseYear
→「ロックジャンルの曲を発売年順で取得したい」
パーティション:Artist(ベーステーブルと同じ)
ソートキー:ReleaseYear
→「同じアーティストの曲をリリース順で並べる」

Query/Scanの使い分け

DynamoDBでデータを取得する方法は主に GetItemQueryScan の3つがある。

Query

  • パーティションキー(必要であればソートキーも)を指定して、条件に合う項目だけを取得する
  • KeyConditionsExpressionでパーティションキー(+ソートキー)の条件を指定する。例:Artist = :artists AND SongTitle > :title
  • 取得した項目に対して、さらにFilterExpressionで絞り込みができる。ただし、フィルターはDynamoDBが項目を読み込んだ「後」に適用されるため、消費するキャパシティユニットは絞り込み前の件数分になる(フィルターで件数を減らしてもコストは減らない)
  • 返す属性を絞りたい場合はProjectionExpressionを使うと、不要な属性を転送しないので通信量を抑えられる

Scan

  • テーブル(またはインデックス)全体を先頭から読み込み、条件に合う項目を返す
  • パーティションキーの指定が不要な代わりに、テーブルが大きくなるほど遅く・高コストになりやすい
  • 後述の「キャパシティユニットの消費量」の例4のとおり、Scanは実際に条件に合致した項目ではなく、読み込んだ項目全体のサイズが計算対象になる

基本的にはQueryで済むようにテーブル設計(パーティションキー・ソートキー・セカンダリインデック)を考えて、Scanは最終手段くらいに考えておくと良さそう

DynamoDB Streams

DynamoDBテーブルのデータ変更イベントをキャプチャするオプション。
これらのイベントはほぼリアルタイムに、イベントの発生順にストリームに表示される。
各イベントはストリームレコードによって表される。
DynamoDBで以下のイベントが発生するたびにストリーミングレコードが書き込まれる

イベント取得データ
(作成)新しい項目がテーブルに追加された場合すべての属性を含む項目全体のイメージ
(変更)項目が更新された場合項目で変更された属性の「前」と「後」のイメージ
(削除)テーブルから項目が削除された場合削除される前の項目全体のイメージ
  • 各ストリームレコードにはテーブル名、イベントのタイムスタンプ、その他メタデータが含まれる
  • ストリームの保持期間は24時間で、それ以降は自動的に削除される。

テーブル変更時にストリームに書き込まれる情報をStreamViewTypeで指定できる

StreamViewType内容
KEYS_ONLY変更された項目のキー属性のみ
NEW_IMAGE変更後に表示される項目全体
OLD_IMAGE変更前に表示されていた項目全体
NEW_AND_OLD_IMAGE項目の新しいイメージと古いイメージ

Streams使用例:新規登録時のウェルカムメール送信

顧客情報を持つCustomerテーブルでDynamoDB Streamsを有効化し、Lambda関数に関連付ける。

  1. Customerテーブルに新規ユーザーが追加される
  2. ストリームレコードがLambda関数に渡される
  3. Lambda関数がEmailAddress属性を参照し、Amazon SESを呼び出してメールを送信する

Time to Live (TTL)

TTLを有効化すると、不要になった項目を、WCUを消費することなく削除できる
例えば、expireAtという属性をTTL属性として設定し、その属性にエポック秒で有効期限を設定すると、有効期限が切れてから数日以内に項目が自動的に削除される
設定方法

  • テーブルでTTLを有効化する
  • 有効期限を設定する属性を決める(例:expireAt

例えば expireAt をTTL属性として設定し、そこにエポック秒で有効期限を入れておくと、期限切れ後に項目が自動的に削除される。

グローバルテーブル

複数AWSリージョンにまたがって、自動的にデータを複製・同期するDynamoDBテーブル
仕組み

  • 例:東京リージョンとオレゴンリージョンに同じテーブルを持たせる
  • どちらのリージョンに書き込んでも、自動的にもう一方のリージョンへレプリケーションされる
  • 結果として、どのリージョンからでも同じデータにアクセスできる

メリット

  • 東京リージョンで障害が発生しても、オレゴンリージョンのテーブルでサービスを継続できる(可用性の向上)
  • 日本のユーザーは東京リージョン、米国のユーザーはオレゴンリージョンというように、地理的に近いリージョンにアクセスさせることでレスポンスを高速化できる

バックアップとポイントインタイムリカバリ(PITR)

グローバルテーブルは「リージョン障害」への備えだったが、こちらは「誤って項目を消してしまった」「アプリのバグでデータを壊してしまった」といった事故への備え。

DynamoDBのバックアップには2種類ある。

オンデマンドバックアップ

  • 好きなタイミングで手動(またはAPI経由)でフルバックアップを取得する
  • テーブルのパフォーマンスに影響を与えず、保持期間の制限もない(消すまで残る)

ポイントインタイムリカバリ(PITR)

  • 有効化しておくと、過去35日以内の任意の秒単位の時点にテーブルを復元できる
  • 「1時間前の状態に戻したい」のような復元に強い
  • 有効化するだけで継続的にバックアップが取られる(オンデマンドバックアップのように都度実行する必要はない)

どちらも、復元は新しいテーブルとして作成される点は共通(元のテーブルに直接上書きされるわけではない)。「常に守りたい」ならPITR、「大きな変更の直前に念のため取っておきたい」ならオンデマンドバックアップ、というイメージで使い分けるとよさそう。

アクセス制御(項目レベルの権限制御)

DynamoDBのアクセス制御は基本的にIAMポリシーで行うが、DVA的に面白いのが「項目単位」でアクセスを絞れる仕組み。

通常のIAMポリシーはテーブル単位・API単位(GetItemは許可するがDeleteItemは許可しない、など)で権限を制御する。それに加えて、IAMポリシーの条件キー dynamodb:LeadingKeys を使うと、パーティションキーの値を条件にしてアクセス範囲を絞ることができる。

ユースケース

Usersテーブルに全ユーザーの情報をまとめて保存しているが、各ユーザーには「自分の項目だけ」を読み書きさせたい
Cognitoなどで発行した一時的な認証情報(IAMロール)に、dynamodb:LeadingKeys${cognito-identity.amazonaws.com:sub} のようなプレースホルダーを条件として設定する
これにより、ユーザーAの認証情報ではパーティションキーがユーザーAのものと一致する項目にしかアクセスできなくなる

テーブルをユーザーごとに分けなくても、1つのテーブルの中でアクセス範囲を制限できるのがポイント。

読み込みの整合性

読み込み:DynamoDBテーブルからのデータ取得
書き込み:DynamoDBテーブルへのデータの挿入、更新、削除

読み込み/書き込みはアプリケーションのパフォーマンスとコストに影響し、オペレーションの種類により消費するキャパシティユニットが異なる

読み取り整合性の対象

  • DynamoDB テーブル
  • ローカルセカンダリインデックス(LSI)
  • グローバルセカンダリインデックス(GSI)
  • ストリーム

2つの読み込み整合性オプション

結果整合性のある読み込み(デフォルト)

全ての読み取りオペレーションのデフォルト。結果整合性のある読み取りでは、直近でテーブルに書き込まれた内容が反映されない場合があり、少し時間が経ってから再度読み取りリクエストをすると最新の項目が取得できる。強力な整合性のある読み込みの半分のコストで済む。

強力な整合性のある読み込み

読み取りオペレーション(GetItem, Query, Scan)には、オプションのConsistentReadパラメーターがある。これをtrueにすると必ず最新データを取得できる。

対応範囲に注意:強力な整合性のある読み込みに対応しているのはDynamoDBテーブルとLSI(セカンダリインデックス)のみ。GSI(グローバルセカンダリインデックス)とDynamoDB Streamsは強力な整合性のある読み込みに対応していない。

まとめ

読み込み整合性オプション対象動作
結果整合性のある読み込み(デフォルト)テーブル、LSI、GSI、ストリーム最新ではないデータが取得される可能性がある。強力な整合性のある読み込みの半分のコスト
強力な整合性のある読み込みテーブル、LSI必ず最新のデータを取得する

条件付き書き込み(楽観的ロック)

ここまでは「読み込み」の整合性の話だったが、「書き込み」側にも気をつけたいポイントがある。それが条件付き書き込み。

DynamoDBの PutItem UpdateItem は、デフォルトでは「今の状態がどうであれ上書きする」という動作になる。複数のユーザーやプロセスが同時に同じ項目を更新すると、後から書き込んだ方が先の変更を意図せず消してしまう可能性がある。

これを防ぐのが ConditionExpression(条件式)。書き込みの実行前に「指定した条件を満たす場合のみ書き込みを実行する」という制御ができる。

使い方のイメージ

  • 項目に version のような属性を持たせておく
  • 更新時に ConditionExpression: "version = :expectedVersion" を指定する
  • 条件が満たされない(=他のプロセスが先に更新していてversionが変わっている)場合、書き込みは失敗し ConditionalCheckFailedException が返される
  • アプリ側は失敗を検知したら、最新の項目を読み直してから再度更新を試みる

競合が発生する確率は低いという立場で、事前にロックを取得するのではなく「更新時に競合をチェックする」方式なので、楽観的ロック(Optimistic Locking)と呼ばれるらしい。

トランザクション

条件付き書き込みは「1つの項目」に対する競合防止だったが、「複数の項目」や「複数のテーブル」にまたがって、全部成功するか全部失敗するかを保証したい場合はトランザクションを使う。

DynamoDBには次の2つのトランザクション用APIがある。

  • TransactWriteItems:最大100件の項目に対する書き込み(PutItem/UpdateItem/DeleteItem/ConditionCheck)をまとめて実行し、すべて成功するかすべて失敗するかのどちらかになる(ACID特性)
  • TransactGetItems:複数の項目をまとめて、一貫性のある状態で読み込む

ユースケースの例

銀行口座アプリで「Aさんの口座から1万円引く」処理と「Bさんの口座に1万円足す」処理は、両方成功するか両方失敗するかのどちらかであってほしい。片方だけ成功するとお金が消えたり増えたりしてしまう
在庫管理で「注文テーブルに注文を追加する」処理と「在庫テーブルの在庫数を減らす」処理をセットで行いたい

後述の「キャパシティユニット消費量」の表にある「トランザクション」の消費量(読み込み2、書き込み2)は、通常のオペレーションの倍かかることを覚えておくと、コスト面でも判断しやすい。

キャパシティユニット消費量

読み込みの場合

読み込みタイプによってキャパシティユニットの消費量がことなる(4KB以下の項目を読み取る場合)。

読み込みタイプ消費キャパシティユニット
強力な整合性1
結果整合性0.5
トランザクション2
*読み込み項目サイズは4KBの倍数に切り上げられる。
  • (例1)GetItem オペレーションでテーブルから10KBの単一の項目を読み込んだ場合、項目のサイズは4KBの倍数まで切り上げられるため、読み込む項目のサイズは12KBとして計算される。
  • (例2)BatchGetItemオペレーション(1つ以上のテーブルから最大100個の項目の読み込み)の場合、バッチ内の各項目を個別のGetItemリクエストとして処理する。例えば、1.5KBと6.5KBの2つの項目を読み込んだ場合、読み込む項目のサイズは4KB + 8KBとして計算される。(1.5KB+6.5KB=8KBではない)
  • (例3)Queryオペレーションの場合。返されるすべての項目の合計サイズを単一の読み込みオペレーションとして扱う。Queryオペレーションの結果、合計サイズが40.8KBの10項目が返される場合、読み込む項目のサイズは44KBとして計算される。
  • (例4)Scanオペレーションの場合、テーブル内の全ての項目を読み込み、そのサイズが読み込む項目のサイズとして計算される。(スキャンにより返される項目のサイズではない)なので、コストが高くつきやすい

書き込みの場合

書き込みタイプによってキャパシティユニットの消費量が異なる(最大1KBの項目を書き込む場合)。

書き込みタイプ消費キャパシティユニット
通常の書き込み
トランザクション書き込み

*書き込む項目のサイズは1KBに切り上げられて計算される(例:500バイトの書き込みは1KBとして計算)。

  • (例1)PutItemオペレーションの場合。テーブルに項目を書き込む時にすでに項目が存在する場合、このオペレーションによって項目が置き換えられるが、キャパシティユニットの消費量は、置き換え前と置き換え後の2つのうち大きい方となる。UpdateItemオペレーションでも同じで、前の項目サイズと後の項目サイズの大きい方のキャパシティユニットが生じされる。
  • (例2)DeleteItemオペレーションの場合。削除する項目のサイズがキャパシティユニットとして計算される。
  • (例3)BatchWriteItemオペレーション(1つ以上のテーブルに最大25個の項目を書き込む)の場合。バッチ内の各項目を個別のPutItemまたはDeleteItemリクエストとして処理する。消費キャパシティ料の計算は、各項目のサイズを1KBで切り上げてから合計サイズを計算する。例えば、500バイトと3.5KBの2つの項目を書き込んだ場合、5KB(1KB + 4KB)として計算される。4KB(500バイト + 3.5KB)ではない。

スループットモード

オンデマンドモード

  • 管理が簡単・自動的にスケールする
  • キャパシティの事前計画がやスケーリングポリシーの検討が不要
  • 読み書きした分だけ支払う従量制。

プロビジョンドモード

  • アプリケーションに必要な1秒あたりの読み込みと書き込みの回数を指定する
  • 実際の使用量ではなく、プロビジョニングした時間単位キャパシティに基づいて課金される(使っても使わなくでもプロビジョニングした分は課金される)

使い分け

オンデマンドモード

  • リクエストに応じて「使った分だけ」課金
  • リクエスト数が多いと、プロビジョンドより高くなりやすい

プロビジョンドモード

  • あらかじめ1秒あたりの読み込み/書き込みキャパシティを予約して、その分の料金を支払う
  • 使っても使わなくても同じ料金
  • 常に一定のリクエストがあるアプリの場合、オンデマンドモードよりも安くなる

その他

  • オンデマンドモードからプロビジョンドモードへは切り替え可能。ただし、条件あり。

エラーハンドリングとリトライ

プロビジョンドモードで、想定より多くの読み込み/書き込みが発生すると、キャパシティが不足して ProvisionedThroughputExceededException が返ってくることがある。いわゆるスロットリング(throttling)。

基本的な対処の考え方

  • AWS SDKには、スロットリングエラーが発生した場合に自動でリトライする仕組みが組み込まれている
  • リトライの間隔は指数バックオフ(exponential backoff)、つまり「失敗するたびに待ち時間を倍々に増やしながら再試行する」方式が採用されている。一定間隔で連打するより、輻輳を悪化させにくい
  • アプリ側で独自にリトライ処理を書く場合も、この指数バックオフ+ジッター(ランダムなゆらぎ)を入れるのがベストプラクティスとされている

根本対策

リトライはあくまで一時しのぎなので、頻発するようならキャパシティ設計を見直す必要がある
オンデマンドモードへの切り替え、プロビジョンドキャパシティの引き上げ、Auto Scalingの設定、パーティションキー設計の見直し(特定のキーにアクセスが集中していないか)などが根本対策になる

「リトライで何とかする」と「そもそもスロットリングしない設計にする」の両輪で考えるのがポイントらしい。

DynamoDB Accelerator(DAX)

スロットリング対策の1つとして、読み込みの負荷そのものを減らす方法もある。それがDAX。

DynamoDB Accelerator(DAX)は、インメモリキャッシュを用いて、結果整合性のある読み込みワークロードのアクセス応答時間を、1桁ミリ秒単位からマイクロ秒単位まで短縮するサービス。

特徴

  • 最小限のコード変更で導入できる
  • 読み込みの多いワークロード・急激に増大するワークロードにおいて、読み込みキャパシティユニットの消費を抑えられるためコスト削減につながる
  • データ保存時の暗号化(暗号化 at rest)をサポート
  • データ転送中の暗号化(暗号化 in transit)をサポート

最後に

AWS DVA対策として、DynamoDBの機能をまとめてみました。思っていたよりも長くなってしまいました。書いた後に読み返してみましたが、あんまり分かりやすくないなあ。と思いました。なので、AWS DVAの勉強をする際には、この記事で理解するというよりは、すっと頭に入ってこなかった内容=理解が浅いところを追加で調べるという使い方ができると思います!(無理やり)

この記事がどなたかのお役に立てれば幸いです。
もし間違いを見つけた場合は、コメントで教えていただけると助かります。

コメント