技術メモ

技術メモ

ラフなメモ

DynamoDBを使ったデータ設計の反省点2020

2020年に取り組んだ案件の一つですが、サーバレスで検索アプリを開発していました。インフラはAWSを採用していたため、ほぼ必然的な運びでデータストアはDynamoDBを採用しています。業務アプリケーションではデータ設計が重要で、データ設計の良し悪しでアプリケーションの実装がシンプルor複雑になります。本記事はDynamoDBのデータ設計の反省点とこうすればよかったな、という振り返り記事です。

アプリケーション概要

映画館のシートような縦×横の空間に対して、人を割り当てるためのアプリケーションです(設定はダミーですが本質的には似ている構造です)。

制約

  • 席の空間は予め決まっている
  • 席は前(最前列)から詰めて座っていく
  • 各席には座れる人の区分がきまっている(ジュニア/成人/シニア)
  • 各席は事前に予約することもできるし、映画館の現地で予約なしに直接席を決めることもできる
  • 事前に予約した場合も、最終的には現地で席を確定させる
  • 30分前までには映画館内で着席していないといけない。そうでない場合は確定した席は取り消しされて、予約可能な席になる。別の方が確保することが可能

席の状態遷移図

http://www.plantuml.com/plantuml/png/SoWkIImgAStDuOhMYbNGrRLJUBvo5nSGWzbFTdKytzAYO0LbF6wS_hXnFXU40r6yQDVJTRCKhA0CY0AuTeHi_xwdSpOyRbp-VFQMPtrBKHH3E13rSnkUxbYhO0LbO2e06Oomg-Tnq_R7JJiVDxSzRbxmk77ruwQEnusB7pSkUze_xTas87lguwOUa0bS4FF0HW2zoUMGcfS2z380

反省点

反省点1:状態を区別するためにアイテムを分離させたこと

事前「予約」と、「確定」/「着席」の状態を区別するために2つのアイテムに分けたこと。これが一番の反省点です。なぜこのアイテム設計にしたか、というと、実は業務アプリを開発する前に、同じチームの別のメンバがPoCを実施していました。私が参画したときにはPoCである程度良好なフィードバックが得られていました。そのときに採用していたデータモデル上、事前「予約」と「確定」/「着席」の状態を別に管理していたため、この分離するモデルを採用しました。

分離したときのデータモデルのキーは以下のようなものです。

  • ハッシュキー(hkey)
    • 日付をもつ
  • ソートキー(skey)
    • 席の座標や状態を持つ

例えば日付が 20210101 で座標が縦=A、横=1の席で「空」の場合は以下の2つのアイテムを持ちます。ソートキーは複数の条件で絞り込みできるように複合キーになっています。

hkey skey ... (その他の属性)
20210101 YOYAKU#UNUSE#A#1 ...
20210101 KAKUTEI#UNUSE#A#1 ...
  • 日付が 20210101 で座標が縦=B、横=2の席で「確定」の場合
hkey skey ... (その他の属性)
20210101 YOYAKU#USE#B#2 ...
20210101 KAKUTEI#USE#B#2 ...

まずRDBやDynamoDBといったKey-Valueストアに関わらず、 基本的な原則として状態をもつアイテム(レコード)は1つにするべき です。状態が2つ以上のアイテムに分かれていると管理が大変になって、アプリケーションが複雑になります。PoCではfeatureの機能を開発、動作させてフィードバックをもらうことが主眼ですが、プロダクションに採用するアプリ開発となると別です。保守運用しやすい、追加の開発コストが低いといった、単に機能だけでなく、別の重要な観点があります。データモデルはフラットに検討して、PoCの結果に引きづられるべきではありませんでした。

弊害

この分離モデルを採用したことの一番の弊害は、状態の把握が複雑になることです。データストアで把握すべき状態が事実上2倍になっています。特にある席を予約していたが、確定時は別の席を選択した場合は、もとの「予約」の席を未使用にする。確定として選択した席を「確定」にするだけでなく、別の人からの予約の対象にならないように、「確定」した席に対して「予約」の割当を行う必要があります。本来は不要な複雑性を持ち込んでいて、アプリケーションが必要以上に複雑になりました。保守性が悪く、これは大きな反省点です。

こうすべき

状態は1つの属性(キー)にまとめるべきです。今回の場合は、状態を「空、予約、確定、着席」という状態とすれば、必要十分でした。

hkey skey ... (その他の属性)
20210101 CYAKUSEKI#A#1 ...
20210101 KAKUTEI#A#2 ...
20210101 KAKUTEI#B#1 ...
20210101 YOYAKU#B#2 ...
20210101 EMPTY#C#1 ...

ここがつらい

つらみ1:ソートキーの複合キー

DynamoDBはRDBとはデータモデルが異なります。柔軟な検索クエリを投げることができないため、RDB以上にデータモデルが重要になります。RDBであれば、検索条件の追加、別テーブルのJOINなどSQLを用いて柔軟に検索できますが、DynamoDBではクエリはソートキーに対してのみ実施できます。その他の属性はデータの絞り込みには使えません。フィルターはデータを取得したあとに、結果を落とします。

ただし、座標をキー(ハッシュキーorソートキー)に含めないとアイテムが一意にならないため、座標をキーに含める設計は必須です。かつ状態(空、予約、確定、着席)によるクエリも必須ですので、状態もキーの一部に含める必要があります。

結果として以下のようなソートキーとなりました。DynamoDBで複雑なクエリを処理しようとすると、どうしてもソートキーが複雑になってしまい、アプリケーションのコードが複雑になるのがネックですが仕方がありません。

つらみ2:トランザクション管理

DynamoDBを使う上での制約ですが、トランザクションが最大25アイテムまで、という制約がつらいです。アプリケーションとしては数百件のレコードをトランザクショナルに処理したいのですが、DynamoDBでは実現できません。結局現在のところ、アプリケーション側でバッチインサートを行って、バッチインサート中にエラーが発生した場合はロールバックする、という処理をアプリケーションのコードで実装しています。ただしロールバック中にエラーが発生する可能性があり、この場合は運用でカバーする必要があるなど運用が複雑になってしまいました。

つらみ3:ソートキーの更新

他のつらみに比べるとだいぶ細かいで小さなことですが、ソートキーは更新できないので、アイテムを一度削除して、更新後のアイテムを追加する、という操作がDynamoDBでは必要です。まぁこの程度であれば、アプリケーション側でラッパーでも挟めばいいので、そこまでつらみではないです。

つらみへの対処

結局のところ、複雑なクエリやトランザクションをDynamoDBだけで処理しようとすると、どうしても複雑性がアプリケーションに漏れ出してしまう、と感じています。冒頭ではサーバレス環境ではほぼ必然的にデータストアはDynamoDB一択と言いましたが、2021年1月現在では、RDS Proxy+Amazon Aurora( Serverless)などの選択肢も十分考えられると思います。複雑性をインフラ側にも押し込むかアプリで吸収するのか、インフラ側に一部を寄せたときの可用性、運用保守性はどうか?など考慮するポイントは多いですが、設計しがいがあるポイントです。

まとめ

  • 状態を管理するレコードはまとめよう
  • 複雑なクエリや大量データのトランザクションが必要な場合はDynamoDBだけでなく、RDS(RDB)を検討しよう