Level 5 · Hidup-Mati Pod di OpenShift
Sesi 37: Satu Pod 10 Koneksi, 200 Pod 2.000 Koneksi: Aritmatika Redis Connection Pool
Bagaimana jika setiap pod hanya membuka 10 koneksi Redis yang terlihat sangat wajar, tetapi saat HPA mencapai batas maksimum di tengah rolling update, Redis menolak koneksi baru dengan pesan ERR max number of clients reached padahal CPU-nya baru terpakai 15%?
1. Bayangkan Jika
Sebuah kedai kopi punya satu barista super cepat. Ia hanya bisa membuat satu pesanan pada satu waktu, tetapi tiap pesanan selesai dalam 2 detik. Di depan meja barista ada 10.000 kursi antrean.
Setiap kantor di sekitar kedai mengirim kurir. Satu kantor mengirim 10 kurir "untuk jaga-jaga", walaupun pesanan mereka cukup dibawa oleh satu kurir. Ketika kantornya bertambah menjadi 200, ada 2.000 kurir yang duduk di kursi. Sebagian besar hanya duduk diam tanpa memesan apa pun.
Lalu manajemen membuka cabang kantor baru tanpa menutup cabang lama (rolling update), setiap kantor mempekerjakan 4 shift sekaligus (worker process), dan kantor tetangga ikut memakai kedai yang sama. Kursi penuh. Kurir berikutnya ditolak di pintu, padahal barista sedang santai.
2. Apa yang Sebenarnya Terjadi
Redis mengeksekusi command di satu thread utama. Sejak Redis 6 ada I/O threads opsional untuk membaca dan menulis socket, tetapi eksekusi command tetap berurutan. Artinya Redis tidak butuh banyak koneksi untuk cepat. Yang ia butuhkan adalah command yang singkat.
Setiap koneksi tetap punya biaya:
- Memory per client: query buffer dan output buffer. Baseline-nya puluhan KB per koneksi, dan bisa membengkak saat client membaca reply besar (misal
SMEMBERSpada set berisi jutaan member). - File descriptor: satu socket = satu fd.
maxclientsdefault 10000, tetapi tetap dibatasiulimit -nproses Redis. - Penolakan keras: koneksi ke-10.001 langsung ditolak dengan
ERR max number of clients reacheddan tercatat direjected_connections.
Aritmatika yang sering dilupakan:
total koneksi = pod x worker per pod x client instance per worker x pool per client
| Skenario | Perhitungan | Koneksi |
|---|---|---|
| Baseline | 200 pod x 10 | 2.000 |
| Gunicorn 4 worker per pod | 200 x 4 x 10 | 8.000 |
| Rolling update, maxSurge 25% | 8.000 x 1,25 | 10.000 |
| Service lain berbagi Redis yang sama | 10.000 + 1.500 | 11.500 (> maxclients) |
Memory-nya juga ikut dihitung: 11.500 koneksi x ~30 KB = ~345 MB hanya untuk client buffer, sebelum ada satu key pun yang disimpan.
Library client punya model berbeda:
- Lettuce (default Spring Data Redis): multiplexed. Satu koneksi bersama melayani banyak thread lewat pipelining. Pool biasanya tidak perlu, kecuali untuk blocking command (
BLPOP) atau transaksi (MULTI/EXEC). - Jedis: satu command per koneksi pada satu waktu, jadi wajib pool (
maxTotal,maxIdle,minIdle,maxWait). - redis-py:
ConnectionPool(max_connections=...), dan pool dibuat per process, jadi dikalikan jumlah worker. - ioredis: satu koneksi per instance dengan auto-pipelining. Masalah muncul saat kode membuat
new Redis()di setiap request.
Berapa yang sebenarnya dibutuhkan? Pakai Little's Law dari Sesi 3 dan Sesi 6:
koneksi konkuren = throughput x waktu per command
500 ops/s x 0,002 s = 1 koneksi sibuk rata-rata
| Throughput per pod | Waktu command rata-rata | Koneksi sibuk | Pool yang masuk akal |
|---|---|---|---|
| 500 ops/s | 2 ms | 1 | 4–8 |
| 2.000 ops/s | 2 ms | 4 | 8–16 |
| 2.000 ops/s | 20 ms (command lambat) | 40 | perbaiki command-nya |
Kebanyakan tim memasang 50 "untuk jaga-jaga". Hasilnya 49 koneksi idle per pod yang memakan memory dan fd di Redis.
Dua kegagalan yang berlawanan:
- Pool terlalu kecil: request menunggu koneksi. Muncul
Could not get a resource from the pool(Jedis) atau borrow timeout. Latency aplikasi naik padahal CPU Redis rendah, karena waktu habis di antrean pool, bukan di Redis. - Pool terlalu besar: ribuan koneksi idle, dan saat semuanya putus bersamaan, terjadi reconnect storm.
3. Bagaimana Jika Kita Coba...
"Banyak timeout ke Redis. Naikkan saja pool jadi 100 per pod!"
Hitung dulu:
200 pod x 100 = 20.000 koneksi
maxclients = 10.000
Separuh pod tidak mendapat koneksi sama sekali dan langsung error max number of clients reached. Pod yang beruntung mendapat koneksi pun tidak jadi lebih cepat, karena Redis tetap memproses satu command pada satu waktu.
Yang lebih parah: akar masalahnya biasanya bukan ukuran pool. Ketika diselidiki lewat SLOWLOG GET, ternyata ada KEYS user:* yang dijalankan cron job setiap menit, memakan 800 ms. Selama 800 ms itu, semua client di semua pod menunggu. Pool yang lebih besar hanya menambah jumlah orang yang mengantre di belakang command lambat itu.
Pelaku umum yang menahan koneksi dan memblokir Redis:
| Command | Masalah |
|---|---|
KEYS * |
O(N) di seluruh keyspace, gunakan SCAN |
SMEMBERS / HGETALL pada key besar |
reply raksasa, output buffer membengkak |
| Lua script panjang | atomik, memblokir semua client sampai selesai |
BLPOP / BRPOP |
menahan satu koneksi pool selama menunggu |
4. Nama Resminya
- Connection Pool: kumpulan koneksi yang dipakai ulang agar tidak membuka socket baru per request.
- Connection Multiplexing / Pipelining: banyak command dikirim lewat satu koneksi tanpa menunggu reply satu per satu (Lettuce, ioredis).
- maxclients: batas jumlah client yang diterima Redis, default 10000, dibatasi ulimit.
- Pool Exhaustion / Borrow Timeout: semua koneksi pool sedang dipakai, request baru menunggu hingga
maxWaithabis. - Reconnect Storm: banyak client membuka ulang koneksi pada saat yang sama setelah failover, deploy, atau scale-out.
- Little's Law: L = λ x W, dasar sizing pool.
5. Di Dunia Kita
Contoh konfigurasi yang masuk akal untuk cache dengan target command < 5 ms:
# Spring Boot + Lettuce (multiplexed, tanpa pool kecuali butuh blocking command)
spring:
data:
redis:
host: redis.cache.svc
timeout: 300ms # command timeout
connect-timeout: 1s
lettuce:
shutdown-timeout: 2s
# pool hanya diaktifkan jika memakai BLPOP / MULTI
# pool:
# max-active: 8
# max-idle: 8
# min-idle: 0
// Jedis: wajib pool
JedisPoolConfig cfg = new JedisPoolConfig();
cfg.setMaxTotal(8); // bukan 50
cfg.setMaxIdle(8);
cfg.setMinIdle(0); // hindari pre-warm massal saat 200 pod start bersamaan
cfg.setMaxWait(Duration.ofMillis(200)); // gagal cepat, jangan menggantung thread
JedisPool pool = new JedisPool(cfg, "redis.cache.svc", 6379, 300 /* socket timeout ms */);
# redis-py: pool per process, ingat dikali jumlah gunicorn worker
pool = redis.ConnectionPool(
host="redis.cache.svc", port=6379,
max_connections=8,
socket_connect_timeout=1, socket_timeout=0.3,
health_check_interval=30,
)
r = redis.Redis(connection_pool=pool)
// ioredis: satu instance per process, bukan per request
const redis = new Redis({
host: "redis.cache.svc",
connectTimeout: 1000,
commandTimeout: 300,
enableAutoPipelining: true,
retryStrategy: (times) => Math.min(times * 200, 3000) + Math.random() * 500, // jitter
});
Hal-hal yang sering muncul di OpenShift:
- Connection storm saat deploy dan failover: 200 pod dengan
minIdle: 10yang start bersamaan membuka 2.000 koneksi dalam beberapa detik, ditambah biaya TLS handshake per koneksi. Saat Redis failover, semua client reconnect serentak. Gunakan lazy init,minIdlekecil, dan backoff dengan jitter (Sesi 26). - Timeout wajib: tanpa command timeout, Redis yang lambat membuat thread aplikasi menggantung, pool habis, dan kegagalan merambat ke upstream (Sesi 25). Untuk cache, 200–500 ms sudah longgar.
- Fail open dengan hati-hati: jika Redis down dan semua request langsung dialihkan ke database, itu thundering herd (Sesi 19) yang memindahkan masalah ke Sesi 16.
- Graceful shutdown harus menutup pool (Sesi 36): tanpa
pool.close()di shutdown hook, Redis menyimpan client setengah mati sampaitimeoutatautcp-keepalivemembersihkannya. Selama rolling update, koneksi zombie ini ikut menghabiskan jatah maxclients. - Redis Cluster: client membuka pool per node. 6 shard x 200 pod x 8 = 9.600 koneksi total, tersebar ke beberapa node tetapi tetap harus dihitung.
- Proxy untuk client yang sangat banyak: Twemproxy atau Envoy Redis proxy sebagai sidecar/gateway, mirip peran PgBouncer di Sesi 17.
6. Lensa Performance Tester
Metrik di sisi Redis:
redis-cli INFO clients
# connected_clients:8123
# blocked_clients:12
# maxclients:10000
redis-cli INFO stats | grep rejected_connections
# rejected_connections:347
redis-cli CLIENT LIST | awk '{print $2}' | cut -d= -f2 | cut -d: -f1 | sort | uniq -c | sort -rn | head
# hitung koneksi per IP pod
redis-cli SLOWLOG GET 10
redis-cli LATENCY LATEST
| Metrik | Arti | Tanda bahaya |
|---|---|---|
connected_clients |
koneksi aktif saat ini | > 70% dari maxclients |
blocked_clients |
client di blocking command | naik terus tanpa turun |
rejected_connections |
ditolak karena penuh | lebih dari 0 |
| Pool active / idle / waiting | kondisi pool di aplikasi | waiting > 0 terus-menerus |
| Borrow wait time p99 | waktu menunggu koneksi | mendekati maxWait |
| Reconnect rate | koneksi baru per detik | lonjakan saat rolling restart |
Skenario uji:
- Scale ke HPA max: naikkan replica ke batas HPA, hitung
connected_clientsdan bandingkan dengan rumus di bagian 2. Lakukan sambil rolling update agar maxSurge ikut terhitung. - Redis failover di tengah beban: matikan primary, ukur durasi error, puncak reconnect rate, dan apakah backoff memiliki jitter.
- Latency injection: tambahkan 50–200 ms delay ke Redis (Toxiproxy atau network chaos). Amati apakah command timeout bekerja atau thread aplikasi menggantung dan pool habis.
- Slow command test: jalankan
KEYS *atau Lua script berat di staging, lihat efeknya ke p99 semua service yang berbagi Redis. - Shutdown check: setelah rolling restart, apakah jumlah client kembali ke baseline, atau ada client zombie dengan
idletinggi diCLIENT LIST?
7. Pertanyaan untuk Ronde Berikutnya
Anggap pool sudah dihitung dengan benar, timeout sudah dipasang, dan shutdown sudah menutup koneksi dengan rapi. 200 pod, 8 koneksi masing-masing, aman di bawah maxclients.
Tetapi bagaimana jika scheduler OpenShift menaruh 150 dari 200 pod itu di node yang sama, atau di zona yang sama? Satu node mati, dan 75% kapasitas hilang dalam satu detik, lalu 150 pod pengganti reconnect ke Redis bersamaan.
Jawabannya ada di Sesi 38: Semua Telur di Satu Keranjang, tentang topology spread constraints dan maxSkew.