Lambda から RDS につなぐ構成で、RDS Proxy を挟むんだ方がよいのかいらない場合があるのか理解できていなかった。
「サーバーレスなら必須」と書いてある記事もあるし、「小規模ならいらない」と書いてある記事もある。
そこで、両者がそれぞれ何をしているのか、どこで必要になってどこで不要なのか、コストがいくら乗るのかを整理します。
Lambda は接続を使い捨てる
まず Lambda 側の事情から。
Lambda の実行環境は、同じ環境が再利用されるあいだはハンドラの外に張った接続を使い回せる。だから「ハンドラの外で接続する」のは今も有効なテクニックです。
import os
import pymysql
# ハンドラの外で接続する。同じ実行環境が再利用されるあいだは使い回せる
connection = pymysql.connect(
host=os.environ["DB_HOST"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
database=os.environ["DB_NAME"],
connect_timeout=5,
)
def handler(event, context):
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
return cursor.fetchone()
問題は同時実行のほう。Lambda は同時に来たリクエストの数だけ実行環境を並べる。実行環境が 100 個立てば、DB 接続も 100 本になる。ECS や EC2 のように「1 プロセスの中でコネクションプールを共有する」という逃げ道がない。AWS 自身もこう書いています。
As Lambda functions share nothing between the runtime environments, unlike containers they can't rely on connection pools when connecting to a relational database. For this reason, we created Amazon RDS Proxy
(Developing portable AWS Lambda functions)
Lambda 単体では、同時実行数がそのまま DB への接続数になる。ここが出発点です。
RDS Proxy は DB の前に立つ受付係
すごく雑に言うと、DB の前に立って接続を取りまとめる受付係。
Lambda は DB ではなく Proxy につなぐ。Proxy は自分と DB のあいだに温めた接続の束(プール)を持っていて、トランザクションが終わるたびにその接続を別のクライアントへ回す。これを多重化(multiplexing)と呼ぶ。Lambda が 100 並列で来ても、DB 側の実接続は数十本で足りるという仕組みです。
接続の取りまとめ以外に効くところが 2 つあります。
ひとつはフェイルオーバー。Proxy が新しいライターを検知して裏でつなぎ替えるので、アプリから見た切断が減る。AWS はフェイルオーバー時間を最大 66% 短縮すると書いている。もうひとつは認証で、Secrets Manager と IAM 認証を Proxy 側で受けられるため、Lambda のコードから DB のパスワードを消せます。
注意点もひとつ。Proxy は DB と同じ VPC に置く必要があって、パブリックには公開できない。手元の PC から直接叩いて試せないので、検証は Lambda 経由か踏み台越しになります。
接続数の壁は意外と低い
「100 並列くらいなら大丈夫だろう」と思いがちだけど、小さいインスタンスの上限は本当に低い。
Aurora MySQL は max_connections のデフォルトがインスタンスクラスごとに決まっています。
| インスタンスクラス | max_connections のデフォルト |
|---|---|
| db.t3.medium / db.t4g.medium | 90 |
| db.t3.large / db.t4g.large | 135 |
| db.r5.large | 1000 |
| db.r5.xlarge | 2000 |
RDS for MySQL のほうは式で決まって、 {DBInstanceClassMemory/12582880} 、ざっくりメモリ MB ÷ 12。1 GiB なら 85 本くらいになります。
db.t4g.medium に Lambda を 100 並列でぶつければ、上限 90 をあっさり超える。しかも AWS は接続数に 30% の余裕を持たせることを推奨しているので、安心して使えるのは 60 本あたり。
判断の目安はここ。想定する Lambda の同時実行数が max_connections の 7 割を超えるなら、Proxy を入れるか同時実行数を絞るか、どちらかが必要になります。
入れなきゃいけないパターン
一番分かりやすいのは、いまの接続数の壁に当たるケース。同時実行が数百まで伸びる API や、朝の同期処理で一気にスパイクするバッチは、Proxy なしだと Too many connections で落ちる。厄介なのは、落ちるのが一部のリクエストだけなので、原因が接続数だと気づくまでに時間がかかるところ。
次に、フェイルオーバー中のエラーを許容できないケース。Multi-AZ は 1 分程度で切り替わるけれど、切り替えのあいだに走った Lambda は普通に失敗する。リトライで拾えない処理があるなら、つなぎ替えを Proxy に任せたほうが素直です。
3 つ目は、1 回の処理が短いのに呼び出し回数が多いケース。接続のハンドシェイクは TLS を使うとそれなりに重くて、これを DB の CPU で処理するのはもったいない。AWS のドキュメントも、この負荷を比較的安い Proxy 層へ逃がす、という整理をしています。
入れなくていいパターン
小規模で、同時実行数を自分で押さえられるなら要らない。Lambda の予約済み同時実行数(reserved concurrency)を 20 に設定すれば、DB 接続も最大 20 本で頭打ちになる。db.t4g.medium でも余裕で収まります。
aws lambda put-function-concurrency \
--function-name my-function \
--reserved-concurrent-executions 20
この設定は追加料金なしで効くので、まずこれで足りないかを考えたい。
Aurora なら Data API という選択肢もある。HTTP で SQL を投げる仕組みなので、接続という概念そのものが消える。以前は Aurora Serverless v1 専用でしたが、いまは Serverless v2 とプロビジョンドクラスターの両方で使えます。料金はリクエスト課金(最初の 10 億リクエストまで 100 万件あたり 0.42 USD)なので、リクエストが少ないうちはほぼタダ。
夜間バッチのような低頻度・低並列の処理も、素直に直接つないだほうが安い。Proxy には「停止」がなく、1 日 5 分しか使わなくても 24 時間ぶん課金されるからです。
コストは「最低料金」で決まる
ここが一番ハマりやすい。RDS Proxy の料金は使った量ではなく、後ろにいる DB のサイズで決まります。
プロビジョンドインスタンスなら vCPU 1 個あたり 1 時間 0.015 USD(バージニア北部)。効いてくるのは、最低 2 vCPU 分が課金されるという条件。最小構成でも 0.03 USD/時、月 730 時間で約 22 USD になります。
Aurora Serverless v2 は消費した ACU あたりの課金ですが、最低 8 ACU 分が課金される。DB が 0.5 ACU でアイドルしていても、Proxy は 8 ACU 分を請求してきます。
| 構成 | RDS Proxy の最低コスト(バージニア北部・月 730 時間) |
|---|---|
| 2 vCPU のプロビジョンド(db.t4g.medium など) | 約 22 USD |
| 4 vCPU のプロビジョンド | 約 44 USD |
| Aurora Serverless v2(0.5 ACU で運用) | 約 88 USD(8 ACU 分の最低料金) |
東京リージョンは単価が上がるので、Serverless v2 で月 140 USD を超えたという報告もあります(Aurora Serverless v2+RDS Proxy の最低料金は 8ACU 分です)。「Serverless v2 で安く抑えるつもりだったのに、Proxy のほうが DB より高い」という逆転が起きるのは、この最低料金のせい。開発環境やステージングに何も考えず付けると、環境の数だけ掛け算で効いてきます。
デフォルトエンドポイントは無料ですが、読み取り専用エンドポイントなどを追加すると PrivateLink のインターフェイスエンドポイント料金が別に乗る点も頭に入れておきたい。最新の単価は公式の料金ページで確認してください。
入れたのに効かないパターン
Proxy を入れれば必ず接続が減る、というわけでもない。接続がピン留め(pinning)されると多重化が止まります。
ピン留めは、Proxy が「このセッションの状態は他のセッションへ使い回せない」と判断したときに起きる。SET でセッション変数を変えた、といった操作が引き金になる。ピン留めされたクライアント接続は、セッションが切れるまで同じ DB 接続を占有し続ける。ほとんどの接続がピン留めされていたら、Proxy を挟んだ意味はかなり薄れます。
導入したら CloudWatch の DatabaseConnectionsCurrentlySessionPinned を必ず見る。ここが高止まりしていたら、アプリ側の SET 文を Proxy の初期化クエリへ寄せるといった調整が必要になります。
結局どこで判断するか
自分用にフローへ落としました。
小さい環境なら予約済み同時実行数を絞るだけで足りるし、Aurora が使えるなら Data API のほうが安く済む場合もある。逆に本番でスパイクする API なら、月 22 USD は保険として安い。

