ちょっと話題の記事

AWS Summit 2014 Tokyo「Amazon DynamoDB テーブル設計と実践 Tips」レポート

2014.08.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、虎塚です。

先月のAWSサミットで行われたセッション「Amazon DynamoDB テーブル設計と実践 Tips」をタイムシフト聴講しましたので、またまたレポートします。

講師は、アマゾン データサービス ジャパンの安川健太さんです。

はじめに

Amazon DynamoDBとは

  • NoSQLの技術をサービスとして使う
    • 構築と運用はAWS
    • 開発者はコードを書くだけで使える
  • 安定した予測可能なパフォーマンス
  • シームレスなスケーラビリティの低コスト
Amazon DynamoDBの特徴
書き込んだデータは3ヶ所のAvailabilityZoneにコピーされて保存される(高い堅牢性と可用性)
ストレージに容量制限がない

ご利用のお客様

ゲームや広告系(gumiさま、リプレーションさま、Mynetさま)に加えて、エンタープライズでも(積水さま、日経さま)利用されている。

リプレーションさま: 人気ゲーム「騎士とドラゴン」

裏側はAWSで動いており、DynamoDBが使われている。少人数で運用できるようにAWSを採用。Elastic BeanstalkでWebフロントエンドを用意し、後ろはDynamoDBのみという構成。アクセスが跳ねた時も、運用担当の1人がコンソールでインスタンスを増やし、DynamoDBのスループットを増やすだけで対応できた。

Timersさま: カップル向け思い出共有SNS Pairy

思い出のある写真やチャットを失わないようにバックアップに気を遣っていた。当初はRedisをチャットデータの保存に使っていたが、高機能だがGB単位が高かった。台数が増えてきて大変だったこともあり、DynamoDBに移行した。ユーザIDとタイムスタンプでの検索が主な用途だったので、チャットの検索にマッチした。DynamoDBの高信頼性も、思い出の保存に向いていた。

Amazon DynamoDBの基礎知識

TableのKey, Indexとクエリ操作

次の基本要素を用途に応じて組み合わせて、テーブルの設計を行う。

Hash Key
これだけを使うとKey-Value Storeのように使えるスキーマになる。full scanのみ
Hash Key + Range Key
2つの要素をコンポジットキーとして持ったスキーマになる。Range Keyで範囲を絞れる。scan + 範囲指定query
Local Secondary Index
テーブル定義で指定。Range Key以外の属性を使って検索Queryを投げることができる
Global Secondary Index
テーブル定義で指定。Hash Key指定の制限を超えたQueryを投げることができる

Table操作の基礎知識

  • 1アイテム(1行)の更新はアトミックにできる
    • 複数アイテムの場合、それぞれの更新は独立して行われるので、RDBのトランザクションのようには行われないことに注意
  • 読み込み一貫性は2種類のモードがある
    • Strongly Consistent: ある時点で書いたデータが反映されたものが返ってくることが保証される
    • Eventually Consistent: ある時点で書いたデータを直後に読むと、以前のデータのコピーが返ってくるかもしれない
  • Filterを設定したScanやQueryが使える

並列アクセスの制御

Conditional Update
あるアイテムの特定の値が○○だったら更新する、そうでなければ失敗する、という条件つきの更新クエリが書ける
Set型を使う際に、値がSetに含まれているかを条件に設定できる
Atomic Counter
数値の加算減算をアトミックに行う
カウンタや、皆で更新する値を使いたい時に便利(ユースケース例は後述)
UpdateItemのADDを使う

テーブル設計&クエリ例

4つのユースケースごとに、テーブル設計とクエリの例を見ていく。

1. アプリのイベント履歴管理: Hash Key + Range Key

ユーザIDをHash Keyに、Range KeyにTime Stampを設定して、データを書き込んでいく
高いパフォーマンスを活かしてイベントログを書き込める
ある時刻のログを、特定のユーザに絞って読み出すことができる
DynamoDBに保管したデータは、Amazon EMRやRedshiftで読み出して使える
EMR: DynamoDBのテーブルをExternal TableとしてHiveで参照できる
Redshift: 直接コピーコマンドでデータを読み込んで、カラムナ型アーキテクチャを活かした演算ができる

このように履歴をどんどんDynamoDBに取り込むユースケースはありうる。RDBへの書き込みはスケールさせるのが難しいので、DynamoDBに移すだけで楽になるケースもある。

Time Based Partition Tables(安川さんが命名)
月ごとにログのテーブルを作って切り替えていくパターン
書き込みは最新のテーブルのみ、古いテーブルは書き込みがなく、たまに読み込まれる程度、というケースで利用
古いテーブルはスループットを抑えることができる
古いログはアーカイブしてRedsihtやS3に書き出し、drop tableで削除できる

ログの収集とアーカイブにも便利に使える。1つのテーブルにすべてのログを書いてしまうと、古いデータを消す時にTimestampを見ながら消し込んでいかなければならず、時間がかかる。このパターンを使うと一気にdrop tableで消せる。

2.ソーシャル画像共有アプリ: 複数テーブルによるデータモデル、LSI、GSI

タイムラインに写真が流れてきたり、皆で写真に友達をタグ付けしたりするようなサービスをイメージ。

テーブル設計: 複数のテーブルを使い分ける例
DynamoDBにもIndexはあるが、検索に使えるのはHash KeyとRange Keyであることを意識する
2つのテーブル(ユーザプロファイル情報=Users Table、友達リスト=Friends Table)を定義する

  • Users Table: UserIDをHash Keyにする
  • Friends Table: 友達の関係をHash key + Range Keyで登録
友達一覧を取得する例

  • Aliceの友達一覧を取得するには、Hash KeyにAliceを指定し、Range Keyには何も使わないクエリを投げる
  • 取得できた各UserIDをキーにして、UsersTableに問い合わせると、友達の詳細が取得できる
投稿画像の保存と検索
画像のメタデータを管理するImages Tableを用意

  • 投稿したユーザのUserIDをHash Keyに、画像IDをRange Keyにする
  • UserIDをキーにしてImages Tableに問い合わせれば、そのユーザが投稿した画像を取ってくることができる
  • しかし、時系列でソートしたいときはどうするか(Range Keyでソートされて返ってくるが、このケースでは意味のない値なので、アプリケーション側で並べ替えなければならない)
  • Local Secondary Indexを投稿日時(Date)につけることで、Queryのインデックスに使える。時系列に並んだ特定ユーザの投稿画像が取得できる

Local Secondary Indexは、テーブルごとに5つまで付けられる。内部的には、UserをHash Keyに持ち、Indexに指定した値をRange Keyにしたテーブルが作成されて、それを検索している。

Local Secondary Indexを使えば、検索のためだけに別のテーブルを追加しなくてよいのが利点。

画像にユーザのタグ付け
ImageTags Tableを用意

  • 画像IDをHash Keyに、ユーザをRange Keyにする
タグ付けをするクエリや、ある画像にタグ付けされているユーザを取得するクエリは簡単
あるユーザがタグ付けされている画像の一覧を取得するにはどうすればよいか
Global Secondary Indexを使うことで、指定したAttributeをHash KeyとRange Keyにしたテーブルを、DynamoDBに作らせることができる

テーブル設計の際には、基本の設計で満たせるかを見て、足りない機能はインデックスを組み合わせてクエリを最適化するとよい。

3. マルチプレーヤーバトル: Conditional UpdateとAtomic Counter

MMOで複数のプレイヤーでボスを倒すようなイメージ。

RDBで実装しようとすると大変: ボスのレコードをロックし、HPからダメージを引き、ボスのレコードを更新、ボスのレコードロックを解除...低遅延で実行するのは難しい。並列度が上がるほどスケールせずつらい。DBにも大きな負荷がかかる。

DynamoDBの場合
ボスのHP > 0を条件にしたConditional Writeが使える

  • 攻撃成功: ダメージ反映
  • 攻撃失敗: すでにボスは倒れているとみなす
通常の1アイテム更新と同じコストとパフォーマンスで、バリデーション付き更新ができる
戦士の攻撃
DynamoDBの各アイテムに、プレイヤーとボスの情報が記録されているものとする。ボス側にはLastHitBy(最終攻撃プレイヤー)の値がある。
  1. 戦士がドラゴンに80のダメージを与えた!: ボスのHPを更新し、LastHitByにプレイヤーIDを更新(Conditional Writeを使う)
  2. 魔法使いの杖からいかづちがほとばしる!: ボスのHPを更新し、LastHitByにプレイヤーIDを更新(Conditional Writeを使う)
  3. ほぼ同時に戦士も攻撃した!: ボスのHP > 0という条件がついているので、ConditionalCheckFailedExceptionが返る。LastHitByプレイヤーは魔法使いのまま保持される

Conditional Writeを使うことで、多人数で操作するテーブルの制御ができる。

デモ: Dynamo Quest
ダメージ一発一発がDynamoDBに格納される
ちなみに、S3のバケットで実現(HTMLとJavaScriptだけで記述)している
Cognitoを使うと、モバイルアプリから直接DynamoDBが使えるようになる

4. 投票システム: Write Sharding

複数の候補者に皆で投票するユースケース。最後に皆の投票数をカウントする。

Atomic Counterでカウンタを持ち、UpdateItemで投票数をカウントアップしていくことで、投票は実現される。

  • 少人数の利用なら、候補者のIDをHash Keyにして、得票数をADDしていけばよい
  • しかし大量の投票があるユースケースでの問題
    • DynamoDBのプロビジョンのスループットを上げることになる。DynamoDBがパーティションの数を増やす。
    • 各パーティションには、プロビジョンしたスループットをパーティションで割った数が割り当てられる
    • 特定のパーティションにリクエストが集中すると、本来のパフォーマンスを出せない
    • つまり、特定の候補者に投票が偏った場合、スループットが出ないこともある

この問題は、次のように解決する。

Write Sharding で Partition負荷を分散
書き込みが重く、どこかに偏りそうなケースにマッチ
候補者IDにランダムなサフィックス値をつけて書き込みリクエストを投げる。候補者IDの後ろに0〜9の適当な値を使う等
Hash Keyが1バイトでも異なれば、DynamoDBは別のパーティションを選べるので、書き込み先のパーティションを分散させられる
集計時は、すべての投票を集めて候補者ごとに足し合わせてから、表示すればよい

まとめ

  • ユーザの行動履歴
    • Hash + Range Key
    • Time Based Partition Table
  • ソーシャル画像共有アプリ
    • Index Table
    • LSI / GSI
  • マルチプレーヤーバトル
    • Conditional Update + Atomic Counter
  • 投票システム
    • Write Sharding

感想

DynamoDBを使いこなすためのKeyやIndexの使い方についてでした。ユースケースが具体的で分かりやすかったです。

検索で使われるのはHash KeyとRange Keyのみ(Indexは使われない)という特徴を覚えておくとともに、Hash Keyに指定しなかった属性を使ったテーブルをDynamoDBに内部的に作らせる、という技を使っていきたいと思います。

それでは、また。