Gejala: Latency p99 Meledak saat Resource CPU/RAM Rendah
Masalah thread pool starvation sering kali tidak memicu alarm infrastruktur konvensional. Metrik CPU backend berada di bawah 15% dan utilisasi RAM stabil, namun pengguna mulai mengalami request drop massal dan reverse proxy memuntahkan respon HTTP 504 Gateway Timeout. Metrik p99 latency melonjak dari ratusan milidetik menjadi puluhan detik.
Kondisi ini terjadi ketika seluruh worker thread backend (seperti Tomcat worker threads, Gunicorn sync workers, atau PHP-FPM processes) habis terpakai. Permintaan HTTP baru yang masuk terpaksa mengantre di accept-queue OS TCP listen backlog sampai mencapai batas timeout.
Root Cause: Efek Serupa Slowloris dari Koneksi Seluler
Penyebab utama skenario ini bukanlah query database yang lambat atau komputasi backend yang berat, melainkan transmisi multipart/form-data dari aplikasi React Native yang berjalan pada jaringan seluler buruk (EDGE/3G atau packet loss tinggi).
Pada arsitektur backend tradisional berbasis synchronous thread-per-request (seperti Apache Tomcat standar atau Gunicorn sync worker), alokasi satu worker thread dilakukan seketika koneksi diterima. Jika klien React Native mengunggah file 4 MB dengan kecepatan koneksi hanya 10 KB/s, worker thread backend akan diblokir selama lebih dari 400 detik hanya untuk menunggu byte-byte payload selesai dialirkan melalui socket.
Ketika puluhan hingga ratusan klien mobile melakukan upload lambat secara bersamaan, thread pool backend (misalnya 200 threads pada Tomcat) langsung habis (starvation). Akibatnya, request baca sederhana seperti GET /api/v1/profile tidak dapat dilayani karena tidak ada worker thread yang tersisa.
Langkah Diagnostik
1. Analisis Thread Dump
Eksekusi thread dump pada instance backend menggunakan jstack (Java) atau pstack/gdb untuk runtime lain ketika latensi sedang naik. Pada thread pool starvation akibat slow network, sebagian besar worker thread berada dalam status RUNNABLE namun tertahan di system call socket read:
"http-nio-8080-exec-42" #85 daemon prio=5 os_prio=0 tid=0x00007f9c24001000 nid=0x1a4b runnable [0x00007f9bf8ef8000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:785)
at org.apache.coyote.http11.Http11InputBuffer.parseHeader(Http11InputBuffer.java:558)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)Jika 80-90% worker pool terkunci pada socketRead0 di level framework parsing request body, dipastikan backend sedang menunggu data dari klien lambat.
2. Korelasi Access Log Reverse Proxy
Periksa access log reverse proxy (Nginx). Format log harus menyertakan metrik waktu $request_time (durasi total klien terhubung ke proxy) dan $upstream_response_time (durasi proxy berkomunikasi dengan backend):
log_format performance '$remote_addr - [$time_local] "$request" '
'$status $body_bytes_sent $request_time '
'upstream: $upstream_addr time: $upstream_response_time';Jika $request_time bernilai sangat tinggi (misal: 45.210 detik) dan $upstream_response_time bernilai identik tinggi atau mencatat 504, ini membuktikan backend menahan koneksi selama proses pengiriman byte dari klien, bukan saat pemrosesan logika bisnis.
Solusi Arsitektur dan Konfigurasi
1. Terapkan Request Buffering di Reverse Proxy
Solusi paling efektif adalah memisahkan I/O jaringan klien yang lambat dari synchronous worker backend. Gunakan arsitektur event-driven non-blocking (Nginx atau Envoy) di depan backend dengan mengaktifkan request buffering.
Nginx akan membaca seluruh body request dari klien React Native ke memory/disk buffer internalnya terlebih dahulu. Setelah payload diterima 100%, barulah Nginx meneruskan request utuh ke backend sync worker via jaringan lokal (loopback/VPC) berkecepatan gigabit:
# /etc/nginx/conf.d/api.conf
server {
listen 443 ssl http2;
server_name api.example.com;
client_max_body_size 20M;
location /api/ {
proxy_pass http://backend_upstream;
# Wajib diaktifkan untuk isolasi slow client
proxy_request_buffering on;
# Alokasi buffer memori sebelum spill ke disk
client_body_buffer_size 128k;
client_body_timeout 30s;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}Penting: Jangan mematikanproxy_request_buffering(jangan set keoff) kecuali Anda mengimplementasikan endpoint streaming khusus (misal: chunked video streaming) dengan backend asynchronous (Netty, Node.js, FastAPI/Uvicorn).
2. Atur Connection Timeout di Backend Sync
Sebagai pertahanan lapis kedua, batasi batas toleransi backend terhadap socket read idle. Di Tomcat, atur connectionTimeout pada Connector di server.xml:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
disableUploadTimeout="false"
connectionUploadTimeout="30000"
maxThreads="200" />Jika menggunakan Gunicorn (sync workers), gunakan parameter --timeout ketat (misal 30 detik), atau beralihlah ke worker class asynchronous (gevent atau uvicorn.workers.UvicornWorker) untuk endpoint upload.
3. Mitigasi Client-Side di React Native
Di sisi React Native, kurangi ukuran data dan kendalikan network timeout secara eksplisit.
- Kompresi Gambar/File Sebelum Upload: Gunakan pustaka native seperti
react-native-image-resizeruntuk mengubah resolusi dan kompresi JPEG ke 70-80% sebelum membentuk payload multipart. - Batas Waktu Request Tegas: Jangan gunakan konfigurasi default tanpa timeout. Terapkan batas pembatalan via
AbortController. - Bypass Backend API (Direct-to-Storage): Hindari streaming file media besar langsung melalui server aplikasi. Buat endpoint ringan di backend untuk menghasilkan Pre-signed URL (AWS S3, Cloudflare R2, Google Cloud Storage), lalu biarkan React Native mengunggah langsung ke storage bucket tersebut.
// Implementasi Timeout Upload di React Native
const uploadWithTimeout = async (uri, presignedUrl, timeoutMs = 45000) => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(presignedUrl, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: { uri, type: 'image/jpeg', name: 'upload.jpg' },
signal: controller.signal,
});
return response.ok;
} finally {
clearTimeout(timer);
}
};Kombinasi proxy request buffering di layer ingress dan kompresi di layer mobile client mengeliminasi risiko thread starvation tanpa perlu menambah kapasitas hardware backend.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!