Connection Pooling di Distributed System
Connection pooling adalah konsep yang dipahami dengan baik di dunia monolith: buat pool saat startup, pinjam koneksi saat ada request, kembalikan ke pool setelah selesai, atur batas maksimumnya, dan sistem berjalan stabil. Masalah muncul ketika pemahaman yang sama dibawa masuk ke distributed system tanpa pertanyaan. Di sana, hampir semua asumsi yang mendasari model klasik itu runtuh — dan runtuhnya bukan perlahan, tapi tiba-tiba, di production, saat traffic naik. Artikel ini membahas mengapa model klasik gagal secara mendasar di distributed system, bagaimana connection exhaustion terjadi di Lambda, dan bagaimana RDS Proxy — serta alternatif lainnya — menggeser tanggung jawab pooling ke lapisan yang tepat.
Asumsi yang Runtuh di Distributed System
Connection pool klasik bekerja karena satu asumsi mendasar yang hampir tidak pernah disebutkan secara eksplisit: jumlah instance aplikasi yang mengakses database adalah angka yang kecil dan terprediksi.
Di monolith dengan 3 instance di belakang load balancer, kamu bisa menghitung dengan tepat berapa koneksi maksimum yang akan dibuka ke database: 3 instance × 20 koneksi per pool = 60 koneksi. Angka ini tidak akan berubah kecuali kamu menambah instance secara manual. Ini membuat konfigurasi max_connections di PostgreSQL atau MySQL menjadi mudah dan deterministik.
Di distributed system, terutama yang serverless, semua angka ini berubah menjadi variabel yang bisa berfluktuasi dalam hitungan detik.
flowchart TD
subgraph Monolith["Monolith — Terprediksi"]
APP1[Instance 1\nPool: 20 conn] --> DB1[(Database\nmax_conn: 100)]
APP2[Instance 2\nPool: 20 conn] --> DB1
APP3[Instance 3\nPool: 20 conn] --> DB1
NOTE1["Total koneksi: 60\nDeterministik, aman"]
end
subgraph Serverless["Lambda — Tidak Terprediksi"]
L1[Lambda #1\nPool: 5 conn] --> DB2[(Database\nmax_conn: 100)]
L2[Lambda #2\nPool: 5 conn] --> DB2
L3[Lambda #3\nPool: 5 conn] --> DB2
LDOT["... hingga\n500 instance"] --> DB2
NOTE2["Total koneksi: bisa 2.500\nConnection exhaustion!"]
end
style NOTE2 fill:#ffebee,stroke:#e53935
style NOTE1 fill:#e8f5e9,stroke:#43a047Ini bukan skenario edge case. Ini adalah perilaku normal Lambda saat menghadapi traffic spike — dan inilah mengapa connection pooling di level aplikasi tidak cukup di arsitektur serverless.
Anatomi Connection Exhaustion di Lambda
Untuk memahami masalahnya dengan tepat, perlu dipahami dulu bagaimana Lambda mengelola instance dan koneksi database.
Lifecycle Lambda Instance
Lambda tidak menjalankan semua invocation di satu proses. Setiap concurrent execution membutuhkan instance tersendiri, dan setiap instance memiliki runtime yang sepenuhnya terisolasi — termasuk memory, variabel global, dan koneksi database.
stateDiagram-v2
[*] --> ColdStart: Invocation pertama\natau kapasitas habis
ColdStart --> WarmInit: Container dibuat\nRuntime diinisialisasi
WarmInit --> Running: Handler dieksekusi\nKoneksi DB dibuka
Running --> Idle: Handler selesai\nKoneksi tetap terbuka di pool
Idle --> Running: Invocation berikutnya\n(warm start, reuse koneksi)
Idle --> Terminated: Idle terlalu lama\natau Lambda reclaim
note right of Running: Setiap concurrent execution\nmembutuhkan instance berbeda
note right of Idle: Koneksi di pool tetap hidup\nselama instance idleYang krusial dari diagram ini: koneksi database dibuka saat cold start dan tetap terbuka selama instance masih hidup — bahkan saat tidak ada handler yang berjalan. Ini adalah perilaku yang diinginkan di monolith (reuse koneksi = efisien), tapi di Lambda ia menciptakan masalah: ratusan instance idle yang masing-masing memegang koneksi ke database.
Kronologi Connection Exhaustion
Berikut skenario yang sangat umum terjadi di production:
sequenceDiagram
participant Traffic as Traffic (spike)
participant Lambda as Lambda Instances
participant DB as PostgreSQL (max 100 conn)
Note over DB: max_connections = 100
Traffic->>Lambda: 10 concurrent requests
Lambda->>DB: 10 instance × 3 conn = 30 koneksi
Note over DB: 30/100 koneksi terpakai — aman
Traffic->>Lambda: Traffic naik 5x
Lambda->>DB: 50 instance × 3 conn = 150 koneksi
Note over DB: 100/100 koneksi — database penuh!
Lambda->>DB: Instance #51 mencoba connect
DB-->>Lambda: FATAL: sorry, too many clients already
Lambda-->>Traffic: HTTP 500 — Lambda timeout
Note over Lambda,DB: Instance baru terus dibuat\nTapi tidak bisa connect\nSemua request gagalYang membuat situasi ini bertambah buruk: Lambda yang gagal connect biasanya langsung retry, yang justru menambah tekanan ke database yang sudah kewalahan. Ini adalah positive feedback loop menuju kegagalan total.
Mengapa Pool di Level Kode Tidak Membantu
Reaksi pertama banyak engineer ketika menghadapi masalah ini adalah mengurangi MaxOpenConns di level kode. Ini memang mengurangi koneksi per instance — tapi tidak menyelesaikan masalah fundamentalnya.
// ANTI-PATTERN: menganggap ini cukup untuk Lambda
sqlDB.SetMaxOpenConns(5) // 5 koneksi per instance
// Dengan 200 concurrent Lambda: 200 × 5 = 1000 koneksi ke DB
// Database tetap exhausted
// Setengah solusi: kurangi drastis
sqlDB.SetMaxOpenConns(1) // 1 koneksi per instance
// Dengan 200 concurrent Lambda: masih 200 koneksi ke DB
// Lebih baik, tapi tetap tidak terkontrol dan tidak bisa di-cap
Tidak ada nilai MaxOpenConns di level Lambda yang bisa memberikan batas atas yang pasti untuk koneksi ke database — karena kamu tidak bisa mengontrol berapa banyak Lambda instance yang dibuat oleh AWS.
Mengurangi MaxOpenConns di Lambda adalah optimasi yang benar tapi tidak mencukupi. Ia hanya mengurangi keparahan masalah, bukan menghilangkannya. Selama Lambda bisa scale ke ratusan instance, tidak ada setting pool di level kode yang bisa memberikan perlindungan yang sesungguhnya terhadap connection exhaustion.RDS Proxy: Menggeser Pooling ke Infrastruktur
Solusi yang tepat bukan memperbaiki pooling di level aplikasi, tapi memindahkan pooling ke lapisan yang berada di antara aplikasi dan database — lapisan yang bisa melihat dan mengontrol semua koneksi secara terpusat, terlepas dari berapa banyak instance aplikasi yang ada.
RDS Proxy adalah implementasi resmi AWS untuk pola ini.
Cara Kerja RDS Proxy
sequenceDiagram
participant L1 as Lambda Instance 1
participant L2 as Lambda Instance 2
participant L3 as Lambda Instance N
participant PROXY as RDS Proxy\n(Shared Pool)
participant RDS as RDS Database
L1->>PROXY: Connect (koneksi ringan)
L2->>PROXY: Connect (koneksi ringan)
L3->>PROXY: Connect (koneksi ringan)
Note over PROXY: Proxy memiliki pool koneksi\nke RDS yang terpusat dan terbatas
PROXY->>RDS: Hanya membuka koneksi\nsesuai yang dibutuhkan\n(bukan per Lambda instance)
L1->>PROXY: Query: SELECT * FROM orders
PROXY->>RDS: Forward via koneksi yang sudah ada di pool
RDS-->>PROXY: Result
PROXY-->>L1: Result
L1->>PROXY: Query selesai, koneksi dikembalikan ke pool
Note over PROXY: Koneksi dikembalikan ke pool\nsiap dipakai Lambda lain
L2->>PROXY: Query berikutnya menggunakan koneksi yang samaPerbedaan fundamentalnya: tanpa proxy, setiap Lambda instance mempertahankan koneksinya sendiri ke database. Dengan proxy, semua Lambda instance terhubung ke proxy (koneksi yang sangat ringan), dan proxy yang mengelola koneksi ke database secara terpusat.
Apa yang RDS Proxy Berikan
Selain pooling terpusat, RDS Proxy memberikan beberapa manfaat lain yang relevan untuk arsitektur Lambda:
Connection multiplexing. Satu koneksi fisik di pool proxy bisa digunakan bergantian oleh banyak Lambda instance yang tidak sedang aktif querying secara bersamaan. Ini adalah multiplexing yang tidak mungkin dilakukan di level aplikasi.
Failover yang lebih cepat. Saat terjadi failover RDS (primary down, replica dipromosikan), proxy meng-handle reconnection secara transparan. Lambda tidak perlu tahu bahwa failover terjadi.
Integrasi IAM dan Secrets Manager. RDS Proxy mendukung autentikasi menggunakan IAM token, sehingga Lambda tidak perlu menyimpan credential database — credential dikelola oleh Secrets Manager dan dirotasi secara otomatis.
Pinning control. RDS Proxy secara default melakukan connection pinning ketika mendeteksi operasi yang tidak bisa di-multiplex (seperti SET statement atau stored procedure yang mengubah state sesi). Memahami kapan pinning terjadi penting untuk memaksimalkan efisiensi proxy.
Setup RDS Proxy: Langkah per Langkah
Prasyarat Infrastruktur
Sebelum membuat RDS Proxy, pastikan komponen berikut sudah ada:
Prasyarat:
✓ RDS instance (PostgreSQL / MySQL) yang sudah berjalan
✓ AWS Secrets Manager secret yang menyimpan credential database
✓ VPC yang sama untuk Lambda, RDS Proxy, dan RDS
✓ Security group yang dikonfigurasi dengan benar
✓ IAM role untuk RDS Proxy (akses Secrets Manager)
Menyimpan Credential di Secrets Manager
RDS Proxy mengambil credential database dari Secrets Manager. Ini bukan hanya untuk keamanan — ini adalah requirement arsitektur proxy untuk bisa mengelola autentikasi secara terpusat.
# Buat secret untuk credential database
aws secretsmanager create-secret \
--name "prod/myapp/rds-credentials" \
--description "RDS credentials for myapp production" \
--secret-string '{
"username": "app_user",
"password": "strong-password-here",
"engine": "postgres",
"host": "mydb.cluster-xyz.ap-southeast-1.rds.amazonaws.com",
"port": 5432,
"dbname": "app_db"
}' \
--region ap-southeast-1
# Output: catat SecretARN untuk digunakan saat buat proxy
Membuat IAM Role untuk RDS Proxy
Proxy perlu permission untuk membaca secret dari Secrets Manager.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowGetSecretValue",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "arn:aws:secretsmanager:ap-southeast-1:123456789:secret:prod/myapp/rds-credentials-*"
},
{
"Sid": "AllowDecryptWithKMS",
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-southeast-1:123456789:key/your-kms-key-id",
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.ap-southeast-1.amazonaws.com"
}
}
}
]
}
Membuat RDS Proxy via AWS CLI
# Buat RDS Proxy
aws rds create-db-proxy \
--db-proxy-name "myapp-prod-proxy" \
--engine-family POSTGRESQL \
--auth '[{
"AuthScheme": "SECRETS",
"SecretArn": "arn:aws:secretsmanager:ap-southeast-1:123456789:secret:prod/myapp/rds-credentials-AbCdEf",
"IAMAuth": "DISABLED"
}]' \
--role-arn "arn:aws:iam::123456789:role/rds-proxy-role" \
--vpc-subnet-ids subnet-aaa subnet-bbb subnet-ccc \
--vpc-security-group-ids sg-proxy-id \
--require-tls \
--idle-client-timeout 1800 \
--region ap-southeast-1
# Daftarkan RDS instance sebagai target proxy
aws rds register-db-proxy-targets \
--db-proxy-name "myapp-prod-proxy" \
--db-cluster-identifiers "myapp-prod-cluster" \
--region ap-southeast-1
# Konfigurasi connection pool di target group
aws rds modify-db-proxy-target-group \
--db-proxy-name "myapp-prod-proxy" \
--target-group-name "default" \
--connection-pool-config '{
"MaxConnectionsPercent": 80,
"MaxIdleConnectionsPercent": 50,
"ConnectionBorrowTimeout": 120,
"SessionPinningFilters": ["EXCLUDE_VARIABLE_SETS"]
}' \
--region ap-southeast-1
Parameter MaxConnectionsPercent: 80 berarti proxy akan menggunakan maksimal 80% dari max_connections database. Sisanya 20% adalah buffer untuk koneksi administratif dan monitoring. SessionPinningFilters: EXCLUDE_VARIABLE_SETS menginstruksikan proxy untuk tidak melakukan pinning saat ada SET statement — ini meningkatkan efisiensi multiplexing untuk kebanyakan aplikasi.
Konfigurasi Security Group
# Security group untuk RDS Proxy
# Inbound: dari Lambda security group, port database
aws ec2 authorize-security-group-ingress \
--group-id sg-proxy-id \
--protocol tcp \
--port 5432 \
--source-group sg-lambda-id \
--region ap-southeast-1
# Security group untuk RDS
# Inbound: dari RDS Proxy security group saja (bukan langsung dari Lambda)
aws ec2 authorize-security-group-ingress \
--group-id sg-rds-id \
--protocol tcp \
--port 5432 \
--source-group sg-proxy-id \
--region ap-southeast-1
# BENAR: Lambda → Proxy → RDS
# ANTI-PATTERN: Lambda → RDS langsung (bypass proxy, tidak ada pooling terpusat)
Implementasi Go: Lambda dengan RDS Proxy
Inisialisasi Koneksi yang Tepat
Di Lambda, ada satu trik penting: inisialisasi koneksi database di luar handler function. Ini memungkinkan koneksi di-reuse antar invocation pada instance yang sama (warm start).
package main
import (
"context"
"database/sql"
"fmt"
"log"
"os"
"time"
"github.com/aws/aws-lambda-go/lambda"
_ "github.com/lib/pq"
)
// db dideklarasikan di level package — diinisialisasi sekali, di-reuse antar invocation
var db *sql.DB
func init() {
// init() dipanggil sekali saat Lambda instance pertama kali dibuat (cold start)
// Koneksi yang dibuat di sini akan di-reuse oleh semua invocation di instance yang sama
var err error
db, err = initDB()
if err != nil {
// Jika DB tidak bisa diinisialisasi, Lambda tidak bisa melayani request apapun
// Lebih baik fail fast daripada return error di setiap invocation
log.Fatalf("Gagal inisialisasi koneksi database: %v", err)
}
}
func initDB() (*sql.DB, error) {
// Gunakan endpoint RDS Proxy, bukan RDS langsung
host := os.Getenv("DB_PROXY_ENDPOINT") // dari env var Lambda
port := os.Getenv("DB_PORT")
user := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
dbname := os.Getenv("DB_NAME")
dsn := fmt.Sprintf(
"host=%s port=%s user=%s password=%s dbname=%s sslmode=require",
host, port, user, password, dbname,
)
database, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("sql.Open gagal: %w", err)
}
// BENAR: pool di Lambda harus kecil — RDS Proxy yang jadi pengatur utama
// MaxOpenConns kecil karena RDS Proxy yang melakukan multiplexing
database.SetMaxOpenConns(2)
database.SetMaxIdleConns(1)
// ConnMaxLifetime pendek untuk memastikan koneksi di-rotate secara reguler
// dan tidak menjadi stale setelah Lambda instance idle lama
database.SetConnMaxLifetime(5 * time.Minute)
// ConnMaxIdleTime: tutup koneksi idle yang tidak dipakai
// Ini penting agar proxy bisa reclaim koneksi ke pool-nya
database.SetConnMaxIdleTime(1 * time.Minute)
// Verifikasi koneksi bisa dibuat
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := database.PingContext(ctx); err != nil {
return nil, fmt.Errorf("database ping gagal: %w", err)
}
log.Printf("Koneksi database berhasil ke: %s", host)
return database, nil
}
// ANTI-PATTERN: inisialisasi koneksi di dalam handler
// func handler(ctx context.Context, event MyEvent) (string, error) {
// db, _ := initDB() // ✗ Buat koneksi baru setiap invocation
// defer db.Close() // ✗ Tutup koneksi setelah selesai — tidak ada reuse
// // ...
// }
Handler dengan Context dan Retry
type OrderRequest struct {
UserID int64 `json:"user_id"`
Amount float64 `json:"amount"`
Items []Item `json:"items"`
}
type OrderResponse struct {
OrderID int64 `json:"order_id"`
Status string `json:"status"`
}
func handler(ctx context.Context, req OrderRequest) (*OrderResponse, error) {
// Gunakan context dari Lambda untuk propagasi cancellation dan timeout
// Lambda secara otomatis cancel context ini saat function timeout
order, err := createOrder(ctx, req)
if err != nil {
log.Printf("ERROR createOrder: %v", err)
return nil, fmt.Errorf("gagal membuat order: %w", err)
}
return &OrderResponse{
OrderID: order.ID,
Status: "created",
}, nil
}
func createOrder(ctx context.Context, req OrderRequest) (*Order, error) {
// Gunakan transaksi untuk operasi yang membutuhkan atomicity
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("BeginTx gagal: %w", err)
}
defer tx.Rollback() // No-op jika sudah Commit
var orderID int64
err = tx.QueryRowContext(ctx,
"INSERT INTO orders (user_id, amount, status) VALUES ($1, $2, 'pending') RETURNING id",
req.UserID, req.Amount,
).Scan(&orderID)
if err != nil {
return nil, fmt.Errorf("insert order gagal: %w", err)
}
for _, item := range req.Items {
_, err = tx.ExecContext(ctx,
"INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)",
orderID, item.ProductID, item.Quantity,
)
if err != nil {
return nil, fmt.Errorf("insert order_item gagal: %w", err)
}
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("Commit gagal: %w", err)
}
return &Order{ID: orderID}, nil
}
func main() {
lambda.Start(handler)
}
Menggunakan IAM Authentication (Opsi yang Lebih Aman)
Daripada menyimpan password database di environment variable, Lambda bisa menggunakan IAM token untuk autentikasi ke RDS Proxy. Token ini di-generate secara otomatis dan berlaku 15 menit.
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
)
func generateIAMToken(ctx context.Context, proxyEndpoint, region, dbUser string) (string, error) {
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
if err != nil {
return "", fmt.Errorf("gagal load AWS config: %w", err)
}
// GenerateAuthToken membuat IAM authentication token yang berlaku 15 menit
authToken, err := auth.BuildAuthToken(
ctx,
fmt.Sprintf("%s:5432", proxyEndpoint),
region,
dbUser,
cfg.Credentials,
)
if err != nil {
return "", fmt.Errorf("gagal generate IAM token: %w", err)
}
return authToken, nil
}
// Dengan IAM auth, Lambda tidak perlu tahu password database sama sekali
// IAM Policy Lambda hanya perlu: rds-db:connect ke resource proxy yang spesifik
IAM authentication memerlukan IAM policy yang memberikan permission rds-db:connect ke Lambda execution role, dan RDS Proxy dikonfigurasi dengan IAMAuth: REQUIRED. Pendekatan ini lebih aman karena tidak ada credential statis yang perlu dirotasi secara manual.
Pola untuk Arsitektur Lain
RDS Proxy adalah solusi spesifik untuk Lambda + RDS di AWS. Distributed system yang lebih umum punya beberapa pola lain yang menyelesaikan masalah serupa.
PgBouncer untuk PostgreSQL Self-hosted atau ECS
Jika infrastrukturmu tidak sepenuhnya di Lambda atau kamu butuh lebih banyak kontrol, PgBouncer adalah connection pooler open-source yang sangat battle-tested untuk PostgreSQL.
flowchart LR
subgraph Services["Banyak Service Instance"]
S1[Service A\nInstance 1]
S2[Service A\nInstance 2]
S3[Service B]
S4[Service C]
end
subgraph Pooler["PgBouncer"]
PG["PgBouncer\nTransaction pooling mode\nPool size: 50"]
end
subgraph DB["Database"]
RDS[(PostgreSQL\nmax_connections: 100)]
end
S1 & S2 & S3 & S4 --> PG
PG --> RDS
note["Transaction mode:\nkoneksi dikembalikan ke pool\nsetelah setiap transaksi selesai\nbukan setelah sesi berakhir"]PgBouncer mendukung tiga mode pooling:
Session mode — koneksi diassign ke client selama sesi berlangsung. Paling kompatibel dengan semua fitur PostgreSQL tapi paling sedikit multiplexing.
Transaction mode — koneksi diassign ke client hanya selama transaksi aktif, lalu dikembalikan ke pool. Ini adalah mode yang paling efisien dan cocok untuk kebanyakan aplikasi web.
Statement mode — koneksi diassign per statement. Paling agresif tapi tidak kompatibel dengan transaksi multi-statement.
Untuk microservice di ECS atau Kubernetes, deploy PgBouncer sebagai sidecar container atau sebagai service tersendiri.
ProxySQL untuk MySQL
Ekuivalen PgBouncer untuk MySQL adalah ProxySQL — proxy yang lebih feature-rich dengan kemampuan query routing, read/write splitting, dan monitoring yang lebih detail.
flowchart LR
subgraph Apps["Microservices"]
A1[Service 1]
A2[Service 2]
A3[Service 3]
end
subgraph ProxySQL["ProxySQL"]
PS["ProxySQL\nQuery routing\nConnection pooling\nRead/Write splitting"]
end
subgraph MySQL["MySQL Cluster"]
PRI[(Primary\nWrite)]
REP1[(Replica 1\nRead)]
REP2[(Replica 2\nRead)]
end
Apps --> PS
PS -->|Write queries| PRI
PS -->|Read queries| REP1
PS -->|Read queries| REP2ProxySQL dapat secara otomatis merutekan query berdasarkan apakah ia SELECT (ke replica) atau INSERT/UPDATE/DELETE (ke primary), tanpa perubahan di level aplikasi.
Observability: Apa yang Harus Dipantau
Pooling yang dikonfigurasi dengan benar tanpa observability yang memadai tetap berbahaya — kamu tidak akan tahu kapan mendekati batas atau kapan ada masalah yang berkembang.
Metrik yang harus dipantau di CloudWatch:
RDS:
□ DatabaseConnections — total koneksi ke database
Alert: > 80% dari max_connections
□ FreeableMemory — memori yang tersedia
Alert: < 20% dari total memory
□ CPUUtilization
Alert: > 80% selama > 5 menit
RDS Proxy:
□ DatabaseConnectionRequests — request koneksi ke proxy
□ DatabaseConnections — koneksi aktif dari proxy ke RDS
□ DatabaseConnectionsCurrentlySessionPinned — pinned sessions
(Tinggi = kemungkinan ada pattern yang bisa dioptimalkan)
□ QueryDatabaseResponseTime — latensi response database
□ ClientConnections — koneksi dari Lambda ke proxy
Lambda:
□ Duration — jika naik drastis, mungkin ada connection wait
□ Errors — spike errors bisa mengindikasikan connection failure
□ Throttles — Lambda throttled karena concurrency limit
# Query metrik RDS connections via AWS CLI
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name DatabaseConnections \
--dimensions Name=DBInstanceIdentifier,Value=myapp-prod \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Average Maximum \
--region ap-southeast-1
# Query metrik RDS Proxy pinned sessions
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name DatabaseConnectionsCurrentlySessionPinned \
--dimensions Name=ProxyName,Value=myapp-prod-proxy \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Average \
--region ap-southeast-1
Jika metrikDatabaseConnectionsCurrentlySessionPinnedkonsisten tinggi, artinya proxy tidak bisa melakukan multiplexing secara optimal. Periksa apakah aplikasi menggunakanSETstatement, prepared statement dengan cara yang menyebabkan pinning, atau transaksi yang sangat panjang. Setiap session yang pinned adalah koneksi fisik yang eksklusif — tidak bisa di-share.
Kapan Menggunakan Apa
GUNAKAN RDS Proxy jika:
✓ Lambda + RDS (ini adalah use case utamanya)
✓ Banyak short-lived service instance yang connect ke RDS
✓ Traffic sangat spiky dan tidak terprediksi
✓ Butuh failover yang transparan tanpa perubahan kode
✓ Sudah di ekosistem AWS dan ingin managed solution
GUNAKAN PgBouncer jika:
✓ PostgreSQL self-hosted atau di container (ECS, Kubernetes)
✓ Butuh kontrol penuh atas konfigurasi pooler
✓ Ingin menghindari biaya tambahan RDS Proxy
✓ Tim sudah familiar dengan administrasi PostgreSQL
GUNAKAN ProxySQL jika:
✓ MySQL dan butuh read/write splitting otomatis
✓ Butuh query routing berdasarkan pattern
✓ Butuh observability detail di level query
TIDAK BUTUH PROXY jika:
✗ Monolith dengan jumlah instance yang kecil dan terprediksi
✗ Service dengan pool yang sudah terkonfigurasi dengan benar
dan jumlah instance yang di-cap
✗ CI/CD job atau batch processing yang tidak concurrent
Ringkasan
- Pooling klasik gagal di distributed system karena asumsinya runtuh: jumlah instance tidak terprediksi, lifecycle pendek, dan tidak ada “global pool” yang bisa di-share antar instance.
- Lambda + RDS tanpa proxy adalah bom waktu — saat traffic spike, ratusan instance masing-masing membuka koneksi dan database exhausted dalam hitungan detik.
- Mengurangi
MaxOpenConnsdi Lambda tidak cukup — ia mengurangi keparahan masalah tapi tidak menghilangkan ketidakpastian jumlah koneksi total karena bergantung pada berapa banyak instance yang dibuat AWS.- RDS Proxy menggeser pooling ke infrastruktur — satu pool terpusat yang melayani semua Lambda instance, dengan batas koneksi ke database yang bisa dikontrol secara eksplisit.
- Inisialisasi koneksi di luar handler, bukan di dalam — ini memungkinkan reuse koneksi antar warm invocation dan menghemat cold start overhead.
- Pool di Lambda harus kecil —
MaxOpenConns: 1–2cukup karena RDS Proxy yang melakukan multiplexing. Pool besar di Lambda justru membuang potensi multiplexing proxy.SessionPinningFilters: EXCLUDE_VARIABLE_SETSmengurangi pinning yang tidak perlu dan meningkatkan efisiensi multiplexing untuk kebanyakan aplikasi yang tidak memerlukan session-level state.- Pantau
DatabaseConnectionsCurrentlySessionPinned— metrik ini menunjukkan seberapa efektif multiplexing berjalan. Nilai tinggi mengindikasikan potensi optimasi di level aplikasi.