Secara default, Go runtime mengelola konkurensi menggunakan model GMP (Goroutine, Machine, Processor) dalam satu process space. Namun, framework Fiber menyediakan opsi konfigurasi Prefork: true yang meniru model multi-process Node.js cluster atau Nginx master-worker. Di lingkungan bare-metal atau VM besar, prefork dapat memaksimalkan throughput CPU-bound dengan memintas lock contention tertentu pada runtime scheduler. Namun, di dalam ekosistem Kubernetes (K8s), pendekatan ini sering berbenturan dengan filosofi single-process container, memicu isu alokasi resource, dan merusak observabilitas sistem.
Mekanisme Kerja Prefork di Go Fiber
Fiber dibangun di atas FastHTTP. Saat Prefork diaktifkan, proses utama (master process) tidak melayani traffic secara langsung. Master process membaca jumlah core yang tersedia (via runtime.NumCPU() atau GOMAXPROCS) lalu mengeksekusi binary yang sama secara berulang sebagai child processes menggunakan os.StartProcess dengan flag penanda lingkungan internal (seperti FIBER_PREFORK_CHILD=on).
Setiap child process membuat listening socket pada port yang sama menggunakan socket option SO_REUSEPORT di level kernel Linux. Kernel kemudian mendistribusikan koneksi TCP masuk ke antrean socket masing-masing child process secara merata (round-robin / hash-based). Pendekatan ini menghilangkan bottleneck single-accept loop dan mendistribusikan beban jaringan langsung di kernel space tanpa sinkronisasi antar-goroutine di tingkat user space.
Konfigurasi Minimal Go Fiber dengan Prefork
Berikut adalah implementasi standar Fiber dengan prefork aktif beserta penanganan graceful shutdown berbasis signal OS:
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New(fiber.Config{
Prefork: true,
})
app.Get("/healthz", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
// Jalankan server di goroutine terpisah
go func() {
if err := app.Listen(":3000"); err != nil {
log.Printf("Listen error: %v", err)
}
}()
// Tangkap signal penghentian
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Memulai shutdown server...")
if err := app.Shutdown(); err != nil {
log.Fatalf("Shutdown paksa terjadi: %v", err)
}
log.Println("Server selesai berhenti.")
}
Trade-off Arsitektural: Multi-Process vs Containerized K8s
Meskipun prefork meningkatkan skor benchmark sintetis, integrasinya dengan lingkungan Kubernetes memperkenalkan komplikasi arsitektur yang signifikan.
1. Penggandaan Database Connection Pool
Ketika aplikasi Go konvensional menginisialisasi connection pool (misalnya melalui database/sql atau pgxpool), connection pool tersebut dikelola bersama oleh semua goroutine dalam satu heap memory. Pada mode Prefork, setiap child process merupakan OS process mandiri yang mengeksekusi inisialisasi kode aplikasi secara terpisah.
Jika Anda menentukan parameter connection pool:
SetMaxOpenConns = 25- CPU Container = 4 Core (Fiber membuat 4 child processes)
- Replika Pod = 4 Pod
Total koneksi database maksimum yang dibuka adalah: 25 koneksi × 4 child process × 4 pod = 400 koneksi. Pola ini dapat dengan cepat menghabiskan max_connections pada PostgreSQL atau MySQL, menyebabkan koneksi ditolak (connection exhaustion) tanpa ada lonjakan traffic aktual.
2. Overhead Memori dan Risiko OOMKilled (Exit Code 137)
Setiap child process Go mengalokasikan runtime overhead tersendiri: heap awal, runtime thread metadata, stack memory, dan garbage collection (GC) pacing. Linux menggunakan mekanisme Copy-on-Write (CoW) saat proses melakukan fork, namun Go runtime tidak dioptimalkan untuk CoW; inisialisasi garbage collector dan memory arena allocation langsung mengubah dirty memory pages sesaat setelah child aktif.
Jika sebuah Pod dialokasikan resource limit sebesar 512MiB dan Fiber menjalankan 4 child process, baseline memory per process harus bertahan di bawah 100MiB. Saat terjadi lonjakan alokasi memori dinamis pada salah satu worker, total penggunaan cgroup memory akan melampaui limit node K8s, memicu kernel OOM Killer untuk menghentikan seluruh Pod.
3. Fragmentasi Observabilitas dan Metrik Prometheus
Library metrik standar Prometheus (client_golang) menyimpan data counter, gauge, dan histogram dalam in-memory atomic storage. Ketika endpoint /metrics diakses oleh Prometheus scraper melalui Kubernetes Service:
- Request scrape hanya masuk ke satu child process secara acak via
SO_REUSEPORT. - Scrape interval berikutnya berpotensi masuk ke child process yang berbeda.
- Grafik metrik di Grafana akan mengalami flapping (naik-turun tidak menentu) karena nilai counter internal antar proses tidak tersinkronisasi.
Untuk mengatasi masalah ini, engineer harus menambahkan pushgateway eksternal, IPC shared memory, atau menjalankan Prometheus client dalam mode disk-backed directory (seperti prometheus-client-mmap), yang menambah kompleksitas operasional serta latency profiling.
4. Tantangan Graceful Shutdown (SIGTERM)
Kubernetes menghentikan Pod dengan mengirimkan sinyal SIGTERM ke PID 1 di dalam container. Dalam konfigurasi container standar dengan Fiber Prefork:
- Master process menerima
SIGTERM. - Master process harus meneruskan sinyal tersebut ke seluruh child processes.
- Semua child processes harus berhenti menerima koneksi baru pada listening socket, menyelesaikan request yang berjalan (in-flight requests), lalu menutup koneksi database.
Jika koordinasi sinyal antara master process dan child process gagal atau melampaui terminationGracePeriodSeconds, Kubernetes akan mengirim SIGKILL. Akibatnya, request aktif pengguna akan diputus seketika secara paksa (HTTP 502/connection reset by peer).
Pod Density, Efisiensi Biaya, dan Maintainability
Dalam desain sistem cloud-native modern, Kubernetes dirancang untuk melakukan scaling pada unit terkecil yang independen (Pod). Membandingkan efisiensi operasional:
- Model Fine-Grained (Standar K8s): Pod berukuran kecil (contoh:
cpu: 500m,memory: 256MiB) tanpa prefork, memanfaatkan single-process multi-threading standar Go. Kubernetes Horizontal Pod Autoscaler (HPA) menambah replika pod saat CPU mencapai target utilization. Scheduler K8s dapat menyebar (bin-packing) pod-pod kecil ini ke berbagai node cluster secara optimal. - Model Coarse-Grained (Fiber Prefork): Pod berukuran besar (contoh:
cpu: 4,memory: 2GiB) dengan Prefork aktif. Scheduling Pod menjadi kaku karena node harus memiliki fragmentasi CPU yang cukup besar. Jika salah satu child process mengalami crash atau memory leak, dampaknya dirasakan oleh 25% atau 50% kapasitas Pod tersebut (blast radius lebih besar).
Matriks Keputusan: Kapan Menggunakan Prefork?
Pilih arsitektur sesuai batas kendala infrastruktur dan kebutuhan runtime:
| Kondisi Sistem | Rekomendasi | Alasan Teknis |
|---|---|---|
| Deploy di Kubernetes dengan HPA | Standar Runtime (Prefork: false) | K8s mengelola skala horizontal. Mempertahankan single connection pool, isolasi fault-tolerance, dan akurasi scrape Prometheus tanpa overhead IPC. |
| Aplikasi I/O Bound (banyak query DB, Redis, external API) | Standar Runtime (Prefork: false) | Go runtime network poller (epoll) menangani ribuan goroutine I/O secara non-blocking jauh lebih efisien dibanding multi-process. |
| Deploy di Bare-Metal / Dedicated VM (misal: CPU 32 core) tanpa K8s | Fiber Prefork (Prefork: true) | Mengurangi contention Go scheduler pada core berdensitas sangat tinggi dan memeras throughput raw HTTP maksimal. |
| Aplikasi Stateless CPU-Bound (misal: enkripsi, hashing, image processing lokal) | Fiber Prefork (Prefork: true) | Isolasi memory space per-core dan optimasi kernel socket routing mengurangi cross-thread synchronization overhead. |
Kesimpulannya, dalam ekosistem Kubernetes, menjalankan Go Fiber dengan runtime standar dan mendelegasikan skalabilitas ke HPA adalah pendekatan yang lebih stabil, aman terhadap resource cgroups, dan mudah dipelihara oleh tim platform engineering.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!