「サーバレスがもてはやされてるけど RDS 使いたい時もあるじゃん?」について調べた
Docker・Kubernetes を前提としたコンテナアプリケーションの他に、AWS Lambda のような FaaS・サーバレスアプリケーションが主流になって久しい。それまでの「MVC アプリ + DB (RDS)」といったモノリシックな構成は、スケーラビリティに欠けるなどの理由で避けられるようになってきた。
しかし、サーバレスやコンテナといったステートレスなアプリ基盤でも、何らかのデータを永続化したり、永続化データを取得して返したり、といったことは発生しうる。そんな時、データ永続化層に RDS を組み合わせて良いものか、何をどう扱うのが良いものなのか。今さらな話題かもしれないが、素人が調べたことを簡単にまとめておく。
目次
- サーバレスと RDS の組合せはアンチパターン
- サーバレスアプリには「分散型 DB」を組み合わせる
- サーバレス・分散型 DB の組み合わせで検索処理を高速に行うには
- サーバレス + RDS でコネクションプーリングしたい
- マイクロサービスをまたがるトランザクションをいかに扱うか
- サーバレスでロックや排他制御は?
- そもそも RDS 相手にサーバレスを選定する必要はあるのか?
- 以上
- 参考文献
サーバレスと RDS の組合せはアンチパターン
まず、「サーバレスと RDS」の組み合わせはアンチパターン。組み合わせた設計にしないのが妥当といわれている。なぜかというと、リクエストの数だけアプリコンテナが作成され、コンテナごとに DB コネクションが張られてしまうからである。
- 「DB への接続」というのは、認証やら接続確立やらで CPU リソースを使うし、応答時間も遅くなる
- DB への同時接続数には限界があり、コネクションプーリングという仕組みで同時接続数を節約したり、接続可能になるまで待機してもらったりするのが一般的だが、コンテナ間でコネクションプールを共有することは困難
- コネクションプーリングを実現するためには、「DB との接続を保持しておく場所 (ミドルウェア)」が必要になるが、コンテナが破棄されてしまう仕組みではそうした部分が存在しないため
- するとリクエストの数だけ新規 DB 接続が試行されるが、最大同時接続数を超えると接続に失敗してしまうことになる
- 新規接続自体にリソース負荷や処理時間がかかるし、最大同時接続数を超えた時に「DB 接続失敗」となって終了してしまうため、かなりイマイチ
- じゃあ、DB の最大同時接続数を上げておくか、と思うと、DB のスペックを無尽蔵に上げていくしかなくなるが、それでも対処しきれるかは不明
- RDS はデータ一貫性を重視するため、スケールアウト (サーバ台数増加) による対応が難しい。スペックを上げるにはスケールアップ (サーバ性能増強) による対応が必要になる
- しかし、リクエスト数が不定なので、暇な時は性能を持て余すし、高負荷の場合はいくらスペックを上げていても足りなくなる恐れがある。クラウド利用料などのコストにも影響するので、高スペックな DB を待機させておく方法は採用しづらい
じゃあサーバレス基盤において、データ永続化はどうしたらいいの?というと…
サーバレスアプリには「分散型 DB」を組み合わせる
サーバレスアプリケーションには、Amazon DynamoDB のような分散型 DB (NoSQL 系 DB) を組み合わせることが一般的には推奨されている。
Amazon DynamoDB などは、データ一貫性を犠牲にすることでスケールできるようにしているので、サーバレスアプリ側の接続数増加にもスケールアウトで対応できるようになっているワケだ。
- DynamoDB の場合、同じデータを3箇所にコピーして保持するような仕組みだが、2箇所に書き込みができた時点で「書き込み OK」と応答し、残り1箇所はそのうち書き込まれる、という扱いになる
- 大抵は1秒以内に残り1箇所への書き込みも完了するので、よほどシビアな使い方をしていなければ「データのズレ」を感じることはないものの…
- 仕組み的には、更新が反映されていないサーバから更新前のデータを取得してしまう可能性があるので、アプリ側で考慮していないといけない
- そうしたデメリットはあるものの、RDS が苦手としていた「スケールアウト」に対応できることで、同時接続数の上昇にも対応してリクエストを捌ける
- 例えば、「IoT デバイスからのテレメトリ書き込み」といった用途で大量トラフィックをさばく必要がある場合は、「読み取り時のデータ一貫性」はある程度捨てても良いことになる
- ちなみに DynamoDB なんかでは、読み取り速度が低下するものの、「読み取り一貫性」を重視して読み取りを行えるオプションもあったりするので、コレで足りれば良い
ということで、無尽蔵にスケールしうるサーバレスアプリに対しては、RDS よりもスケールさせやすい、分散型 DB を選択するのが妥当であろう。
分散型 DB のマネージドサービスは、VPC の準備なども必要ないことから管理コストは低いが、一方でその仕組みから、検索処理やトランザクション処理が苦手だったりもする。こうした用途で使いたい場合はどうしたらいいだろうか。
サーバレス・分散型 DB の組み合わせで検索処理を高速に行うには
分散型 DB はデータが分散配置されているため、それらを通して検索するような処理はパフォーマンス面で不利だったりする。そんな時に高速に検索処理を行うにはどうしたらいいかというと、RDS とのハイブリッド構成という案がある。
- データ書き込みは分散型 DB に行う
- 書き込まれたデータは、非同期で RDS に登録する
- 検索処理は RDS に接続して行う
検索処理を行うサーバレス関数が結局 RDS に接続するので、そこのリクエスト量と DB 性能は考慮しておかないといけない。
サーバレス + RDS でコネクションプーリングしたい
少し考え方を変えて、「サーバレスアプリと RDS の間に、コネクションプーリングしてくれる場所を作れないかしら?」と思ったところ、AWS RDS Proxy というサービスがあった。
- 前述のとおり、「コネクションプール」という仕組みは、RDS へのアクセスの度に接続と切断を繰り返すのではなく、一度形成した接続窓口 (コネクション) を維持して使い回す手法である。初回の接続確立までにはハンドシェイクやらユーザ認証やらが発生するが、一度接続した状態を維持しておけばその辺の接続要求が省略できるワケだ
- コネクションプールの仕組みがない場合は、同時接続数が上限超過した場合に接続拒否となってしまうが、コネクションプールがあれば、プーリングレイヤが DB への流量を調整して待機してくれたりする
- AWS RDS Proxy は、Lambda (サーバレス) と RDS の間を取り持ってコネクションプールの役目を担ってくれるサービスだ
AWS に限ったサービスではあるものの、サーバレスといえば Lambda、といってもいいぐらいデファクト・スタンダードなサービスなので、採用は比較的容易であろう。
他には、Azure も Functions の機能次第で、似たような調整ができるようだ。
マイクロサービスをまたがるトランザクションをいかに扱うか
Kubernetes 上にデプロイした複数のマイクロサービスだったり、複数のサーバレス関数なんかで、トランザクションを扱いたい時にどうしたらいいかを調べた。
- そもそもトランザクションとは : 全てが成功するか、全てが失敗するかとなる一連の処理のこと
- マイクロサービスでは、RDS レベルのトランザクションが利用できないことになる
→ そこで、分散トランザクションという考え方・仕組みで対処する必要がある。分散トランザクションを実現する代表的なパターンが2つある。
- TCC パターン : Try operations, Confirmation, and Cancellation
- はじめにデータを仮登録する。全てのサービスで仮登録が成功したら通知を流し、正式にデータを登録する
- どこかのサービスで失敗やタイムアウトがあれば、局所的に再試行したり、それでもダメな場合は中止したりする
- 「仮登録」と「本登録」とで、各サービス間で最低でも2回は通信するので、処理時間がかかるかもしれない
- Saga パターン
- 各サービスに処理を依頼する。どこかで失敗があった場合は、RDS におけるロールバックの代わりに「補償」と呼ばれる擬似的なロールバック、取り消し処理をリクエストする
- 補償リクエストは冪等性を担保すること、処理順序に関係なく実行できること (順番交換可能)、必ず補償リクエストが成功すること (失敗不可能・成功するまで繰り返すこと) を満たす必要がある
- ACID 原則の内、Atomicity (原子性・トランザクション)、Consistency (一貫性・整合性)、Durability (永続性・結果が失われない) は実現できているが、Isolation (独立性・操作の過程が隠蔽されること) が実現できておらず、未完了のトランザクションからデータの読み書きが可能になっているので例外の対策が必要となる
いずれのパターンであっても、トランザクション内のコミットとロールバックに対応する処理を自分で実装する必要がある。
- オーケストレーション (中央集権型) と呼ばれるサービス連携方式では、中央の指揮者が各サービスを呼び出し、結果を確認して取り扱っていく、同期的な処理方式となる
- コレオグラフィ (分散型) と呼ばれる連携方式では、個々のサービスが自律的に動作し、他のサービスに MQ (メッセージキュー) などの通知を送る、非同期的な処理方式となる
どちらのサービス連携方式であっても、TCC パターンと Saga パターンの両方が実装できる。オーケストレーションの方は分かりやすいと思うが、コレオグラフィの方は
- サービス A が処理を完了したら、サービス B に通知を送る
- 通知を受け取ったサービス B が処理を完了したら、サービス C に通知を送る
…といった形で、サービスがネストされたような呼び出し方になる。
AWS のサービスでいくと、AWS Step Functions で Saga パターンの管理がしやすそうだとか、AWS AppSync でトランザクション結果をリアルタイム通信で通知できそうだとか、クラウドサービスを利用することで扱いやすくなるみたい。
サーバレスでロックや排他制御は?
Amazon DynamoDB なんかはレコード作成時の前提条件が定義できるので、コレを用いて排他制御ができるみたい。
そもそも RDS 相手にサーバレスを選定する必要はあるのか?
さて、ココまで色々とサーバレス + RDS の扱いについて調べたことをまとめてきたが、ココでガラッと入れ替えて、RDS と組み合わせる相手がサーバレスアプリである必要があるのか、を考えてみる。
RDS Vs. 分散型 DB
まず、なぜ RDS を使いたいかというと、「データ一貫性」や「高速な検索処理」が狙いであろう。しかし、RDS はスケーラビリティに欠け、リクエスト数が見えない用途ではスペックを持て余したり不足したりすることが交互に発生しうる。
一方、リクエスト数の急激な変化に対応できるスケーラビリティを追求したいのであれば、分散型 DB を選択すれば良い。この場合、「データ一貫性」や「高速な検索処理」はある程度犠牲になる。
データ永続化層の目的や性質として、どちらを追求するか、まずは明らかにする。
サーバレス・アーキテクチャを選ぶ狙いをおさらいする
続いてアプリのアーキテクチャ。
昔ながらのモノリシックなアプリケーション構成は、M・V・C が密結合になり、変更にかかるコストが大きくなりがちである。そこで、変更に強いアプリにしたいために、フロントエンドアプリとバックエンドサーバの分離だったり、バックエンドもマイクロサービス化するとかサーバレス関数に分けるとかして、疎結合に作ることが増えた。
なぜ変更に強くしたいかというと、変更の頻度が多いからであろう。事業・ビジネスの変化やスピードに合わせて、アプリも頻繁に、かつ高速に変更していきたいからこそ、それに対応できるアーキテクチャとして、サーバレスを選定するワケだ。
サーバレスやマイクロサービス構成のデメリットは、高遅延であること、低効率であること、動作が不安定であることだ。処理ごとに複数のサービスを呼び出すため、どうしても速度は出ないし、ネットワーク上のトラブルに巻き込まれるリスクも増える。一部のサービスだけ処理に失敗したりすることもあり得ることを、事前に了承しておかないといけない。
すなわち、高遅延・低効率・不安定さを飲み込んだ上で、それでも改修スピードを上げることを望むなら、サーバレスやマイクロサービスといった構成を選択する意味があるワケだ。
逆に、環境がしばらく変化せず、業務が固定的なシステムであれば、モノリシックなアプリ構成を今から選定することだって間違いではないのだ。
要件とメリデメから構成要素を選定する
それぞれの技術の特徴やメリデメが分かったところで、対象のシステムの要件から構成要素を考え直してみよう。
- アプリケーション基盤
- 事業・業務が頻繁に変わり、それに素早く追従したい → サーバレスやマイクロサービスを選定する方が良い
- 事業・業務の変更が少ない → モノリスでも良い
- リクエスト数が読めない・頻繁に急変する → サーバレスにしてスケーラビリティを得る
- 高速・安定動作を重視する → モノリスで作る方が実現しやすい
- データ永続化層
- リクエスト数 (= 最大接続数) が読めない・頻繁に急変する → 分散型 DB の方が対応しやすい
- データ一貫性が重要 → RDS の方が良い
- 検索処理を高速に行う必要がある → RDS の方が良い
別のまとめ方をすると、こんな感じ。
- 頻繁に改修を行いたい・リクエスト量が読めない
- → サーバレス + 分散型 DB を選定する
- ⇔ 低速・不安定さ・データ不整合はある程度許容する必要がある
- 変更が少ない業務・リクエスト量が見込めている・データ一貫性が重要・高速な処理が必要
- → モノリス + RDS を選定する
- ⇔ 変更時のコスト、スケーラビリティの悪さはある程度許容する必要がある
…ということだ。
モノリシック・密結合な構成というのは、新規の開発はやりやすかったりする。一度作ったらしばらく放置できるようなアプリであれば、こうした構成も悪くはないのだ。
もちろん、複数の目的が被っていて、一部でハイブリッドな構成を選択する場面もあるだろうが、基本はこのような考え方で技術選定をすればよいだろう。全てをサーバレスで作る必要が本当にあるのだろうか? ということを、もう一度考えても良いだろう。
マイクロサービスについては、設計や構成を適切に行わないとメチャクチャになるので、以下の点に注意したい。
- マイクロサービスの粒度を適切にしたい → ドメインベースで設計する
- マイクロサービス間の通信を制御・監視したい → サービスメッシュを導入する
- マイクロサービス処理の見通しを良くしたい → ワークフローを整理・管理する (AWS Step Functions、Azure Logic App、Azure Durable Functions、Google Composer など)
以上
「RDS を使いたいけど、サーバレスとは相性が悪い…?」そんな疑問から色々調べた結果、「別にサーバレスに拘らなくて良い」「モノリスが向いている場合すらある」という話にまで発展した。
サーバレスに対しては分散型 DB を組み合わせるのが妥当だが、検索処理など一部では RDS を併用する、ハイブリッドな構成もアリということで、だいぶ理解が深まったと思う。
Saga パターンなど、マイクロサービスにおけるトランザクションの実現方法を学べた。
参考文献
- サーバレスアンチパターンが無くなった日 - Qiita
- TCCパターンとSagaパターンでマイクロサービスのトランザクションをまとめてみた - Qiita
- 「注文サービスをサーバーレスで作ってみた」JAWS DAYS 2020登壇資料 #jawsug #jawsdays #jawsdays2020 | DevelopersIO
- RDSプロキシは未来を変えるか - Speaker Deck
- Keisuke NishitaniさんはTwitterを使っています 「モノリス最高とかSQL最高って別にふざけてるわけではなく、実際のところ、RDBを使ったモノリスアプリの開発生産性は高い。モノリスはレガシーでイマイチみたいな感じで話されがちだけど、それで済むのであれば断然いい。RDBも同じで多くのユースケースをかなりのレベルまでさばけるし絶対こっちのが楽」 / Twitter
- Keisuke NishitaniさんはTwitterを使っています 「Microservicesが必要になるのは事業がスケールしてから。事業のスピードに開発のスピードが追いつかない、開発と運用においてモノリスのメリットをデメリットが上回ったときに初めてMicroservicesのメリットが出てくる。そして、Microservicesにも多くのデメリットがあることを忘れてはいけない」 / Twitter
- HATANO HirokazuさんはTwitterを使っています 「変化しない環境で固定的な業務ならモノリス(密結合)の方が低遅延、高効率、安定動作なのは間違いない。 環境や使い方が変化しやすい時代だからマイクロサービスで疎結合にする。 高遅延、低効率、不安定動作を呑み込むメリットがある場合ならば、という前提で。(単一障害点の原則が重視される理由」 / Twitter
- ポエム データベースで消耗している話
- AWS LambdaとDynamoDBがこんなにツライはずがない #ssmjp
- Cocoaの日々: 【番外】AWS Lambda / API Gateway / DynamoDB を使ったサーバレスなネットワークロック機構
- コンテナ&サーバーレス : トレンドの考察と少し先の未来の展望
- sagaを使用したマイクロサービスのデータ一貫性