はじめに
管理画面をつくる!
一覧が欲しい!
ちなみに検索もしたい!
部分一致!前方一致!後方一致も!
・・・・・みたいな要件、それもAWSを使って、という制約付きのものがもしきたら。
あなたはMySQL(RDS)にしますか?
それとも、
「コストが安い!」「高速!」と言われている、
最近話題のDynamoDBにしますか?
私の決断はMySQLでした。
両DBの特徴
そもそもDynamoDBとは
AWSが提供するフルマネージド型のNoSQLデータベースサービスです。サーバーレスで運用でき、インフラ管理が不要なのが大きな特徴です。Key-Value型のデータモデルを採用しており、パーティションキー(とオプションでソートキー)を使ってデータにアクセスします。スキーマレスなのでテーブル設計の柔軟性が高く、自動的にスケーリングしてくれるため、トラフィックの変動にも強いです。
では、DynamoDBと比べてMySQLとは
MySQLはオープンソースのリレーショナルデータベース(RDB)で、SQLを使ってデータを操作します。テーブル同士をJOINしたり、複雑な条件での検索、集計、ソートなどが得意です。「WHERE句で柔軟に絞り込みたい」「複数テーブルを結合したい」「部分一致検索をしたい」といった要件には、やはりRDBが向いています。
あと、私が学生時代初めて授業で学び、それ以降使い続けたのがMySQLだから、というのもありますが。
では、DynamoDBの落とし穴は
1. 検索条件の制約
冒頭で「部分一致!前方一致!後方一致も!」と書きましたが、実はここにDynamoDBの大きな制約があります。
DynamoDBのソートキーで使える条件は、前方一致(begins_with)のみです。部分一致(contains)や後方一致はソートキーの条件としては使えません。
つまり「名前に"田中"を含むユーザーを検索」のような要件は、DynamoDBのQueryでは効率的に実現できません(Scanして全件なめるか、FilterExpressionを使うことになりますが、後述の通りコストがかかります)。
こういった柔軟な検索要件がある場合は、やはりMySQLのLIKE '%田中%'が強いです。
2. Scan
これはなにかというと、全件Scanのことです。
量が少ないうちは微々たるものですが、例えばこれが何万件・・・・となった場合。。
DynamoDBにはRCUとWCUというのがありますが、RCUを大量に食い尽くすことになりコストが一気に跳ね上がるのです。
RCUとは
Read Capacity Unit(読み込みキャパシティユニット)の略で、DynamoDBの読み込み処理に対する課金単位です。1RCUで、最大4KBのアイテムに対して1秒あたり1回の強い整合性のある読み込み、または2回の結果整合性のある読み込みが可能です。Scanは条件に関係なくテーブル全体を走査するため、データ量に比例してRCUを消費します。ちなみにWCU(Write Capacity Unit)は書き込み用の課金単位で、1WCUで最大1KBのアイテムを1秒あたり1回書き込めます。
では逆にDynamoDBはなぜ使われるのか
1つのキーを指定して1つのデータを取り出したい場合、DynamoDBは威力を発揮します。
MySQLでいうと、
SELECT * FROM T WHERE id = N
みたいなものです。
DynamoDBでは、こういうところこそ大きな威力を発揮します。
認証とか、セッションとか、例えば1ユーザーにつき一つしかレコードを持たないものとか。
DynamoDBのキー構成(補足)
DynamoDBのキー構成には2パターンあります。
| キー構成 | 説明 |
|---|---|
| パーティションキーのみ | パーティションキーの値はテーブル内で一意である必要があります。MySQLでいうPRIMARY KEYと同じイメージです。 |
| パーティションキー+ソートキー(複合キー) | 同じパーティションキーを持つレコードが複数存在できます。ただし「パーティションキー+ソートキー」の組み合わせは一意です。 |
例えば、ユーザーIDをパーティションキー、注文日時をソートキーにすれば、1ユーザーに対して複数の注文レコードを持てます。この場合でもDynamoDBが有効なのは、「特定のユーザーの注文一覧を取得する」といったアクセスパターンが明確で、パーティションキーを指定したQueryで効率的に取得できるからです。
一方で、「全ユーザーの注文を横断的に検索したい」となるとScanが必要になり、コストの問題が出てきます。つまり、アクセスパターンが事前に決まっていて、キー指定で取得できる設計ができるなら、複合キーでもDynamoDBは有効です。
そんなところではコストがMySQLのほうに勝るので、弊社でも採用していることが多いです。
では、絞り込みはできないのか?
いいえ。必ずしもそうではありません。
DynamoDBではパーティションキーの他にソートキーを設定しますが、ソートキーで絞り込む場合は、これはScanにはならずQueryによる絞り込みになります。
もちろん、指定したソートキーの値に合致するものは全件くるので、ソートキーX: 値がaのレコードが1件、値がbのレコードが100万件・・・・となれば、当然コストはかかります👼
ですが1ソートキーあたりのレコード量がある程度予測できている場合、逆にMySQLよりもコスト的に有利になる場合があります。
DynamoDBでも他に絞り込みがしたい!!
そういう声に答えるような仕組みが、一応はあります。
GSIとLSIです。
GSIとは
Global Secondary Index(グローバルセカンダリインデックス)の略です。元のテーブルとは異なるパーティションキー・ソートキーの組み合わせでクエリを実行できるようにする仕組みです。
例えば、元テーブルが「ユーザーID」をパーティションキーにしていても、GSIを作れば「メールアドレス」や「作成日」などでも効率的に検索できるようになります。
簡単に言うと、絞り込みの効率をよくするために別のテーブルがこっそり立てられます。(ただし、AWSのコンソール上では見えないですが)
LSIとは
Local Secondary Index(ローカルセカンダリインデックス)の略です。GSIと違い、パーティションキーは元テーブルと同じまま、ソートキーだけを別の属性に変えてクエリできる仕組みです。
例えば、パーティションキーが「ユーザーID」でソートキーが「注文日」のテーブルに対して、「ユーザーID」×「商品カテゴリ」でも検索したい場合にLSIを使います。
GSIのように別テーブルが作られるわけではなく、元テーブルと同じパーティション内にインデックスが作られるイメージです。要するにもう一つソートキーを作れる!みたいなイメージです。
ただし、そのため、テーブル作成時にしか定義できない(後から追加できない)という制約があります。
そして、「同一パーティションキー内(元データ+LSI)で合計10GBまで」という非常に厳しい制約があります。サービスが成長してデータが増えたとき、この10GB制限に達すると書き込みができなくなり、「詰み」(Item Collection Size Limit)が発生するわけです。
そのため、近年ではLSIではなくGSIの使用が推奨されるケースがほとんどです。
GSI vs LSI のパフォーマンス比較
| 項目 | GSI | LSI |
|---|---|---|
| 整合性 | 結果整合性のみ | 強い整合性も可能 |
| 書き込み時のタイムラグ | 少しあり(別テーブルへの書き込み) | なし |
| データ量制限 | なし | 同一パーティション内で10GBまで |
| 追加タイミング | いつでも追加可能 | テーブル作成時のみ |
GSIはこっそり立てられたテーブルへの書き込みがあるので、当然(誤差の範囲だとは思いますが少々)タイムラグが発生します。
LSIは元テーブルと同じパーティション内に存在するため、強い整合性(Strong Consistency)での読み込みが可能で、書き込み時のタイムラグもありません。ただし、同一パーティション内のデータ量が10GBを超えられないという制限があるので、大量データを扱う場合は注意が必要です。
FilterExpressionという罠
「GSI/LSI以外でも絞り込みできるじゃん!FilterExpressionがあるじゃん!」と思った方もいるかもしれません。
確かにFilterExpressionを使えば、パーティションキーやソートキー以外の属性でも条件を指定できます。
ただし、これはScan/Queryの結果に対してフィルタリングしているだけです。つまり、一度データを読み込んでからフィルタするので、RCUの消費は減りません。100万件読み込んで、FilterExpressionで1件に絞り込んだとしても、100万件分のRCUがかかります。
便利そうに見えて実はコスト面では落とし穴なので、注意が必要です。
MySQLのコストについて
EC2、Lambda、RDS、VPC...以外にも触った経験はありますが、コストに気を遣うものといったらやはりRDSではないでしょうか。
ゆえに、私が属するチームではコストが低いとされるDynamoDBにほぼ走っているのですが、先程挙げたコストの問題があります。
では、MySQLではそのコストの問題は存在しないのか、
1. データ量におけるコスト
DynamoDBでは、一度に読むデータが多ければ多いほどRCUが発生し、コストになると書きました。
では、MySQLではどうかというと、以下の要素でコストが決まります。
- 稼働時間
- リソースのスペック(インスタンスタイプ)
- ストレージ容量
- バックアップストレージ
- データ転送量(リージョン外への転送など)
DynamoDBでは100万件を一気に・・・・となりましたが、RDSのほうでは何件読んだかはコストに影響しません。
2. 予測可能性
RDSは月額固定に近いコスト体系なので、予算が立てやすいです。一方DynamoDBは従量課金なので、予期せぬトラフィック増加でコストが跳ね上がるリスクがあります。
ただし、DynamoDBにはオンデマンドモードとプロビジョンドモードがあり、プロビジョンドモードでキャパシティを固定すればある程度コストを予測できます(その代わり、キャパシティを超えるとスロットリングが発生します)。
3. アクセス頻度による逆転
ここまでDynamoDBのコストの落とし穴を書いてきましたが、実はアクセス頻度が低い場合はDynamoDBの方が安くなるケースもあります。
- RDS:24時間365日インスタンスが起動している限りコストがかかる
- DynamoDB(オンデマンドモード):使った分だけの課金
例えば「月に数回しかアクセスしない社内管理画面」のようなケースでは、RDSを常時起動しておくよりも、DynamoDBでたまにScanする方がトータルコストが安くなることがあります。
アクセス頻度と1回あたりのデータ量、この両方を考慮して選定することが大切です。
スペックアップについて
最後に、上記以外にDynamoDBかRDSかを決める決定的な要件はないのか、と考えたときに、サービスが大きくなれば大きくなるほど必要になるのがスペックアップだと思います。
これについても、DynamoDBかRDSで大きな差があり、それぞれ要件に応じた選定が必要になります。
DynamoDBは水平型
DynamoDBは水平スケーリング(スケールアウト)を前提に設計されています。
- データは自動的に複数のパーティションに分散される
- トラフィックが増えればパーティション数も自動で増加
- ダウンタイムなしでキャパシティを変更可能
- 理論上は無制限にスケールできる
ただし、パーティション設計(ホットパーティション問題の回避など)をしっかり考えないと、特定のパーティションに負荷が集中してパフォーマンスが落ちることがあります。
RDSは垂直型
RDSは一度スペックを決めたら、変更する際にはDBの再起動が必要になります。シングル構成の場合、これは直接ダウンタイムにつながります。
ここで重要になるのがMulti-AZ構成です。
- Multi-AZ構成にしておけば、スタンバイインスタンスへのフェイルオーバーにより、ダウンタイムを数分程度に抑えられる
- 完全にゼロにはなりませんが、シングル構成よりは遥かにマシ
- 本番環境では基本的にMulti-AZ構成にしておくことを強くおすすめします
また、読み込み負荷を分散させたい場合は、リードレプリカを追加するという手もあります。
さらにAurora(MySQL互換)を使えば、より柔軟なスケーリングが可能です。
- Auto Scalingでリードレプリカを自動増減
- Aurora Serverlessで使用量に応じた自動スケーリング
書き込みのスケールアウトは難しいですが、読み込み負荷の分散という点ではRDSでも対応策があります。
最後に
おもに以下の2つで、DynamoDBかRDSかが決まるのではないかと思います。
- 一覧表示するか
- 絞り込みをするか
最初DynamoDBで進めようとしていたので、今考えるとそれが恐ろしく感じます。
逆に、以下の条件であればDynamoDBが威力を発揮します。
- 1つのキーの値が一意であること、他のレコードで同じキーと値の組み合わせがないこと
- 指定したもの一つだけ取れたら良い
こういう選定の経験もしっかり自分のものにして、案件ごとに最適なものをご提案していきたいですね!
コメントなどありましたらご遠慮なくどうぞ!
前回こちらの記事も書かせていただいたので、よろしかったらあわせてどうぞ!
https://qiita.com/bsj-s-kuhara/items/1d4909121e4a2bb71c15
ありがとうございました!