
Lambda Managed Instancesを利用する場合はRDS Proxyが不要になる?実行モデルの違いによるコネクションプーリングの考え方について検証してみた #AWSreInvent
リテールアプリ共創部@大阪の岩田です。
Lambda Managed Instancesを利用するLambda Functionは通常のLambda Functionsとは同時実行に関するモデルが異なります。このモデルの違いがコネクションプーリングの利用にどのような影響を与えるのかを考察・検証してみました。
環境
今回検証に利用した環境です。
- Lambdaのランタイム: Node.js 24.x
- pg: 8.16.3
- PgBouncer: 1.25.1
- Aurora: Serverless v2 PostgreSQL17.4互換
- Lambda Managed InstancesのEC2インスタンスタイプ: m8g.large
Lambdaの同時実行モデルとコネクションプーリングの関係
通常のLambdaでは、1つのLambda実行環境が同時に処理できるリクエストは1つまでで、2つ以上のリクエストを同時に処理することはありません。この特性から、アプリケーションレイヤでコネクションプーリングを利用しても複数のリクエストでDB接続を共有できません。

よく「Lambdaはコネクションプーリングが使えない」とか「Lambdaでコネクションプーリングを使う意味はない」とか言われていました。この課題を解決するにはPgpool-IIやPgBouncerのようなミドルウェアレイヤーでのコネクションプーリングが必要であり、それをマネージドサービスとして提供してくれるのがRDS Proxyです。Developers.IO 2020の登壇でこの辺りの詳細やRDS Proxyについて解説しているので良ければご一読ください。
これに対してLambda Managed Instancesを利用する場合は1つのLambda実行環境が同時に複数のリクエストを処理することになります。

このモデルの違いからコネクションプーリングに関する考え方も変わってきます。Invoke 中のLambda Functionのインスタンス間の境界にはVMのレイヤーが存在しないため、VM内にコネクションプーリングのコンポーネントを配置すればLambda Functionのインスタンス間でコネクションを共有できるはずです。

この仮説を検証してみましょう。
やってみる
実際にLambda実行環境内でPgBouncerを起動してPgBouncer経由でAuroraに接続する処理を実行し、通常のLambda実行環境とLambda Managed Instances利用時でDBコネクションの消費量がどのように変化するか確認していきます。
検証用コード
検証に利用したのは以下のコードです。
import { Client } from 'pg'
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
export const handler = async (event) => {
const client = new Client({
host: '127.0.0.1',
port: 5432,
database: 'aurora',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
})
await client.connect();
await client.query('select now()');
await sleep(5000);
await client.query('select now()');
await client.end();
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
処理の概要は以下の通りです。
- Lambda Extensionを利用してデプロイしたPgBouncerに接続
- クエリ発行
- 5秒スリープ処理
- クエリ発行
- PgBouncerから切断
約5秒弱PgBouncerに接続する形になりますが5秒間はDBアクセスを行わないため、うまくPgBouncerにAuroraとの実際のコネクションを管理してもらってAuroraへの同時接続数を抑えたいところです。このコードを後ほど作成するPgBouncerのExtensionと合わせてデプロイし、通常のLambda実行環境・Managed Instancesの実行環境それぞれでAuroraへの同時接続数がどのように変化するか確認します。
PgBouncerのLambda Extensionを作成
それではLambda実行環境内でPgBouncerを起動するためのExtensionを作成していきましょう。
まずbuild.shというファイル名で以下のシェルスクリプトを用意します。
#!bin/bash
dnf install -y tar gcc gzip postgresql17-devel libtool libevent-devel
cd /tmp
curl -O --location https://github.com/jgm/pandoc/releases/download/3.8.3/pandoc-3.8.3-linux-arm64.tar.gz
tar zxvf pandoc-3.8.3-linux-arm64.tar.gz --strip-components 1 -C /usr/local
curl -O --location https://www.pgbouncer.org/downloads/files/1.25.1/pgbouncer-1.25.1.tar.gz
tar -xvzf pgbouncer-1.25.1.tar.gz
cd pgbouncer-1.25.1
./autogen.sh
./configure --prefix=/opt/
make
make install
mkdir /opt/lib
cp /usr/lib64/libevent* /opt/lib/
このシェルスクリプトをAmazon Linux 2023のコンテナイメージ内で実行し、ビルドしたPgBouncerのバイナリと依存ライブラリを抽出します。
docker run --rm -v $(PWD):/opt/ amazonlinux:2023 /opt/build.sh
シェルスクリプトの実行が完了するとカレントディレクトリ配下は以下のようになります。
.
├── bin
│ └── pgbouncer
├── build.sh
├── lib
│ ├── libevent_core-2.1.so.7
│ ├── libevent_core-2.1.so.7.0.1
│ ├── libevent_core.so
│ ├── libevent_extra-2.1.so.7
│ ├── libevent_extra-2.1.so.7.0.1
│ ├── libevent_extra.so
│ ├── libevent_openssl-2.1.so.7
│ ├── libevent_openssl-2.1.so.7.0.1
│ ├── libevent_openssl.so
│ ├── libevent_pthreads-2.1.so.7
│ ├── libevent_pthreads-2.1.so.7.0.1
│ ├── libevent_pthreads.so
│ ├── libevent-2.1.so.7
│ ├── libevent-2.1.so.7.0.1
│ └── libevent.so
└── share
├── doc
│ └── pgbouncer
│ ├── NEWS.md
│ ├── pgbouncer-minimal.ini
│ ├── pgbouncer.ini
│ ├── pgbouncer.service
│ ├── pgbouncer.socket
│ ├── README.md
│ └── userlist.txt
└── man
├── man1
│ └── pgbouncer.1
└── man5
└── pgbouncer.5
続いてPgBouncerを起動するシェルスクリプトを作成します。
mkdir extensions
touch extensions/pgbouncer-extension.sh
スクリプトの本体は以下の通りです。GitHubで公開されているLambda Extensionのサンプルを参考にしつつ、今回は検証用途なのでシグナルの処理などは割愛しました。
#!/bin/bash
set -euo pipefail
OWN_FILENAME="$(basename $0)"
LAMBDA_EXTENSION_NAME="$OWN_FILENAME" # (external) extension name has to match the filename
TMPFILE=/tmp/$OWN_FILENAME
echo "[extension:bash] launching ${LAMBDA_EXTENSION_NAME}"
cat << EOF > "/tmp/pgbouncer.ini"
[databases]
aurora = host=${DB_HOST} dbname=postgres
[pgbouncer]
logfile = /tmp/pgbouncer.log
pidfile = /tmp/pgbouncer.pid
listen_addr = 127.0.0.1
listen_port = 5432
auth_type = trust
auth_file = /tmp/userlist.txt
pool_mode = transaction
server_reset_query = DISCARD ALL
max_client_conn = 1000
EOF
cat << EOF > "/tmp/userlist.txt"
"${DB_USER}" "${DB_PASSWORD}"
EOF
/opt/bin/pgbouncer /tmp/pgbouncer.ini -d
HEADERS="$(mktemp)"
echo "[${LAMBDA_EXTENSION_NAME}] Registering at http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/register"
curl -sS -LD "$HEADERS" -XPOST "http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/register" --header "Lambda-Extension-Name: ${LAMBDA_EXTENSION_NAME}" -d "{}" > $TMPFILE
RESPONSE=$(<$TMPFILE)
HEADINFO=$(<$HEADERS)
EXTENSION_ID=$(grep -Fi Lambda-Extension-Identifier "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)
echo "[${LAMBDA_EXTENSION_NAME}] Registration response: ${RESPONSE} with EXTENSION_ID ${EXTENSION_ID}"
curl -sS -L -XGET "http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/event/next" --header "Lambda-Extension-Identifier: ${EXTENSION_ID}"
やってることはざっくり以下の通りです。
- PgBouncerの設定ファイル作成
- PgBouncerの認証情報ファイル作成
- PgBouncerのプロセスを起動
- Lambda Extensionの登録
順を追ってみていきましょう。まずはPgBouncerの設定ファイル作成です。
cat << EOF > "/tmp/pgbouncer.ini"
[databases]
aurora = host=${DB_HOST} dbname=postgres
[pgbouncer]
logfile = /tmp/pgbouncer.log
pidfile = /tmp/pgbouncer.pid
listen_addr = 127.0.0.1
listen_port = 5432
auth_type = trust
auth_file = /tmp/userlist.txt
pool_mode = transaction
server_reset_query = DISCARD ALL
max_client_conn = 1000
EOF
上記のコードで/tmp/pgbouncer.iniに設定ファイルを作成します。バックエンドDBのURLは環境変数DB_HOSTから展開するようにしています。
続いて認証情報ファイルの作成です。
cat << EOF > "/tmp/userlist.txt"
"${DB_USER}" "${DB_PASSWORD}"
EOF
環境変数DB_USERとDB_PASSWORDを展開し、/tmp/userlist.txtに認証情報ファイルを作成します。
続いて作成した設定ファイルを指定してPgBouncerをバックグラウンドで起動
/opt/bin/pgbouncer /tmp/pgbouncer.ini -d
最後にLambda ExtensionのAPIを呼び出してExtensionの登録と初期化完了をランタイムに通知します。
HEADERS="$(mktemp)"
echo "[${LAMBDA_EXTENSION_NAME}] Registering at http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/register"
curl -sS -LD "$HEADERS" -XPOST "http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/register" --header "Lambda-Extension-Name: ${LAMBDA_EXTENSION_NAME}" -d "{}" > $TMPFILE
RESPONSE=$(<$TMPFILE)
HEADINFO=$(<$HEADERS)
EXTENSION_ID=$(grep -Fi Lambda-Extension-Identifier "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)
echo "[${LAMBDA_EXTENSION_NAME}] Registration response: ${RESPONSE} with EXTENSION_ID ${EXTENSION_ID}"
curl -sS -L -XGET "http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/event/next" --header "Lambda-Extension-Identifier: ${EXTENSION_ID}"
今回は検証目的なのでRegister APIのボディでeventsは指定していません。本来はSHUTDOWNを指定し、シェルスクリプト側でSIGTERMをハンドリングすべきなのでご注意ください。ちなみにLambda Managed Instanceを利用した場合はeventsにINVOKEは指定できず、SHUTDOWNのみ指定可能です。
Lambda Managed Instances: Extensions for Lambda Managed Instances functions cannot register for the
Invokeevent. Because Lambda Managed Instances supports concurrent invocations within a single execution environment, theInvokeevent is not supported. Extensions can only register for theShutdownevent.
シェルスクリプトが作成できたら先程のPgBouncer関連ファイルとまとめてZIPファイルにパッケージングし、Lambda Layerとして登録します。
Lambda Layerがデプロイできたら検証用コードを実行するLambda Functionを作成してLambda Layerを紐づけ、環境変数に以下の情報を設定しておきましょう。
- DB_USER: DBユーザー名
- DB_PASSWORD: DBユーザーのパスワード
- DB_HOST: DBのエンドポイント
これで検証準備完了です。
通常のLambda実行環境の場合
まず通常のLambda実行環境でDBコネクションの消費量を確認してみます。今回は検証用のLambda FunctionをAPI GWのバックに配置し、abコマンドを利用して同時接続数100,総リクエスト数1,000回でリクエストを発行してみます。
ab -c 100 -n 1000 https://<API GWのエンドポイント>
実行結果です。
This is ApacheBench, Version 2.3 <$Revision: 1923142 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking <API GWのID>.execute-api.us-east-1.amazonaws.com (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software:
Server Hostname: <API GWのID>.execute-api.us-east-1.amazonaws.com
Server Port: 443
SSL/TLS Protocol: TLSv1.3,TLS_AES_128_GCM_SHA256,2048,128
Server Temp Key: X25519 253 bits
TLS Server Name: <API GWのID>.execute-api.us-east-1.amazonaws.com
Document Path: /
Document Length: 20 bytes
Concurrency Level: 100
Time taken for tests: 56.443 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 191000 bytes
HTML transferred: 20000 bytes
Requests per second: 17.72 [#/sec] (mean)
Time per request: 5644.341 [ms] (mean)
Time per request: 56.443 [ms] (mean, across all concurrent requests)
Transfer rate: 3.30 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 3 9 13.5 4 85
Processing: 5020 5107 220.4 5034 6016
Waiting: 5020 5107 220.4 5034 6016
Total: 5025 5116 231.5 5038 6101
Percentage of the requests served within a certain time (ms)
50% 5038
66% 5043
75% 5047
80% 5052
90% 5104
95% 5790
98% 5892
99% 5938
100% 6101 (longest request)
テスト実行時のメトリクスは以下のようになりました。

Lambdaの同時実行数,DBコネクション共に最大100に到達していることが分かります。PgBouncerを使ってLambda実行環境内にコネクションプールを作成しているものの、複数のリクエストを跨いでコネクションプールが共有できないため、Lambdaの同時実行数とDBコネクション数がイコールになっています。これではコネクションプールが有効活用できているとは言えず、無駄にオーバーヘッドを発生させているだけです。
Lambda Managed Instancesの場合
続いてLambda Managed Instancesを利用する構成でも検証コードをデプロイし、先ほどと同一のabコマンドを実行してみました。実行結果は以下の通りです。
This is ApacheBench, Version 2.3 <$Revision: 1923142 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking <API GWのID>.execute-api.us-east-1.amazonaws.com (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software:
Server Hostname: <API GWのID>.execute-api.us-east-1.amazonaws.com
Server Port: 443
SSL/TLS Protocol: TLSv1.3,TLS_AES_128_GCM_SHA256,2048,128
Server Temp Key: X25519 253 bits
TLS Server Name: <API GWのID>.execute-api.us-east-1.amazonaws.com
Document Path: /
Document Length: 20 bytes
Concurrency Level: 100
Time taken for tests: 55.676 seconds
Complete requests: 1000
Failed requests: 46
(Connect: 0, Receive: 0, Length: 46, Exceptions: 0)
Non-2xx responses: 46
Total transferred: 191966 bytes
HTML transferred: 20598 bytes
Requests per second: 17.96 [#/sec] (mean)
Time per request: 5567.555 [ms] (mean)
Time per request: 55.676 [ms] (mean, across all concurrent requests)
Transfer rate: 3.37 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 2 7 10.9 4 60
Processing: 30 4817 1047.2 5037 5206
Waiting: 30 4816 1047.4 5036 5206
Total: 77 4824 1040.5 5041 5229
Percentage of the requests served within a certain time (ms)
50% 5041
66% 5046
75% 5051
80% 5056
90% 5080
95% 5139
98% 5170
99% 5202
100% 5229 (longest request)
テスト実行時のメトリクスは以下の通りでした。

キャパシティプロバイダーの設定はデフォルト値を利用したため、Lambda実行環境毎の最大同時実行数は64となっています。そのため、メトリクスのExecutionEnvironmentConcurrencyLimitは64となっており、それに合わせてExecutionEnvironmentConcurrencyも64で頭打ちとなっていました。 ※条件を合わせるために100に調整しておけば良かったと後から気付きました。
DBコネクションの最大値はLambdaの同時実行数の約3分の1である22に抑えられており、通常のLambda実行環境利用時と比べると効率よくDBコネクションが共有できていることが分かります。
想像通りLambda Managed Instancesの場合はPgBouncer等を利用してLambda実行環境内にコネクションプールを配置することでDBコネクションを効率よく扱えることが分かりました!
※今回実行したクエリはselect now()だけなので、DBコネクション数はもうちょっと低い値になるかと予想したのですが今回は最大22という結果になりました。
まとめ
Lambda Managed Instances利用時はコネクションプーリングについても通常のLambdaとは考え方が変わってくることを検証してみました。複数のリクエストが同一のLambda実行環境で処理されるということはLambda Extensionで起動したプロセスも複数のリクエストを跨いで共有利用できるということです。
この特性を活かすとコネクションプーリング以外にも色々とExtensionの応用的な使い方が可能になるのではないでしょうか?可能性を研究してみるのもなかなか面白そうです。








