misc.tech.notes

主に技術的な雑記的な

DynamoDBデータモデリング虎の巻:第壱巻 〜前提知識編〜

動機など

最近、Serverlessの文脈からDynamoDBのテーブル設計の相談を受けることが多くなってきていて、Podcastでも話したけどけっこう図とかが無いと説明しづらい領域なので、まとまった資料がほしいなということでまとめてみる。

cloudinfra.audio

どう考えても長編大作エントリ不可避なので気力が続けば第二巻以降に続きます…!(フィードバックが多いと頑張れるかも…!)

本巻の対象と前提知識

本巻はDynamoDBのデータモデリングにスコープを絞っています。DynamoDBおよびデータベースの一般用語などについての説明は省きます。 前提知識としては以下のようなものになるかと思います。

  • DynamoDBのサービスとしての概要や用語( WCU , RCU , GSI , LSI など)を知っている
  • Hash TableやB-Tree(B+Tree)といったデータ構造がどんなものか分かる
  • RDBで第3以上の正規化されたデータモデリングができる
  • ACIDがそれぞれ何か分かっている

DynamoDBの概要と少し踏み込んだユースケースなどを知るにはこちらのAWS Black Belt Online Seminarの資料がオススメです。

www.slideshare.net

正規化やACIDについてはおググりください。

本巻を読む必要がない方

最近(2018年の5月頃だったかな?)DynamoDBのデータモデリングをする上でとても有益なドキュメントが公式に公開されています。これを全て(かなりのボリュームがありますが)何の疑問もなく読めて理解できてしまう方はきっとこの虎の巻を読む必要はありません。本巻でもかなり引用させてもらう予定です。

docs.aws.amazon.com

DynamoDBはどんなデータベースか

実際にDynamoDBのデータモデリングの話に入る前に、DynamoDBが サービスではなくデータベースとして どのようなものであるかを知っておくと色々な場面で理解の助けになります。

DynamoDBはNoSQLの中で強いて分類するなら分散KVSという種類のデータベースになります。Keyによってデータが分散配置されるデータベースです。KVSと一口に言っても本当にKeyによる完全一致で1つずつしかデータを更新・取得できないものもあれば、内部的にインデックスを構築することで範囲検索や前方一致検索、全文検索などができるものもあります。

DynamoDBにおけるデータの物理構成

DynamoDBの物理的なデータ配置に関する有名なモデルとしてはクォーラムなどの仕組みを利用した分散同期と合意アルゴリズムが挙げられますが、これは内部的な話であり読み込みクォーラムを増やすことで強い整合性のある読み込みができる場合とできない場合があることを抑えておけば良いです。より深く知りたい方は論文を読んでみることをオススメします。

Amazon's Dynamo - All Things Distributed

それよりもデータモデリングにおいて大事なのが、データの物理的な分散配置を決める Partition Key による分散方式です。この Partition Keyハッシュ関数にかけた結果によってデータの配置が決まります。逆に言うと、この Partition Key がなければデータがどのPartition(イメージとしてはDynamoDBのクラスタを構成するデータノード)にあるかと特定することができません。そのため、DynamoDBにおいて Partition Key はテーブルに必須のものであり、フルスキャンを除いた検索クエリにおいては必ず Partition Key を指定する必要があるのです。

f:id:FumblePerson:20180731185233p:plain

docs.aws.amazon.com

そして、DynamoDBテーブルの定義には前述の Partition Key と共に Sort Key というキーとの組み合わせをプライマリキー(主キー)とすることができます。この Sort KeyPartition Key によって配置されたPartitionの中にB+Treeインデックス *1 として構築されます。DynamoDBで Sort Key だけのインデックス探索ができないのは、Partition毎にインデックスが構成されているためなのです。

B+木 - Wikipedia

では、何故わざわざSort Keyだけで検索できた方が便利であるはずなのにPartition毎に構成する必要があったのでしょうか?それは(おそらく)性能を優先した結果です。 Sort Key を共有のインデックス空間に配置してしまうとあらゆるデータのインデックスがそこに構築されてしまい、RDBでは扱いにくい膨大なデータ量やハイトラフィックを扱うためのデータベースとしては致命的なボトルネックになりえます。また、 Partition Key で配置が決まるということは、Partitionの偏りを許容して Partition Key を全て同じ値にしてしまえば一つのインデックスとして構築できるわけで、分散を基本とすることは合理的であると言えます。

DynamoDBにおけるインデックス「GSI」と「LSI

DynamoDBをよく分からずにテーブル設計をすると、KVSというイメージからか一意となるメインのキーだけに焦点を当てがちになる印象があります。しかし、DynamoDBを真に使いこなすためにはこの GSI (Global Secondary Index) と LSI (Local Secondary Index)特に GSI の使い方こそが肝であると言えます。

LSI

LSI は、プライマリキーと Partition Key が同じで Sort Key を別の属性としたキーです。Partition Key が同じためプライマリキーと同じ物理配置となり、 WCURCUGSI のように独立せずテーブル自体のキャパシティユニットを消費します。また、物理配置が同じであるため LSI の更新はデータの更新に対して同期的に行われ、LSI に対するクエリは 強い整合性 を選ぶことができます *2。この LSI はプライマリキーと同様にテーブル作成時にしか指定できず、後から追加や変更することはできません。そのため、 LSI の使い道は比較的限られています。

Partition Key が同じ、後から変更不可ということで、RDBで言う所の主キーと第2キーだけが異なるような一部の候補キーのようなものがあればそれに付けることができる他、 プライマリキーとは違い重複可能であることを利用して、例えば掲示板やチャットシステムであるようなスレッドIDを Partition Key 、その中のコメントIDを Sort Key とするような明確な親子関係を表現したテーブルにおいて子属性であるコメントの作成・更新日時などといった重複する可能性があるがソートに使用するようなキーとして使用できます。

thread_id (Main-PK, LSI-PK) comment_id (Main-SK) created_at (LSI-SK)
aaa ccc 2018-07-30 00:00:00
aaa ddd 2018-07-31 00:00:00
bbb eee 2018-07-30 10:00:00

GSI

GSI の使い方こそがDynamoDBの真髄です。GSILSI と違い、プライマリキーのようにユニークである必要もなければ、 LSI のように Partition Key を同じくする必要もありません。その代り、 GSI はメインテーブルに対する射影として実体を持つ別のテーブルであるということを念頭に入れる必要があります。射影って言われてピンときますか?射影ってRDBでも大事な概念である集合論における重要要素ですからね??(煽り芸)

射影 (集合論) - Wikipedia

slideship.com

GSI は超ざっくり言うとテーブルを別の角度から切り出したソート済みのデータの部分複製です。

https://image.slidesharecdn.com/mysql-160125081954/95/mysql-62-638.jpg?cb=1454483169

雑なMySQLパフォーマンスチューニング from yoku0825

つまり、射影として実体を持つ別のテーブルであるため、通常のテーブルと同じように独立したキャパシティを持ち Partition Key で分散した中で Sort Key によってインデックスが構築されています。セカンダリインデックスではありますが、その実体も分散しているのです。

また、 GSI は別のテーブルとして実体を持つため、本体のテーブルへの更新は非同期で反映されます。つまり、 GSI に対するクエリは結果整合性しか選ぶことができません。 結果整合性 と聞くと「ウッ」とか「大丈夫かな?」ってなる方が多い印象がありますが、MySQLのリードレプリカだって結果整合性なわけで、検索用である GSI の主な用途を考えるとほとんどがMySQLならリードレプリカに投げるようなクエリになるはずで、多くの場面で問題は無いといえるはずです。

分散したインデックスにおいて考慮すべきこと

(プライマリキーも含む)インデックスがPartitionによって分割されているということは、別々のPartitionに対してアクセスが分散されれば高い並列処理性能が出せるということです。これをまず念頭に置きましょう。それと同時に、あえて偏りを許容することで検索に都合の良いインデックスを構成するために、Partition当たりの処理性能を踏まえる必要があります。Partition当たりの処理性能は2018-07-31現在、 3000RCU および 1000WCU です。これを超えるとスロットリングが発生して処理がエラーとなります。逆に言えば、これを超えないならあえて偏りを許容することもできるのです。

docs.aws.amazon.com

次巻に向けて

SQLのような柔軟なクエリ言語を持たず、「フルスキャンしたら負け」と言われるDynamoDBにおいてテーブルの検索を効率的に行うにはこれらのインデックスの構成が非常に重要です。次回は実際のデータモデリングを行う上での考え方から入り、インデックスの設計まで踏み込めるといいなあ(願望)

次巻

書きました

marcy.hatenablog.com

*1:B-TreeではなくB+Treeであるという記述はDynamoDBの論文などでも見つけることはできません。でもSort Keyとか別名Range KeyとかいうくらいだからSortとRangeに強いB+Treeじゃないと説得力無いよね?

*2:あくまでPartition内では同期であるということなので、Partitionが複製されるDynamoDBにおいては読み込みクォーラムの数を増やさないと強い整合性を担保できないためオプションです