AWS RDS Proxy の実戦的な基礎知識

RDS Proxy はリリースして4年以上経過(Amazon RDS Proxy が一般提供開始)しましたが、そこまで一般的な情報は多くないのと、自分の整理の意味を込めて適度に吐き出しておきたいと思います。

用途はいくつかある中で、今回は単純な負荷に対するスケーリング対策としての内容となります。今年最後の記事なのに想像込みの部分もあって絞まらないかもですが、お手柔らかにお願いします。



はじめに

RDS Proxy は便利な可能性を提供してくれるものですが、ただ導入しただけで幸せになれる類のものではありません。どのような仕組みであり、なぜ効率が良くなり、どのように扱えばよいのか、について正しく理解しようとする姿勢が必要なのは、他のシステムと同じです。

基本的な情報についてはリンクを置いておきますので、そちらに任せるとして、ここではそういう情報を一見しただけではわからなそーな部分についてまとめていきます。

ただお断りとして、公式ドキュメントだけでは判断できない部分については、挙動から考察する形での、私なりの一般常識内での駄弁り要素も含むことはご了承願います。

リンク集

公式

一般


目的

本記事で RDS Proxy を扱う目的は、大きなトラフィックを捌こうとする際に、とあるボトルネックを解消しようとするものです。

 『16000』

──という数値を見て青ざめたな… 勘のいいきさまは悟ったようだな… この Aurora の max_connections の最大値を見て、何が始まるのか気づいたようだな!

そう、大量のアプリケーション・サーバーが必要になった時、この上限値を理由にサーバー数を増加できなくなるという『詰みチェック・メイト』にはまるのだッ!

──ということで、RDS Proxy を用いて解決しようとした場合に、どういった考察ポイントがあるのかについて真面目に考えていくとしましょう。


機能概要

RDS Proxy の機能を公式からザックリ今北産業すると、

  • 直接のDB接続をせずProxy経由にして接続管理を任せることができるよ
  • メリットは接続管理の最適化やフェイルオーバーの高速化だよ
  • デメリットはクエリ・レスポンスのレイテンシ増加や費用増加だよ

  • といったところなんですが、これだと各所の度合いがわからない部分があり、初手の検証すら憚られるところがあります。

    導入してみたら良くなったっぽいし問題なく動いている、みたいなケースもあるかもですが、Proxy の有無による差がどのようなもので、どのような理由で課題が解決するのかを理解しようとはするべきで、それには関係する3つのリソースについて考える必要があります。

    1つはアプリケーション・サーバー(クライアント)、1つは RDS Proxy(プロキシ)、そしてデータベース・サーバー(サーバー)です。そして Client → Proxy と Proxy → Server の2つの経路があることになります。

    それぞれの関係性や要素について正しく知らないままに運用しても、Proxy 適用後に困った時に対応しづらくなるので、これらを丁寧に取り扱っていきたいところです。


    環境条件と課題

    イメージしやすいように、困っている内容を以下のように仮定します。数値はワザとわかりやすいモノにしています。

  • ECS Fargate + RDS Aurora を利用している
  • 1タスクあたり1クラスタに対し Writer 100, Reader 100 のDB接続が発生する
  • 160 タスクで 16000 接続となり、タスク数が実質的な最初のボトルネックとなる(※他の細かい接続はいったん無視)

  • この時にまず何が困るかと言うと、Autoscaling において max 160 以下が確定するので、それ相当以上のトラフィックが発生した場合に、CPUパワー不足となってユーザーレスポンスに悪影響が出るということです。そして、その時点における即効性があり、かつ簡単な解決策はなく、せいぜいアプリケーション・サーバーの調整で一時凌ぎできるかも程度です。

    他にも、ECS を Blue/Green でデプロイする場合は、一時的にタスク数が倍になり、Blue側の接続が残った状態で Green側にトラフィックが発生するため、DB接続数も倍になります。ということは、デプロイ前の DB 接続数が 8000未満 じゃないとデプロイ不可となります。オーバーしているとおそらく、Greenタスクの一部でエラーが発生し、ヘルスチェックでタスクが落ち、規定数を用意できずにデプロイが失敗するでしょう。

    データベース側の話だと、max_connections まで接続できるとはいえ、接続は少ないほうが使用リソース量を抑えられるので嬉しいでしょう。ただ、昔は厳密に計算していた Thread Buffer ごとに確保されていたメモリ容量は、今は一部は必要容量しか消費しないようになったりと多少は気楽に運用できるはずで、Proxy 適用に関しては DBリソース自体をそこまで助ける要素にはならないかもしれません。

    こういった色々な課題を整理したうえで、では Proxy を採用するとどうなるのか、どういうことを考察するべきなのか、についてまとめていきます。


    接続数の削減

    変化の度合い

    まず最初に、最も重要な接続数の変化についてです。この数値は私が実際に見た変化ではありますが、環境によって度合いは変わると思いますので、あくまで参考値として見てください。

    先程の例のままに、160 タスク 16000 接続が発生する直接DB接続な環境に対し、接続先を RDS Proxy に切り替えて同等のトラフィックを流し込んだ場合、以下のような数値になりました。

  • Client → Proxy : 4000 接続
  • Proxy → DB Server : 1000 ~ 4000 接続
    (対象クラスタや Writer/Reader によって変動)

  • DB max_connections 16000 に対して最大が 4000 程度と 1/4 に減少したということは、そのトラフィックが4倍かつ ECS タスク数が4倍にまで上限が強化されたと仮定することができます。

    実際には、タスク数がそんなに増えるには、FARGATE の CPUリミットを引き上げたり、KVS の接続数などにも気を使う必要が出てくるでしょうし、DBリソースもシンプルにCPU不足でスケールアップが必要になるかもしれません。が、目的であるタスク数のボトルネックは数倍レベルで解消できると判断してよいでしょう。

    アプリケーション・サーバーの設定

    ここで振り返りとして、アプリケーション・サーバーでなぜ元々その多くの接続数が必要なのかを整理します。過去記事としては、この辺が関係しています。

    アプリケーション・サーバーは、ミドルウェアによって単語や適正値が異なるので、この辺はやや柔軟に読んでください。Ruby の Nginx → Puma の worker数は固定だったり、Apache + PHP は動的にできたり。接続数に1:1で関わるモノも worker / process / thread 数と呼び名は変わるでしょう。

    上記過去記事をザックリ説明すると、適切な worker数は、vCPU数・メモリ容量・平均レスポンスタイム あたりをベースにして決定します。そして、できれば CPU 性能を 100% まで稼働できる worker数で、かつメモリ容量が許容内なら少し多めでも OK とします。FARGATE はメモリ容量だけの増加もできるので、CPU基準で考えてメモリは増やして対応できるのが良いところです。

    よく vCPU の何倍程度を目安とするのは、少なくとも vCPU数と同じ数のリクエストを並列で処理しても、ほぼ確実に CPU 100% を使えないからです。DB / KVS / 外部API を利用している間などは、自身の CPU は使わないから、とかそういう理由です。

    worker数が足りないと、どれだけトラフィックを流し込んでも CPU 100% に届かないし、多すぎると CPU 100% を活用はできてもメモリの無駄遣いだったり、DB接続数過多になるので、これを適切な数に設定するのは基本技術であると言いたいところですが、ぼちぼち難しいところでもあります。

    とりあえず今回は適切と判断した worker数を 100 と、わかりやすさのためだけに仮定します。これは各環境によって異なるので、適正値の算出は各々できるようになっておいたほうがよいのですが、RDS Proxy を活用するとそこが多少は雑に多めにして大丈夫ということにもなりえます(後述)。

    実際のデータベース利用度合い

    アプリケーション・サーバーのリソースを最大限活用するために 100 worker で稼働させたとしますが、通常は Autoscaling で稼働させるので、平時は平均CPU使用率は 40~50% を目安に運用することになります。

    つまり、平時は最大でも DB の利用量が、最大稼働を想定した 100 worker の半分以下になるわけです。また、リクエストを受けてからレスポンスを返す一連の処理の中で、DB を最初から最後まで使い続けることはありえなく、少なくとも最初と最後あたりにはスキマ時間が存在するはずです。

    このように実際にDB接続を必要とする量というか時間的な割合を考えた時に、アプリ事情ベースであらかじめ用意しておいた 100 worker 直接接続を常時必要とすることはなく、平均CPU使用率的に半分以下、処理割合的にさらに低い DB 利用時間のはずだということで、そこまで違和感のない 1/4 以下というDB接続数になったことにも辻褄が合うわけです。

    RDS Proxy 経由の恩恵

    DB に直接接続した場合は、アプリケーション・サーバーの性能発揮事情であらかじめ多くの接続が必要になるところ、RDS Proxy 経由に変更すると Proxy は Client が本当に必要な DB の同時利用と処理時間分だけをデータベース接続し、クライアントに貸し出す形になります。

    Proxy はクライアントが同時利用するために必要な最小限のデータベース接続を行い、一定量をプールすることで接続コストを節約し、プールが過多になれば切断するなど、最適化したDB接続と貸し出しのやりくりをしてくれます。

    これが示すことは、アプリケーション・サーバーの worker数が多少増減しようとも、トラフィックが同じなら Proxy → DB へのデータベース接続数は変わらないだろう、ということです。100 worker に対して 100 RPS こようと、200 worker に対して 100 RPS こようと、アプリケーション処理として必要とする DB 利用時間は一定なので、worker数の細かすぎる検証や調整は不要になるはずです。

    簡単にまとめると接続数に関わる強い恩恵としては、

  • 直接接続よりも 1/4 以下のデータベース接続数で運用できる可能性がある
  • アプリケーション・サーバーの worker数調整で、データベース接続数まで綿密に考慮しなくてもよい
  • ECS Blue/Green デプロイにおいて、DB接続数の倍化は起こらなくなる

  • そして気を配るべき項目は RPS といったトラフィック流量が基本となり、調整や運用がよりしやすくなる可能性が高いです。ただ色々なシステムがあると思いますので、問題を出さずにより効果を出すには Client 接続の設定値などから丁寧に見直すことは重要になると思われます。


    Proxyの設定項目

    こちらのページを読んだほうが早い話ではありますが、ザックリと考察しておきます。

    設定の雰囲気はこんな感じです。


    クライアント接続

    IdleClientTimeout は Client → Proxy の接続が、デフォルト 1800秒 アイドル状態になった場合に切断される設定です。長くして実質的にこれによる切断が発生しないようにするか、切断されても次の処理に問題ないようにする必要があります。

    ConnectionBorrowTimeout は Proxy が管理するプール接続に、Client に貸し出す接続が足りなくなった時や、フェイルオーバー中に Client に最大待機させるデフォルト 120秒 の設定です。相当な量のトラフィックにより、(max_connections * MaxConnectionsPercent%) の接続数で足りなくなると待機させられるわけですが、おそらく多くのクライアント事情においては、数十秒以上もDB利用を待たされるくらいなら、数秒で諦めてユーザーにエラーを返したほうが、待ち行列的な現象が起きづらくユーザーストレスもマシなはずなので、調整の余地ありな項目です。

    これらがクライアント接続の設定から見直したほうがよい、と書いた理由で、もし Proxy側から切断された状態であったり、接続を貸し出されず待たされても、正常に処理を継続したり、適度にエラーレスポンスとしてしまう、といった気の使いようは必要だろうといったところです。

    データベース接続

    MaxConnectionsPercent により Proxy → DB へ接続できる最大接続数が (max_connections * MaxConnectionsPercent%) に設定されます。そもそもの max_connections をパラメータグループのインスタンスクラスに連動するデフォルト自動設定値で使うのか、理解したうえで 8000 や 16000 といった任意値にするのか、といったところは今回は省きます。

    メインのアプリケーション・サーバーが Proxy を経由する以外に、内部的に使用される接続や、Proxy 不要で直接接続するリソースもあるでしょうから、100% にするのは避けて少し小さい数値にすることになります。

    MaxIdleConnectionsPercent はプール接続の確保割合で、デフォルトが MaxConnectionsPercent の 50% の値になるので、そのままだと 5分間 使用されない接続は Proxy → DB の最大接続数の半分まで閉じられることになります。

    プロビジョニングされたDBインスタンスの場合、一番の目的は日のピークタイムを捌ききることにあるので、それ以外の時間帯にプール接続を減らして節約することに意味があるかというと……そこまでない気はします。Aurora Serverless はあまり詳しくないのでアレですが、リソース量の伸縮に対して接続数と設定がどう連動するのか次第では、一考の余地がありそうです。


    メトリクス

    メトリクスもドキュメントで十分なのですが、わかりづらい雰囲気があるので適度にまとめておきます。

    クライアント

    ClientConnections が Client → Proxy の接続数、
    ClientConnectionsReceived / ClientConnectionsClosed がその接続リクエストと、閉じられた回数です。

    Client -> Proxy の接続自体は失敗することはあまりないはずですが、この接続発生頻度や維持数が想定通りの程度なのか、変化はどうなのか、を確認することでわかる何かもあると思います。

    レイテンシ

    3つあり、それぞれどこからどこまでの事なのかを知っておきつつ、単発で何かを判断したり、たまには差分で何かが判明することもあるかもしれません。

    DatabaseConnectionsBorrowLatency は Proxy → DB 接続の取得時間、
    QueryDatabaseResponseLatency は Proxy → DB に投げたクエリの応答時間、
    QueryResponseLatency は Client → Proxy → DB に投げたクエリの応答時間、

    このうち DatabaseConnectionsBorrowLatency については後述します。

    クエリ数

    QueryRequests は Client → Proxy に受信されたクエリ数で、1分間ごとの数値なので、60 で割ると QPS になります。

    これはシンプルにトラフィック量を表すものに近いので重要ですが、コレにまつわる Proxy の性能に関しては後述します。

    DB接続数

    DatabaseConnections がデータベース接続数ですが、この周りは他にも種類が多いので、ドキュメントにお任せします。

    MaxDatabaseConnectionsAllowed は許可されるデータベース接続の最大数で、32000 弱だったり 64000弱 だったりとクラスタごとに変わるので、その条件はよくわかりません。インスタンスクラスや、クラスタ内の台数によって変わるのかも?もし、Writer / Reader の環境で、Reader が最大の 15 近くまで必要な環境とかでは、気をつける必要がありそうです。

    ピン留め

    DatabaseConnectionsCurrentlySessionPinned はいわゆる『ピン留め』されている接続数です。

    アプリケーション・サーバーの環境によって、症状や解決方法は異なりそうですが、少なくとも多く発生することは良くない現象なので、特異点が表れる場合は対処する必要があります。


    Proxy の性能上限

    限界の推測

    RDS Proxy のドキュメントの最初の方に、以下のような文言があります。

    RDS Proxy のインフラストラクチャは可用性が高く、複数のアベイラビリティーゾーン (AZ) にデプロイできます。RDS Proxy の計算、メモリ、およびストレージは、Aurora DB クラスターから独立しています。この分離によって、データベースサーバーのオーバーヘッドが減り、そのリソースをデータベースワークロードの処理に集中させることができます。RDS Proxy コンピューティングリソースはサーバーレスであり、データベースのワークロードに基づいて自動的にスケーリングされます。
    RDS Proxy の概念と用語 – Amazon Aurora


    一見、Proxy 自体の性能に心配はなさそうな記述ですが、一定以上のトラフィックを流し込むと、Proxy 要因で遅延障害が発生することは確認済みです。

    これについては公式の記述はないので完全な私見考察になりますが、まず Proxy 要因であると判断した理由は、同程度の QPS が Proxy に流れ込んだ時、DB が 8xlarge なクラスタでは正常なのに、2xlarge なクラスタではかなりの遅延が発生したこと。そして、2xlarge → 8xlarge に変更したら解決したこと、によるものです。(※DBインスタンスのメトリクスには異常がないことは確認済み)

    遅延発生時は、いくつかの Proxyメトリクスが暴れ、その中でも重要と見たモノが DatabaseConnectionsBorrowLatency です。ただのデータベース接続の所要時間が大きくなったことは、よほどの状態であると推測できます。また、各種グラフの特徴から、性能上限的なリソース状態であるだろうと考えました。

    公式では自動的にスケーリングされると謳っているとはいえ、Proxy もコンピューティングであることに変わりはないので CPU やメモリ容量での限界は必ずあるし、そもそも費用形態がクラスタ内のインスタンスの、vCPU数合計に比例する形なので、常識的に考えれば 8vCPU用 と 32vCPU用 で同程度のトラフィックをさばけるようにしてくれているとは思えません。

    限界がくる要素の推測としては、まず高い QPS による CPU使用率、次に接続管理に必要なメモリ容量、あとは可能性は低いけどクエリとレスポンスのデータサイズなどが関与しなくもない、くらい。vCPU数基準な見えない性能上限に到達した場合、接続の作成や維持が困難になる、というのは何も不思議ではありません。

    不都合なこと (1) インスタンスクラス

    DBクラスタの条件によって Proxy に性能変化があるのはかまわないのですが、若干不都合になる運用状況が考えられます。

    仮に Proxy がただ通過するだけの QPS ベースに性能上限があるとしたら、DBインスタンスの方は QPS ベースにクエリ内容+データ性質によって処理負荷が変動することに痛い差分が生まれる可能性があります。

    DBのクエリ負荷が平均的に超軽い場合、DBインスタンスでは非常に多くの QPS を処理できることになり、DB より Proxy の方が先に音を上げる可能性があるということです。その場合、Proxy の性能を確保するために DB のインスタンスクラスをスケールアップすることになり、Proxy の恩恵は重要であるとしても、DB リソース・費用に無駄が発生するという見方もできます。

    クエリが平均的に重い環境なら、DB の方が CPU使用率で先に音を上げ、スケールアップが必要になって Proxy も結果的に強くなるので問題はないでしょう。

    不都合なこと (2) レプリカ数

    Writer と Reader の利用ができている環境で、かつ参照分散の割合が非常に高いサービスの場合、トラフィックが増えても Reader を増やし続ければ対応できるとします。

    Reader を増やす時はインスタンスクラスそのままに台数だけ増え、その増加するトラフィックは全て Proxy を通るので、QPS ベースでは Proxy がどこかで厳しいことになるかもしれません。

    とはいえ vCPU数ベースの従量課金なので、インスタンスが増える分だけ強くなるかもしれませんし、もし強くなる区切りの幅が広ければ、それに到達する前に Proxy が先に音を上げる、ということも邪推できます。

    Aurora のパラメータグループ

    超昔に調べた Amazon Auroraを始めるためのパラメータ資料 | 外道父の匠 では、デフォルトの設定では max_connections がインスタンスクラスに合わせて増えるものの、比例するわけではないことがわかっています。

    AWS 側として、インスタンスクラスの大きさに対してはこんなもんだろう、という基準はあるわけで、Proxy もそれに合わせる形でスケーリングの幅を決定していてもおかしくはありません。

    ただこれは確実に公開しないし、聞いても回答してもらえない類の情報なので、下手するとここの考察に限っては陰謀論レベルに駄弁ってるだけな可能性もあります。

    とはいえ、どの条件下でも無制限にスケーリングされて正常処理されるでしょ、って考える方が無謀だとも思うので、検証する際には正常な条件だけではなく、異常が発生する条件も発見して、その境を認識しておくというのが無難だと考えます。


    デメリット

    元々判明しているデメリットについて、少しだけ整理しておきます。

    レイテンシ

    DB 直接に対して Proxy 経由になると、余計な通信経路が増える分、クエリあたりにレイテンシが増加します。クエリあたり何%遅くなるのではなく、1クエリあたり何ms 遅くなる、というイメージです。

    そのため、もし1つのユーザーリクエストに対する処理の中に 100 クエリあったとして、1クエリあたり 5ms 遅くなるとしたら、合計で 500ms 遅くなるので、結構致命傷と言えかける数値になります。

    新しく作られたホヤホヤのサービスや機能だと、わりと雑にループでクエリを発行していたりしますが、可能な限り BULK化してクエリ数を減らしたり、キャッシュを駆使したり無駄を探してクエリ数を減らす、といった改善がより効果的になる、というか必須な改善努力ということになります。

    費用

    費用のページはこれらです。

    Aurora のプロビジョニングインスタンスの r7g ベースで計算すると、インスタンスに対して以下のような増加費用となります。
    • オンデマンド:+10.8%
    • リザーブド:+14.1%

    ザックリとは1割増以上になるということで、その費用感に対して十分な効能と感じるかどうかは、レイテンシいうデメリットと合わせると世間一般的には正直微妙なところです。最大接続数のように、どうしても解決したい課題でなければ、そこまで関心を持つ必要はないかもしれません。

    フェイルオーバーが早くなるというメリットも、その恩恵は年に1回あるかないかなので、それをメインの目的に考えるのもちょっと違う感じがしますしね。


    おわりに

    高負荷対策のインフラ設計の超基本的なところとしては、以下の要素があります。
    • アプリケーション・サーバーの Autoscaling
    • DB/KVS の垂直/水平分割、参照分散
    • DB/KVS のスケールアップ

    15年以上前とかだと、圧倒的にデータベースがボトルネックになっていました。それ以降はクラウドで任意の大きなインスタンスサイズを選べつつも、それでもまだ一番手はデータベースにあったと思われます。

    しかし最近はいろんなデータベース形態が登場したり、インスタンスクラスは r7i や r8g では 48xlarge まで登場するといったことで、データベースがよりボトルネックになりづらくなっている傾向があります(※それに甘えて改善を怠るのは違うけど)。

    DB の分割を上手に設計できて、スケールアップにも余念がないとなれば、次はアプリケーション・サーバー量が要因になりやすくなったんじゃね?って話で、今回はその解決手段の1つである RDS Proxy について整理してみたわけです。


    大きなトラフィックを捌こうとする時、何かを導入すれば全てをそのままに解決できるってことは、そうないはずです。

    多少のレイテンシを犠牲にしたり、通常ではやらないようなリファクタリングをしたり、同期/非同期処理を駆使してユーザーストレスを軽減したり、といった何かしらのトレードオフというか努力や創意工夫が必要になるという意識でエンジニアリングしていきたいところです。


    年末最後に重苦しめな〆となってしまいましたが、今年は若手育成志向でまぁまぁ真面目に良い記事を残せたような気がします。

    来年も自身・読者ともに楽しくエンジニアリングできるようなブログを書いていきたいと思いますので、よろしくお願いしゃす:-)