Help us understand the problem. What is going on with this article?

DynamoDBと仲良くなる方法

More than 1 year has passed since last update.

はじめに

この記事は Recruit Engineers Advent Calendar 2018の23日目です。

AWS Lambdaを中心としたサーバレスアーキテクチャも割と普及してきた世の中ですが、それに付随してDynamoDBもよく使われるようになってきたのかな、と最近感じています。

上記のようなアーキテクチャの中でDynamoDBが選定される理由の一つとしてLambdaとRDSの相性の悪さ(これに関しては至る所で語り尽くされていると思うのでここでは割愛します)、つまり本当はRDBMSを使いたいが消極的な理由で(仕方なく)DynamoDBを選択し運用しているケースをよく見かけます。

こういった方々がRDBMSのようにDynamoDBを扱おうとした結果、思ったように扱えず困り果てるというケースを何回か見たことがあるので、(そんな人たちがどれくらいいるのかわかりませんが)今回は自分がDynamoDBを扱うときに気をつけていることをまとめてみることにしました。

なので今現時点で問題なく普通に使えている人には正直あまり学びのない記事になってしまったかもしれません。

DynamoDBとは

え、そっからかよ、って感じかもしれませんがおさらいです。
DynamoDBはスキーマレスなNoSQLです。(ドキュメント指向でもKVSっぽくでも使えます)
重要なのはRDBMSではない、という点です。

詳細はドキュメントを見ると良いです。

仲良くなる方法

個人的にDynamoDBを使うときに意識している点を書き出してみました。

RDBMSの代替品として考えない

当たり前ですがこれが一番大事です。
違うものだということを認識してドキュメントをしっかり読み、特徴をちゃんと把握することが重要です。

必要に応じて非正規化を行う

DynamoDBではテーブルの結合はサポートされてません。テーブル同士のリレーションを定義することがサポートされていないNoSQLであるDynamoDBにおいて、正規化を行うことは必ずしも正解ではありません。

この点ではAWSの公式のブログでも基本的には非正規化して設計していくことをガイドラインとして打ち出しています。

例えばQiitaを例に出すと、もしかしたら記事についているタグ情報はArrayで記事テーブルに持ってしまってもいいかもしれません。(もちろん、タグ名の変更で全てのタグの名前が一斉に変わる必要がある、などの同期・生合成の要求がないことが前提です。)
記事取得の際には複数のテーブルにアクセスすることなく、記事のIDで引いてこれますし、登録の際にもタグ情報もアトミックに保存することが可能です。
(待望のトランザクションがサポートされたのでテーブルが別れていても原子性を担保することはできるようになったと思ってはいますが、まだ自分ががっつり検証したわけではないのでここではトランザクションに関しては言及しません。)

ちなみにAWSの公式のベストプラクティスには DynamoDB アプリケーションではできるだけ少ないテーブルを維持する必要があります。設計が優れたアプリケーションでは、必要なテーブルは 1 つのみです。 と書かれています。後者は理想論だとは思いますが、なるべくテーブル数を少なく保つというのは意識した方が良いです。

ただ1点、1レコードの最大サイズが400KBとそこまで大きいわけではないので、定常的に増え続けるデータを配列で持ったり、何でもかんでも非正規化して横持ちしていくのは控えた方がよいと思います。

結果整合を受け入れる

DynamoDBは高可用性を担保するために3つのデータセンターで常にデータをレプリケーションしています。
その結果基本的にDynamoDBの操作は結果整合になってしまうことを意識する必要があります。

データの書き込みを例に挙げると、例えば一つのレコードを追加しようとするとDynamoDB的には3回書き込みをすることになります。
しかし全部を待っているとレスポンスに影響が出るのでDynamoDBは2箇所の書き込みを終えた時点でAckを返します。
つまり最後の1箇所は まだ正しいデータが書き込まれているかわからないが、いずれ正しく書き込まれる 状態になってしまい、これを結果整合と言います。(DynamoDBのドキュメントには「通常は 1 秒以内」とあります)

上の部分で 基本的に 結果整合になる、といったのは読み込み時に強整合性を担保して読み込むことができるConsistentReadというオプションが用意されているからです。

ただ、だからといって全ての読み込みにConsistentReadをつけるようなことはオススメしません。
強い整合性を持った読み込みはキャパシティユニットを2倍消費するので、強整合性が必要なケースにのみつけることをオススメします。
実は作っているアプリケーションで常に最新の情報が読み込まれないといけない箇所は多くないかもしれません。(もちろん、アプリケーションの特性によりますが。)

キャパシティユニットを(大体)理解する

DynamoDBを使い始めるとRCU(読み込みキャパシティユニット)とWCU(書き込みキャパシティユニット)を設定するところで、なんだこれ?ってなると思います。
正直キャパシティユニットに関しては僕も完全に理解しているわけではないですが、書き込みと読み込みそれぞれのキャパシティ要件を定義するとDynamoDB側でそれに耐えられるために準備をしてくれる、くらいに思っておけば良いです。

なぜこいつを大体理解しておいた方がいいのかというと、アプリの性能面で問題が出たときに大体こいつらがボトルネックになってることが多いからです。逆にいうとキャパシティユニットを上げさえしてしまえばDynamoDBのスループットの問題は解決することがほとんど(もちろん解決しないケースもあります)なので素晴らしい仕組みとも言えます。

一点 急激にキャパシティユニットを上げて下げると性能劣化が起こるケースがあるのでそれは注意が必要です。
「定義されたキャパシティに耐えられる準備」と書きましたがあれは実は中でパーティション分割を行っており、キャパシティユニットが増えた段階でパーティションを増やし、減らす際にパーティション数は落ちずにそれぞれのパーティションにあてがわれるキャパシティユニットだけが減っていくという仕組みになっているかららしいです。

と、ここまで説明したものの先日のre:inventでDynamoDB On-Demandの発表がありましたので今後はあまり考える必要がないかもしれません。このアップデートはRCU, WCUの事前設定をせずに消費した分のみ請求される、といった夢のような内容です。まだがっつり検証したわけではありませんが、今の所 DynamoDB On-Demand にワークロード的に問題なさそうなので今後これが主流になっていくんじゃないかな、と個人的には思ってます。(急激なスパイクなどは未検証)

インデックスを理解する

DynamoDBにはGSI(グローバルセカンダリインデックス)とLSI(ローカルセカンダリインデックス)の2種類のインデックスがあります。
ここではそれらの詳細を説明するというより(ドキュメントを参照した方が早いです)はそもそもDynamoDBのインデックスがどのように作られているかを説明したいと思います。

誤解を恐れずいうとDynamoDBでインデックスを作ることは、ほぼテーブルを作ってることと同一視してよいです。例えば以下のようなPrimaryにNameを持ったテーブルとレコードがあるとします。

この時点ではDynamoDBではNameでしか検索できません。

Name Color Size
Apple red medium
Orange orange small
Banana yellow big

ではColorで検索するためにColorをHashkeyとするGSIを作って上げるとします。

そうすると内部的には以下のようなテーブル(のようなインデックス)が作られます。これによってColorで検索できるようになってるだけです。
実際にはインデックスを作成する際にどのカラムを射影するか選べるのでそこで削っていけば以下のように全てのカラムにはならないのですが、検索で使ったりするユースケースでは結局ほぼ全てのカラムが欲しくて全て射影してしまうケースが多いと思います。

Color Name Size
red Apple medium
orange Orange small
banana Banana big

つまりインデックスをドカドカ作っていくとその分n倍にデータが溜まっていきます。当然そのテーブルのI/Oのコストも上がります。
インデックスは使っていくことにはなると思いますが、射影するカラムをきちんと選択しスリムなインデックスを目指し、(これはRDBでもそうですが)不要なインデックスは作らないようにしましょう。

ちなみに、上で取り上げた、強い整合性を持った読み込みConsistentReadですが、GSIに対してはサポートされていません。GSIは必ず結果整合になる、という点は注意が必要です。

検索は苦手だということを把握しておく

DynamoDBでも検索はできます。が柔軟な検索は苦手だということを理解しておく必要があります。

例えば以下のような生徒ごとの宿題を管理するためテーブルを作りたいとします。
メインのユースケースとして生徒ごとの宿題の一覧を取得する、というものがあるのでプライマリキーとしてパーテーションキーにStudent、ソートキーにCreatedAtを持たせるとします。
これにより Query を使って各生徒ごとの宿題を宿題を出した日で検索やソートをして取得することができます。

Student CreatedAt Subject Status DueDate
John 1545831410 math done 20200101
John 1545831420 English undone 20200101
Smith 1545831430 English done 20200101

ただ実際のユースケースでは教科ごとの宿題の状態を見たい、などの要件がでてくることは容易に想像できます。
これは先ほども説明したようにインデックスを使うことで解決できます。ここではSubjectをパーテーションキーとしたグローバルセカンダリインデックスを作る(かつDueDateをソートキーにしたり。その辺は要求によってお好みで)ことになるでしょう。

さあ次は「来週の月曜日に提出予定の理科の宿題をやってる生徒、やってない生徒を閲覧したい」というような要求を考えて見ます。
つまり「特定のDueDate、かつ特定のSubjectの宿題を Status で検索したい」という要求になります。
DynamoDBはパーテーションキーでパーテーションを特定し、ソートキーで検索・ソートする、という動作が基本なのでQueryの条件式に上記の3つの条件を入れることは不可能です。

もちろん実現できないわけではありません。DynamoDBのQueryでは通常の条件式(パーテーションキーとソートキーしか指定できない)とは別に FilterExpressionというものに条件を指定することができます。なので先ほどの例でいうとSubjectをパーテーションキー、DueDateをソートキーのグローバルセカンダリインデックスから、FilterExpressionで特定のステータスを指定してクエリを流れば期待する値は返ってきます。
しかし、FilterExpressionというのはあくまでも取得結果を Filter しているにすぎず、 SubjectDueDate検索をかけた値をDynamoDBから取得し、その後で特定のステータスのレコードのみを返却しています。
つまりいくらFilterExpressionをつけてもアクセスするレコード量は減るわけではなく、この挙動を知らずにFilterExpressionに頼りっぱなしになると思わぬ性能懸念が生まれます。

さらに厄介なのが上記の通り一度データベースアクセスしてから Filter しているだけなのでLimitが必ず期待通りの動きをするかわからなくなってしまいます。仮にLimit=3で「教科が数学、提出日が明日でまだ未対応の生徒を検索する」場合、まずDynamoDBは「教科が数学、提出日が明日」で検索をかけ(Queryの条件式)、そしてLimit=3ここにかかります。その後取得してきた3件に対して初めてFilterExpressionでステータスのフィルタを行うので例えば、その3件がdoneステータスだった場合は、本当はundoneステータスのものがデータベースには存在していても返却は0件になってしまいます。

上記のような観点から個人的にはこのようなケースはコンポジットキーを作ってしまうのがよいと思っております。
つまり以下の SubjectDueDateのようなSubjectDueDateをくっつけたようなものを作ってしまい、それをパーテーションキー、ソートキーにはStatusのグローバルセカンダリインデックスを作る、ということです。これにより普通にパーテーションキー+ソートキーの条件式のみで検索することが可能です。

Student CreatedAt Subject Status DueDate SubjectDueDate
John 1545831410 math done 20200101 math-20200101
John 1545831420 English undone 20200101 English-20200101
Smith 1545831430 English done 20200101 English-20200101

ただ実際の業務では、これ以上に複雑な要求は容易に考えられます。基本的にはうまく非正規化したり、GSI、LSIの活用で大多数の検索要求には答えられるとは思っておりますが、やはり検索がそれほど得意でないことに変わりはないので個人的には検索要求が肥大化したタイミングでElasticsearchなどの検索エンジンに検索の責務を持たせてしまった方がよいと思っています。(DynamoDBのドキュメント指向なデータの持ち方だとElasticsearchへの連携も楽ですし)

特にLike検索っぽいことをやりたくなったら検索エンジンの導入の検討タイミングだと思っています。(転置インデックスを使うという選択肢もあるようです。)

ちなみにパーテーションキーとソートキーという言葉を使ってきましたが、これらは昔のハッシュキー、レンジキー、と呼ばれていたものです。CFnの設定などはいまだにHASHRANGEなので混乱しがちですが、この記事ではパーテーションキーとソートキーを使います。

最後に

まだまだ書ききれないのですがこれ以上はTips集みたいになりそうなのでやめておきます。
割と書いて見たら当たり前のことをつらつら書いてるだけになってしまったので、どれだけの方に役にたつかはわかりませんが、少しでも役に立てれば幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away