DynamoDBについて今更調べたのでメモ

DynamoDBについて今更調べたのでメモ

日記帳です。

プッシュ配信のためのRegistrationIDをDynamoDBに貯めておけないかと思いいろいろ調べてみました(プッシュ配信はFirebase想定です)。
RDSと併用予定です。

DynamoDB

AWS上のマネージドKey-Value型のNoSQL。1アイテム(RDBで言うレコード)400KBまで。
無料枠はデータストアサイズ25GB、月2億リクエスト(読み込み/書き込み 25ユニットずつ)まで。無期限。

インデックスについて

DynamoDBのプライマリキーには以下の2種類
– パーティションキー(ハッシュキー)のみ
– パーティションキー+ソートキー(レンジキー)の複合キー
どちらも一意に決定できるように指定する必要がある。

DynamoDBは基本的にKey-Value型のNoSQLなため柔軟な検索は出来ない。
検索は全件取得か上記のプライマリキーでの取得が基本。その他、例えばstatusという属性があり
status: “published”な値を検索したい、という場合は事前に検索のためのインデックスを作成しておく必要がある。

その他にローカルセカンダリインデックス(LSI)とグローバルセカンダリインデックス(GSI)という2種類のインデックスがある。
参考にさせていただいたページ
http://dev.classmethod.jp/cloud/aws/conceptual-learning-about-dynamodb-lsi/
http://dev.classmethod.jp/cloud/aws/conceptual-learning-about-dynamodb-gsi/

LSIとGSIの比較

  テーブル(暗黙的インデックス) LSI GSI
定義できるテーブル 出来るとか出来ないとかじゃなくて必須 複合キーテーブルのみ 任意
テーブルごとに定義できる数 必ず1個 0〜5個 0〜5個
サイズ上限 なし 10GB なし
含むattribute 全て 各キー + 指定したもの(全ても可) 各キー + 指定したもの(全ても可)
作成タイミング テーブル作成時に作成後、変更不可 テーブル作成時に作成後、変更不可 任意のタイミングで追加削除可能
スループット テーブルから消費 独自にプロビジョニングされたスループットを消費

事前に作りたいキーがはっきりしていればLSIが作成できるが、パーティションキーとの組み合わせに限定される。
GSIなら後から作成可能で、パーティションキー以外での検索要件も満たすことが出来るためRDBの様な柔軟な検索も可能になる。

課金

DynamoDBはプロビジョニングされたスループットに対して課金される。
スループットは読み込み・書き込み用に予約されたキャパシティのみ使用できる。
– 書き込み
1ユニット=1KBあたりのアイテムについて、1秒あたり1回の書き込みが可能
– 読み込み
1ユニット=最大1KBのアイテムに対して、「強い整合性のある読み込み」を1秒あたり1回できる(「結果的に整合性のある読み込み」については1秒あたり2回)

ユニット数の計算

必要な容量のユニット数 = 1秒あたりの項目書き込み/書き込み数 * 項目のサイズ(KB切り上げ)
(書き込みは読み込みの5倍のユニット数が必要)

例1) 項目のサイズ:512Byte、1秒辺り100個の読み込み
ユニット数= 100 * 1(1KB以下の場合は1)

例2) 項目のサイズ:1.5KB、1秒辺り100個
ユニット数= 100 * 2(1.5切り上げ) = 200

無料枠について

下記に関しては1年の無料期間を過ぎても無料。
– データストレージは25GBまで無料
– 書き込みキャパシティユニットは25まで無料
– 読み込みキャパシティユニットは25まで無料
– DynamoDBストリームからの250万回の読み込みリクエストまで無料

無料枠だけでもそこそこいけるが使い方によってはキャパシティが不足することがあるかも。

参考にさせていただいたページ
http://qiita.com/YU81/items/e1e336990ed8cfb938d9
http://migrantbird.hatenablog.com/entry/2014/04/18/013535

注意点

プロビジョニングされたユニット数を超えた場合エラーになる。
書き込みであれば失敗するため、書き込み失敗を許容しない場合はキューやテーブル設計などで落ち着いた時間にやり直す必要がある。
http://qiita.com/inouet/items/bcf9467a65b27c362ecf

検証

読み込み

単一アイテム取得 => GetItem
複数アイテム取得 => scanqueryBatchGetItemのどれか
BatchGetItemは1回につき最大16MBまたは100アイテムの取得。
scanqueryはインデックスを用いた取得、ユニット数は(取得アイテム容量 * 取得数)を4KB単位で切り上げた値になる。そのため取得するattributeのサイズが小さければ多くのアイテムを読み込める。ただし1度のリクエストに付き1MBまでの制限がある。
registration_idだけならば1リクエストに付き約5800アイテム読み込むことが出来た。

約2万件のregistration_idをsleep等入れずに直列でscanした結果では全件取得は1〜2秒程度、無料範囲内でもキャパシティには十分余裕が合った。何度も続けるとキャパシティをオーバーし遅延が発生する。ただし数秒以内にまた読み込み出来るようになる。

テーブルサイズが小さいうちは、多少効率が悪くともいざとなったらscanしてフィルタすれば何とかなる。

scanとqueryの違い

Scanでは常にテーブル全体がスキャンされるが、Queryではキー条件のセットを持たずに特定の範囲のキーだけが検索される。 => Scanは負荷が大きい
Queryではプライマリキーの属性値だけが検索されるのに対してScanではテーブル内のすべての項目を検索する事が可能。

事前に設計できていれば基本的にはQueryのみを使う方が良い

並列スキャンの速度

マルチプロセスとマルチスレッドでベンチマークを取った結果。アイテム数3万。
(ちなみにデータ投入はDatapipelineによるインポートではなく普通にプログラムで入れた。25ユニット/s超えないようbatchWriteItemの度にsleep 1したら20分位かかった。)
以下のようなコードで検証。grosser/parallelを使用。

マルチプロセスの場合、ワーカー数が増えると非常に遅くなる。
ワーカー数を増やすとマルチスレッドの方が早くなる傾向がある。ただし値はかなり幅がありまちまち。
直列スキャンは毎回のFCMへのリクエストを考えると待ち時間は多くなるが、スループットは安定する。

scanによるキャパシティ消費量

一度のscanで消費されるキャパシティは1MB上限まで読み込んだ場合2強。並列スキャンの場合は並行数によって変わるが1スレッド辺り最大2強 * 並列数になる。ローカルやdevelop環境の場合はコストを抑えるため並列数を1にしてキャパシティを下げておく。cloudwatchメトリクス上では読み込みキャパシティー消費がプロビジョニング済みを超えていたとしても秒間のユニット数は超えていない場合も多いので、スロットルが発生しているかどうかに注目したほうが良い。

書き込み

BatchWriteItem
1回につき最大25アイテム。無料枠が25ユニットなので無料枠で収めるには秒間25アイテムしか書き込めない。それ以上はキューを使って書き込む。
=> registration_idの登録ならば多少の遅延は許容範囲

DynamoDBでのバッチ処理

DynamoDBでは一度に操作できるデータ量が制限される。
BatchGetItemで一度に取得できるのは最大100項目、BatchWriteItemは最大25項目PutItemまたはDeleteItem可能。
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Limits.html
http://qiita.com/yoskhdia/items/6897f66bdf93017ca033

プッシュ配信処理を考える

user_id registration_id name os_type os_version app_version timestamp
1 elCc9 …:APAyTOa … iPhone6 ios 10.1 1.0.0 1489482503

プライマリキー
パーティションキー:user_id
ソートキー: registration_id

こんな感じのテーブルがあって、プッシュ通知毎にDynamoDBからregistration_idを取り出す。

全件プッシュ
並列スキャンが使用可能。

指定ユーザプッシュ
指定ユーザに対するプッシュは特定のuser_idで絞る必要がある。特定の複数のユーザのregistration_id一覧を取得するには以下の方法がある。

  • プッシュ配信毎に通知するreg_idをDynamoDBに登録しqueryで取得
    上記のテーブルとは別に以下の様なテーブル(アイテム)を作成。
user_id registration_id notification_id
1 elCc9 …:APAyTOa … 1

BatchWriteItemかインポートで登録するため無料枠なら1秒25アイテムまでしか登録できず時間がかかる。即時配信が厳しい。事前登録する余裕があれば効率が良い。取得時はqueryが使用可能で早い。
scanした結果をアプリケーション側でフィルタする
queryに比べ速度やキャパシティ等の面で効率が悪いが無料枠内で即時配信を実現する場合は他に選択肢がない。

以下は間違った考え方。
BatchGetItemを使い、アプリケーション側で並列(rubyならThread::Queueもしくはgrosser/parallel使って)取得する
user_idをハッシュキーとして並列呼び出しならば比較的高速に取得できる。(BatchGetItemは1回100アイテムまで)
http://qiita.com/necojackarc/items/f8faf3a7974edc638769~~

BatchGetItemはプライマリキーが必要。つまりuser_idregistration_idの複合キーテーブルなら両方を知っておく必要がある。

結論

規模にもよるけど無料枠内で使うにはかなり制約がありそう。
今回のようなプッシュ配信では一斉送信時に全て読みだすためある程度キャパシティを確保しておく必要があり、遅延を許さないのであればコスト的には高く付きそう。
特に大量データの一括書き込みが難しそう。
インポート等の機能はあるが、キャパシティに左右されるためMySQLでバルクインサートするような感覚では使いづらい。
DynamoDBは高機能であっても基本的にはkey-valueなので検索が必要になる用途ではよく考えたほうが良さそう。

と若干ネガティブな結論になりましたが、ある程度お金を払えばこのへんは一気に解消されるのでそこまで問題にはならなそうです。
書き込み時はともかく、読み込み時はキャッシュを上手く使えればスループットを抑えることが出来るはずです。
システムによってはRDSを使わずにDynamoDBだけで完結することも出来ると思うので上手くすればコストを抑えながら、将来的なスケールアウトまで視野にいれることもできそうです。
ようは使いようですね。身も蓋もないですが。

TAG

  • このエントリーをはてなブックマークに追加
kurashita
エンジニア kurashita kurashita

基本的にRuby on Railsで開発してます。最近はvue.jsも。好きな塔は円城です。