Error accept4: too many open files (errno EMFILE) pada backend berbasis Go Fiber menandakan proses kehabisan File Descriptor (FD) yang dialokasikan oleh kernel sistem operasi. Pada arsitektur Linux, socket jaringan, file, dan pipe direpresentasikan sebagai file descriptor. Ketika alokasi FD habis, runtime Go tidak dapat menerima koneksi TCP masuk baru maupun membuka socket keluar, memicu kegagalan bertingkat pada aplikasi.
1. Gejala Insiden: Pod Failure dan Socket Rejection
Insiden bermula sesaat setelah rilis service Go Fiber yang mengonsumsi API downstream baru. Ketika traffic meningkat, gejala klinis berikut muncul di cluster Kubernetes:
- Liveness dan Readiness Probe Gagal: Kubelet mengirim sinyal
connection refusedatautimeoutke port health check/healthz. Runtime tidak merespons karena antrean TCP backlog penuh dan kernel menolakaccept()socket baru. Pod masuk statusCrashLoopBackOff. - Log Aplikasi: Log service dipenuhi error berikut:
http: Accept error: accept tcp [::]:8080: accept4: too many open files; retrying in 5ms- Kegagalan Outbound Request: Service gagal melakukan panggilan HTTP ke dependensi internal, memicu lonjakan error HTTP 500 dan 502 di API Gateway.
2. Observasi dan Diagnostik: Prometheus, ss, dan lsof
Sebelum mematikan instance untuk mitigasi, langkah diagnostik dilakukan untuk memvalidasi apakah kebocoran terjadi pada file I/O atau network socket.
Query Prometheus
Rasio penggunaan FD dipantau melalui metrik bawaan runtime Go via Prometheus collector:
# Hitung rasio pemakaian FD terhadap limit maksimum
(process_open_fds{job="order-service"} / process_max_fds{job="order-service"}) * 100 > 80Grafik metrik menunjukkan kenaikan linear process_open_fds dari rata-rata baseline 120 hingga menyentuh batas lunak (soft limit) process_max_fds (misal: 1024 atau 65535) tanpa ada kurva penurunan saat traffic melandai.
Inspeksi Socket pada Container Target
Melalui pod debug (ephemeral container) atau exec langsung ke container, periksa status koneksi jaringan menggunakan ss:
# Ringkasan socket pada namespace jaringan container
ss -s
# Filter socket TCP spesifik ke arah downstream service
ss -tan state time-wait sport = :8080 or dport = :9000 | head -n 30Hasil ss -s memperlihatkan ribuan socket dalam status TIME_WAIT dan ESTABLISHED. Untuk melihat proses pemilik socket tersebut, jalankan lsof:
# Hitung jumlah socket TCP terbuka oleh PID aplikasi (PID 1)
lsof -p 1 -a -i TCP | wc -l
# Tampilkan 10 socket terakhir beserta IP tujuannya
lsof -p 1 -a -i TCP | tail -n 10Output lsof memverifikasi bahwa ribuan FD terbuka mengarah ke host downstream yang sama pada port HTTP upstream, bukan berasal dari kebocoran pembukaan berkas statis (log/file read).
3. Mitigasi Darurat: Rollback Deployment
Saat batas FD terlampaui di production, restart pod sederhana (restart pod manual) hanya memberi jeda sementara sebelum pod baru kembali crash akibat lonjakan traffic yang mengantre. Tindakan pertama adalah menghentikan pemicu beban melalui rollback ke image stabil sebelumnya.
# Eksekusi rollback Kubernetes deployment
kubectl rollout undo deployment/order-service -n production
# Pantau transisi pod
kubectl rollout status deployment/order-service -n production --watchSetelah rollback selesai, grafik Prometheus menunjukkan process_open_fds kembali ke level normal (baseline < 200). Probe kembali hijau, dan error EMFILE berhenti total.
4. Postmortem: Akar Masalah pada HTTP Client
Go Fiber dibangun di atas fasthttp, bukan package net/http standar. Kesalahan fatal terjadi di handler layer rilis baru, di mana instance HTTP client diinisialisasi secara berulang di setiap request masuk.
Anti-Pattern: Instansiasi Client per Request
// SALAH: Instansiasi client baru pada setiap request
func handleCheckout(c *fiber.Ctx) error {
client := &fasthttp.Client{} // FATAL: Client dan transport pool baru dibuat di tiap goroutine
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI("http://payment-service:9000/charge")
req.Header.SetMethod("POST")
if err := client.Do(req, resp); err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
return c.Send(resp.Body())
}Dampaknya:
- Setiap kali
&fasthttp.Client{}dibuat lokal, connection pool internal terisolasi di instance tersebut dan tidak dibagi ke handler lain. - Setelah request selesai, connection pool lokal masuk fase garbage collection sementara socket TCP underlying masih berada dalam siklus penutupan kernel (
TIME_WAIT). - Socket yang belum sepenuhnya di-reap oleh OS menahan File Descriptor tetap terbuka. Saat throughput mencapai ratusan request per detik, kuota FD habis jauh lebih cepat dibanding durasi kernel melepas status
TIME_WAIT(biasanya 60 detik).
5. Solusi: Singleton fasthttp.Client dengan Batasan Pool
HTTP client wajib dijadikan singleton di tingkat package atau dependency injection container agar connection pool dapat digunakan ulang (keep-alive). Konfigurasi batas koneksi per host dan timeout secara eksplisit.
package client
import (
"time"
"github.com/valyala/fasthttp"
)
// Inisialisasi fasthttp.Client sebagai singleton
var UpstreamClient = &fasthttp.Client{
Name: "order-service-client",
NoDefaultUserAgentHeader: true,
MaxConnsPerHost: 2048, // Batas atas socket per host
MaxIdleConnDuration: 30 * time.Second, // Tutup koneksi idle jika tidak terpakai
ReadTimeout: 5 * time.Second, // Cegah leak FD karena downstream hanging
WriteTimeout: 5 * time.Second,
MaxConnWaitTimeout: 1 * time.Second, // Error jika pool penuh
}
// ponytail: tuning MaxConnsPerHost sesuai throughput target; gunakan standard net/http jika butuh HTTP/2.Gunakan singleton tersebut di dalam handler Fiber:
func handleCheckout(c *fiber.Ctx) error {
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI("http://payment-service:9000/charge")
req.Header.SetMethod("POST")
// Gunakan client singleton yang reuse connection pool
if err := client.UpstreamClient.Do(req, resp); err != nil {
return c.Status(fiber.StatusServiceUnavailable).SendString("Upstream error")
}
return c.Send(resp.Body())
}6. Pencegahan: Alerting & Resource Tuning
Guna mendeteksi kebocoran socket sebelum memicu insiden berskala penuh, terapkan Prometheus AlertRule berikut:
groups:
- name: process-resource-alerts
rules:
- alert: ProcessNearFDLimit
expr: (process_open_fds / process_max_fds) * 100 > 75
for: 2m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} mendekati batas kapasitas FD ({{ $value }}%)"Selain alerting, pastikan Kubernetes security context menetapkan nofile limit yang realistis via limits atau pod definition, serta audit seluruh outbound client (DB driver, HTTP, gRPC) agar selalu menggunakan pola shared instance atau connection pool tunggal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!