DynamoDB 特有の性能劣化（ホットパーティション）を解消した話

## はじめに
昨今様々な NoSQL 系のミドルウェアが使われていることと思います。
ご多分に漏れず私が参画中のプロジェクトでも AWS の NoSQL サービスである DynamoDB を使った機能を今年実装しました。
DynamoDB は RDB とは異なる仕組みのミドルウェアなので RDB では起こらない特有の問題が起こることがあります。
本記事では私が DynamoDB を使っているときに起こったパフォーマンス劣化とその解消法をご紹介します。

## 今回 DynamoDB を使おうと思った背景
私の現場の web サービスから呼ばれている API は従来の構成ではデータソースとして AWS RDS(MySQL) を使っていました。
しかし、サービスの拡大とともに日々のデータ更新・削除件数が増えていき徐々に

- SELECT にかかる時間が長くなる
- ストレージ容量が増え続けることによる課金額増大
- これらに対応するための RDS インスタンススケールアップによるコスト増大

等の問題が起こっていました。
データ更新・削除が激しいテーブルに対して `OPTIMIZE TABLE` を実行することでこれらの問題が解決するということまでは分かっていましたが、`OPTIMIZE TABLE` を実行すると実行中はテーブルにロックがかかってしまいます。
瞬断程度の短時間であればサービスを稼働させた状態で実行することも可能かもしれませんが、我々のサービスでは長時間テーブルがロックされてしまうためサービス稼働状態での `OPTIMIZE TABLE` 実行は不可能でした。

API が直接 MySQL を参照しているために `OPTIMIZE TABLE` の実行ができないのであれば API と MySQL の間にもう一つデータストアを挟んで MySQL のデータを同期し、API からは新しく追加したデータストアを参照するようにすればサービスを稼働させたままで `OPTIMIZE TABLE` が実行できる、ということで DynamoDB に白羽の矢が立ちました。

![Before-After.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151273/9f8f4147-d26e-b8c2-13bd-7ae58a260160.png)

## どんな問題が起こったか
無事に

- MySQL と DynamoDB を同期するプログラム開発
- API の向き先を DynamoDB に変更

の作業が終わり万事順調のように見えていましたが、負荷試験中に問題は発生しました。

負荷試験を開始して最初の 10 分程度は特に問題なく API が動いていたのですが、一定時間が経過すると急に API のレスポンスが極端に遅くなりほとんどがタイムアウトするようになってしまいました。
MySQL を使っていたときには起こっていなかった現象なので DynamoDB 周りが原因になっている可能性が非常に高いと考えられました。

結論から言うと「ホットパーティション」と呼ばれる状態になってしまい DynamoDB でスロットリング（速度制限）が発生していました。

DynamoDB を使ったことがある方はご存知かと思いますが DynamoDB はテーブルを作成する際に RDB のように予め全てのカラムを定義する必要はなく、パーティションキー（RDB で言うところのプライマリキー）と必要に応じてソートキー（RDB で言うところの複合プライマリキー）のみを設定します。
データを取得する際ははパーティションキーとソートキーのみを条件として検索することが好ましいです。

データの更新や参照が特定のパーティションキーに集中してしまう現象が「ホットパーティション」で、**ホットパーティションによって速度制限が発生するとそのパーティションのデータだけでなく同じテーブルに乗っている全てのデータ取得が遅く**なります。
![ホットパーティション.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151273/502d4d96-3381-d929-3dc9-f521cf44e2ae.png)

## 対処
ホットパーティションによるスロットリングにはいくつか対策方法があります。

### 考えられる対策
- パーティションキーの設計を見直して特定のパーティションに読み書きが偏らないようにする
- キャッシュを設けて最初の一回を DynamoDB から読み取り、以降一定期間はキャッシュからデータを返す
- あるパーティションキーに対するデータの更新処理を複数回分まとめてバッチ処理にする（書き込みでホットパーティションが発生している場合）
- パーティションキーにサフィックスをつけて同じデータを異なるパーティションキーで複数書き込み、データ取得時はサフィックス部分をランダムにしてアクセスを分散させる

などが上げられます。
いずれの方法も一長一短あるのでサービスの要件に合わせてどの方法を採用するか決めてください。

### 実際に採用した対策
私のプロジェクトでは

- パーティションキーは API のリクエストに応じて決定しているので設計の見直しは不可
- 書き込みでホットパーティションが発生しているわけではないので処理のバッチ化も不可
- パーティションキーにサフィックスをつける方法はデータの管理が煩雑になる、データ容量が増える、アクセス量が増えると結局ホットパーティションが発生する可能性があるので不可

などの理由からキャッシュを設ける方針になりました。

キャッシュというと Redis や Memcached が思い浮かぶと思いますが DynamoDB では DynamoDB Accelerator (DAX) という専用のキャッシュサービスがあるのでこちらを導入しました。
使い方は非常に簡単で AWS のコンソールや Terraform などで DAX のクラスタを作成し、アプリケーションコード側では AWS-SDK の DynamoDbClient を作成している箇所を DaxClient を作成するように書き換えるだけです。
DynamoDbClient に定義されているメソッドは DaxClient にも定義されているはずなので処理自体を書き換える必要はありません。（少なくとも私のプロジェクトではありませんでした）
アプリケーションの最終的な構成は以下の通りです。

![最終構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151273/992a567d-da57-0c49-e185-bbcd904985ad.png)

この構成で負荷試験を実施したところ時間が経過しても性能劣化することなく無事に DynamoDB 参照版の API をリリースでき、当初の目的だった RDS の `OPTIMIZE TABLE` も実行できるようになりました！🎉🎉🎉

## まとめ
- DynamoDB を使ったアプリケーションである程度時間が経過してから性能が劣化する場合は「ホットパーティション」の可能性がある
- キャッシュを設けて DynamoDB へ連続でアクセスしないようにするこでホットパーティションは解消可能
- 他にも対策方法はあるのでキャッシュを使えない場合でもやりようはある

全ての性能劣化の原因がホットパーティションではないと思いますが、ホットパーティションが原因の場合は参考になれば幸いです。

## 参考
- [Amazon DynamoDB テーブルがスロットルされるのはなぜですか?](https://repost.aws/ja/knowledge-center/dynamodb-table-throttled)
- [読み込みオペレーションと書き込みオペレーションをパーティションキーに均等に分散する](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/TroubleshootingThrottling.html#TroubleshootingThrottling.DistributeEvenly)
- [DynamoDB Accelerator (DAX) とインメモリアクセラレーション](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DAX.html)
    - [既存のアプリケーションを DAX を使用するように変更する]()