DynamoDBでのポイントまとめ

2018/05/24追記:
AWS公式ドキュメントに [Best Practices for DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html) が公開されているので、コチラをチェックしましょう。


# 0.前提

このドキュメントは、先に下記の参考資料を読み、後で振り返りやすいようポイントをまとめることを目的としています。そのため、用語などの解説は行いません。

## 参考資料

Must
[AWS Black Belt Techシリーズ Amazon DynamoDB - Slideshare](http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-tech-amazon-dynamodb)
[Amazon DynamoDB（初心者向け 超速マスター編）JAWSUG大阪 - Slideshare](http://www.slideshare.net/shimy_net/amazon-dynamodb-23315068)
[DynamoDBによるソーシャルゲーム実装 How To - Slideshare](http://www.slideshare.net/itoyusaku/jawsug-201303)

---

Optional
[DynamoDB ベストプラクティス - Qiita](http://qiita.com/inouet/items/bcf9467a65b27c362ecf)
[AWS Summit 2014 Tokyo「Amazon DynamoDB テーブル設計と実践 Tips」レポート](http://dev.classmethod.jp/cloud/aws/aws-summit2014tokyo-dynamo/)
[DynamoDB のベストプラクティス - aws documentation](http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/BestPractices.html)


# 1.スキーマ設計

## 向いているデータ特性

以下の *全て* に該当するデータ

* トランザクション処理が不要で結果整合性が担保されていれば良い
* テーブルのHash Keyに偏りが少なく均一性が良いものを設定できる  
（例えば、ユーザID、ユーザIDとトランザクションIDのペア等）

小規模データであっても、上記の特性があれば、RDBをDataStoreにするよりも管理しやすかったり、コストが安くなるケースもあるため、規模の大小は判断基準の第一にすべきではありません。
ただ、一般にRDB製品は、特に書き込み性能でボトルネックになりやすいため、スキーマ設計をやり直してでも、NoSQLを使う価値があります。そのため、大規模サービスでの利用が多いです。

その他、複数レコードを大量に頻繁に操作しないことも重要です。複数のレコードに対して操作するには、いくつか制限があるため、集計にはMapReduceを利用するなど工夫が必要です。また、更新処理はバッチオペレーションが提供されているものの、一度に操作できる数に制限（[DynamoDB での制限 - aws documentation](http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Limits.html)）があったり、実際には１オペレーションずつ実行されるようなので、アプリケーション側でキュー処理するなどで制御する必要があります。

## 型

[DynamoDB データモデル - aws documentation](http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DataModel.html)

| 型            | 用途 |
|---------------|--------|
| String        | UTF-8文字列 |
| Number        | 整数または少数。最大 38 桁の精度で、10^-128 から 10^+126 の間 |
| Binary        | 暗号化されたデータ等。圧縮して保存したい場合にも使う |
| Set of String | StringのSet |
| Set of Number | NumberのSet |
| Set of Binary | BinaryのSet |
| List          | json list document |
| Map           | json map document  |

* NULLは含められない
* 長さ0の文字列も含められない
* Booleanも含められない（Numberの0/1で代替）


## キー設計

### Point

* Hash key または Hash key & Range key によって一意になるようにする
* Hash keyによってパーティション間でのデータ分散がされる
* Hash keyには順序が無い
* Range keyには順序が考慮される（※パーティション内の順序）
* プロビジョンしたスループットは各パーティションに均等付与されるため、アクセスするキーに偏りがあるとスループットを超えてパフォーマンスが出ない  
**Hash keyの設定が最重要**

### Index

インデックスのあるAttributeしかQueryの条件に指定することができません。そのため、Queryの条件に使いたいAttributeに別途インデックスを貼ることができます。

#### Local Secondary Indexes

|特徴|
|----|
|あくまでHash keyに対するセカンダリなので、Hash keyが異なるアイテムを横断的にQueryすることはできない|
|元のテーブルのRange keyをAttributeに持つインデックステーブルが作られるだけなので、このテーブルのためのスループット＆保存費用が別途かかる|
|１テーブルにつき５つまで|

#### Global Secondary Indexes

|特徴|
|----|
|Hash keyをまたいでAttributeに貼れる|
|元のテーブルのHash keyをAttributeに持つインデックステーブルが作られる（このテーブルのためのスループット＆保存費用が別途かかる）|
|スループットは個別に指定する|

#### Attributeのプロジェクション

インデックステーブルにAttributeも保持するようにすること。
Attributeのプロジェクションをすることで、Write/ストレージのコストはかかるが、Read時にインデックステーブルだけで事が済むなら、元のテーブルを読みに行くことはなくレスポンスを返すことができます。
LSI/GSIでの違いとしては、GSIでは、インデックステーブルにないデータは返すことができない点があります。

#### 要所

* 極力、LSI/GSIを使わなくて済むようにスキーマ設計を行うべき
* どうしても必要な場合は、用途と必要なデータ（Attribute）を勘案してインデックスを作成する
* あまりに必要な場面が多い場合はRDBの利用を検討する

## テーブル設計

### Point

* １Itemのサイズに制限があるため、格納データのサイズ計算が重要（スループットの設定などにも影響する）
* キー設計を優先して、Attributeを分割して意味のまとまりではなくテーブルサイズを小さくすることを目指したり、非正規化などRDBの常識に囚われないことも必要
* NULLや空文字があり得る場合は、テーブルを分割する

[項目の操作のガイドライン - aws documentation](http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GuidelinesForItems.html)

* 一度に操作できるサイズに制限があるため、テーブルに大規模な設定タイプ属性（Set of XXX）がある場合は、テーブルを分割することを検討した方が良い
* 常に全てのAttributeにアクセスするなどが無いのであれば、テーブルのサイズは小さく分割していった方がワークロードが均一化される
* Attributeに格納するデータが大きい場合は、圧縮やS3への格納を検討する


### Pattern

#### Time Based Partition Tables

[時系列データへのアクセスパターンを理解する - aws documentation](http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GuidelinesForTables.html#GuidelinesForTables.TimeSeriesDataAccessPatterns)

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

容量無制限といっても、運用管理を考えて、区切りを設けることができるデータについては分割も検討した方がベター


# 2.アーキテクチャでの考慮点

## DynamoDBの得意・不得意

### 得意

* 大規模データの保存  
ただし、適切なHash keyが必要
* Keyによるシングルアクセス
* 可用性、対障害性能

### 不得意

* 柔軟な検索
* 集計処理  
自前集計なりMapReduceなりでしか集計できない。
* 長大なレコードの保存  
400KBまでの制限があるためレコード自体が大きなデータは圧縮やS3などを利用する必要がある。
* 複数データをまとめて読取、更新、削除  
一度に操作できるサイズに制限があるため、RDBのような感覚で操作すると危険。  
また、Attributeの操作はできず、ItemのPutかDeleteとなる。


### Point

* Write費用より保存費用の方が安いので、レコードを消さずにおくのも一つの手  
行動履歴のようなものであれば、ユーザがどのような操作をしたかなどを全て追跡できる。障害が発生したなどでデータの整合性が崩れてもリカバリできる可能性が上がる。
* Write（特に複数レコードへのWrite）は失敗することがある点を前提におく  
下記「レコード操作での注意」を参照
* キャッシュとしては専用のサービスの方に分があるので、パフォーマンスが求められるなら素直にElastiCacheなどを使う

## レコード操作での注意

全てのレコード操作は失敗する可能性があります。また、トランザクションは基本的に無いものと思った方が良いです。
基本的に1レコードに対するWriteであれば、成功/失敗の2つしかないので、クライアントに失敗を通知して再実行を促すなども手としてあります。一方、複数のレコードをまたがった操作では、一つ一つの操作はアトミックに行われますが、トランザクションがあるわけではないので、片手落ち更新になる可能性が高くなります。完遂保証をするかどうかで、キュー処理を利用するなどを検討する必要があります。

### キュー処理

ポイントは2つ

1. 操作したい内容自体をデータとして登録  
併せて未処理の操作内容があることを対象のレコードに記録しても良い
2. キューコントローラにて1つ1つ処理  
※下記「再実行耐性」「並列実行耐性」を考慮したロジックであること

#### 再実行耐性

> 同じ処理が2回実行されても結果に影響がでないようにする。

**解決策**

* 入力内容から処理内容が全て決定されるようにする
* レコードの更新をする際に、確かに更新されたことが判別できるように「証拠」（更新日時やステータスなど）を併せて記録する
* 処理済みであればスキップして次の処理へ進むようにする
* 複雑な分岐をせず、シンプルなフローで処理されるようにする

#### 並列実行耐性

> 同じ処理が2つ以上のプロセスで並行して実行されても結果に影響がでないようにする。

**解決策**

* 条件付きアップデート（Conditional Write）を使って楽観的ロックを実装する
* 更新する前にレコードを「一貫性あり（結果整合性保証）」で読み込む
* レコードを更新するときは、「読み込んだ時点から他の誰にも更新されていなければ」を条件にする  
単方向のAttributeなら読取り時点の値、そうでないならVersion番号など
* 更新に失敗した場合は処理を最初からやり直す
*再実行耐性が実現されていることが前提*


## スループット設定の目安

### パーティション数計算

パーティション数はテーブルサイズとスループットから求められる。このパーティション数に対して、キャパシティが均等に振られる。

[パーティションの動作について - aws documentation](http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GuidelinesForTables.html#GuidelinesForTables.Partitions)

> numPartitionsTableSize = tableSizeInBytes / 10GB
> numPartitionsThroughput = (readCapacityUnits / 3000) + (writeCapacityUnits / 1000)
> numPartitionsTotal = MAX(numPartitionsTableSize | numPartitionsThroughput)
> readCapacityUnitsPerPartition = readCapacityUnits / numPartitionsTotal

### 注意

* キャパシティを減らすとき、パーティションの数は変わらない  
→パーティションに割り当てられるキャパシティが減るため、キャパシティエラーを起こしやすい状態になりかねない。