Lonjakan trafik tiba-tiba yang melumpuhkan database tidak selalu berasal dari serangan DDoS eksternal. Dalam arsitektur aplikasi mobile, skenario umum yang sering memicu insiden fatal adalah thundering herd problem akibat reconnect storm. Artikel ini membedah kasus nyata database connection pool exhaustion yang dipicu oleh aplikasi React Native saat konektivitas jaringan pengguna pulih secara serentak.
1. Gejala Insiden: Pool Starvation dan HTTP 504
Insiden bermula ketika sistem monitoring mendeteksi lonjakan error secara mendadak pada API Gateway dan layanan backend utama. Gejala yang teramati pada sistem meliputi:
- Active Connections Mencapai Batas Maksimum: Jumlah koneksi aktif pada database cluster (PostgreSQL) melonjak tajam menyentuh batas
max_connections(misalnya 200/200 pool allocation habis terpakai dalam hitungan detik). - Pool Acquisition Timeout: Log aplikasi backend mencatat error fatal seperti
TimeoutError: ResourceRequest timed outatau Knex/TypeORM errorTimeout acquiring a connection. Worker process tidak bisa mengeksekusi query karena antrean memperebutkan koneksi pool kosong melebihi batas waktu tunggu default (biasanya 10–30 detik). - Cascade Failure HTTP 504 (Gateway Timeout): Load balancer dan API Gateway kehabisan downstream timeout karena permintaan HTTP tertahan di layer aplikasi yang sedang memblokir event loop/thread menunggu koneksi DB rilis.
2. Root Cause: Thundering Herd via NetInfo React Native
Penyebab utama insiden bukan berasal dari lambatnya query database, melainkan pola rekoneksi di sisi client. Pada arsitektur aplikasi mobile React Native, modul seperti @react-native-community/netinfo lazim digunakan untuk mendeteksi perubahan status koneksi data.
Masalah timbul ketika implementasi sinkronisasi data offline dieksekusi secara instan tanpa kontrol konkurensi:
// Anti-Pattern: Trigger flush instan tanpa penundaan atau jitter
NetInfo.addEventListener(state => {
if (state.isConnected && state.isInternetReachable) {
// Ribuan perangkat mengeksekusi ini secara serentak
offlineQueue.flush();
}
});Ketika terjadi gangguan jaringan regional—seperti BTS seluler mengalami flicker, terowongan kereta, atau pemulihan jaringan setelah outage provider—puluhan ribu perangkat mobile beralih dari kondisi offline ke online secara bersamaan. Seluruh instance client langsung mengeksekusi antrean mutasi HTTP (POST/PUT data sinkronisasi) dalam sub-detik yang sama. Serangan rekoneksi massal ini (reconnect storm) menghantam backend secara instan tanpa proses perataan beban (smoothing).
3. Diagnosa Metrik dan Karakteristik Trafik
Langkah penelusuran akar masalah dilakukan melalui tiga observasi analitik utama:
- Distribusi Arrival Rate: Metrik Request Per Second (RPS) tidak memperlihatkan kurva eksponensial bertahap, melainkan lonjakan vertikal curam (step increase) hingga 30–50 kali lipat dari baseline dalam rentang 500 ms.
- Pool Metrics vs DB CPU: Metrik database CPU berada di level rendah (10–20%), tetapi metrik
pool.waiting_clientsmenumpuk ribuan antrean. Ini mengindikasikan bottleneck berada pada overhead konkurensi pool dan I/O koneksi baru, bukan eksekusi kalkulasi query. - Endpoint Correlation: 90% trafik yang membanjiri server mengarah ke endpoint sinkronisasi mutasi (misal
/api/v1/sync/batch) dengan signature header yang sama dari background task mobile.
4. Solusi Sisi Backend: Fail-Fast, Queue Limit, dan Load Shedding
Langkah pertama adalah melindungi database dari saturasi connection pool agar backend tidak crash total ketika dihantam lonjakan trafik berikutnya.
Konfigurasi Pool Timeout Ketat
Turunkan waktu acquireTimeoutMillis pada pool manager. Membiarkan request menunggu 30 detik pada antrean pool hanya menghabiskan memory worker dan socket gateway. Lebih baik gagal cepat (fail-fast):
// Contoh konfigurasi pool (pg-pool / generic-pool)
const poolConfig = {
max: 50,
idleTimeoutMillis: 10000,
acquireTimeoutMillis: 2000, // Gagal cepat dalam 2 detik jika pool penuh
};Load Shedding Middleware dengan HTTP 429
Terapkan middleware load shedding sebelum request menyentuh database query layer. Jika kapasitas antrean connection pool mendekati batas aman, tolak request seketika menggunakan status HTTP 429 Too Many Requests serta berikan header Retry-After:
import { Request, Response, NextFunction } from 'express';
import { dbPool } from './database';
export function poolSheddingMiddleware(req: Request, res: Response, next: NextFunction) {
const MAX_QUEUE_LIMIT = 30;
// Periksa antrean thread yang menunggu pool connection
if (dbPool.waitingCount > MAX_QUEUE_LIMIT) {
// Beri sinyal pada client untuk menunda request (misal antara 5-15 detik)
const retrySeconds = Math.floor(Math.random() * 10) + 5;
res.setHeader('Retry-After', retrySeconds.toString());
return res.status(429).json({
code: 'DB_SATURATED',
message: 'Server sedang memproses antrean tinggi. Silakan coba kembali.',
});
}
next();
}5. Solusi Sisi React Native: Exponential Backoff dengan Full Jitter
Perbaikan di backend hanya berfungsi sebagai sistem pengaman (safety net). Sumber masalah utama di client harus diperbaiki dengan mendistribusikan waktu eksekusi antrean offline.
Mengapa Exponential Backoff Saja Tidak Cukup?
Exponential backoff standar tanpa randomisasi hanya menunda gelombang thundering herd ke interval berikutnya. Ribuan client yang gagal pada detik 0 akan serentak mencoba kembali pada detik 2, detik 4, dan detik 8 secara berkelompok. Solusi standar industri adalah menggunakan Full Jitter.
Formula Full Jitter:
sleep = random_between(0, min(max_delay, base_delay * 2 ** attempt))Implementasi Offline Queue Sync
Berikut implementasi sinkronisasi aman pada React Native:
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
const BASE_DELAY_MS = 1000;
const MAX_DELAY_MS = 30000;
let isSyncing = false;
function calculateFullJitter(attempt: number): number {
const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
// Nilai random dari 0 hingga exponentialDelay meratakan arrival rate secara seragam
return Math.floor(Math.random() * exponentialDelay);
}
async function processQueueWithBackoff(attempt = 0): Promise<void> {
const jitterDelay = calculateFullJitter(attempt);
await new Promise(resolve => setTimeout(resolve, jitterDelay));
try {
await sendOfflineMutations();
} catch (error: any) {
// Tangani respon load shedding dari backend
if (error.response?.status === 429) {
const retryAfterHeader = error.response.headers['retry-after'];
const retryAfterSeconds = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 5;
await new Promise(resolve => setTimeout(resolve, retryAfterSeconds * 1000));
return processQueueWithBackoff(attempt + 1);
}
// Retry hingga batas percobaan wajar
if (attempt < 5) {
return processQueueWithBackoff(attempt + 1);
}
}
}
export function initializeNetworkListener() {
NetInfo.addEventListener((state: NetInfoState) => {
if (state.isConnected && state.isInternetReachable && !isSyncing) {
isSyncing = true;
processQueueWithBackoff(0).finally(() => {
isSyncing = false;
});
}
});
}6. Evaluasi dan Monitoring
Kombinasi perbaikan backend dan client mengubah bentuk kurva traffic arrival rate dari paku vertikal (spike) tajam menjadi bukit landai yang terdistribusi merata sepanjang beberapa detik/menit.
Rekomendasi Produksi: Selalu atur alokasi koneksi connection pool database dengan formula:
((core_cpu * 2) + effective_spindle_count)per node database, jangan menaikkanmax_connectionssecara serampangan untuk mengatasi pool exhaustion karena justru akan memperburuk context switching pada CPU database engine.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!