Kronologi Insiden dan Deteksi Error 502
Saat proses rolling deployment versi baru dari microservice berbasis Go Fiber di kluster Kubernetes, Ingress controller (seperti NGINX Ingress Controller atau AWS ALB Ingress) mendadak mencatat lonjakan HTTP 502 Bad Gateway selama 10 hingga 30 detik. Error ini berhenti dengan sendirinya setelah Pod versi lama sepenuhnya musnah (Terminated) dan Pod baru berstatus Running.
Deteksi insiden terkonfirmasi melalui log NGINX Ingress:
[error] 1421#1421: *8941291 connect() failed (111: Connection refused) while connecting to upstream, client: 10.244.0.1, server: api.internal, request: "GET /v1/orders HTTP/1.1", upstream: "http://10.244.2.84:3000/v1/orders"Atau varian lain:
upstream prematurely closed connection while reading response headerKueri Prometheus berikut menunjukkan metrik lonjakan error 502 yang berkorelasi tepat dengan waktu deployment:
sum(rate(nginx_ingress_controller_requests{status="502", ingress="api-fiber-ingress"}[1m])) by (status) > 0Root Cause Analysis: Mengapa Terjadi 502?
Kesalahan umum adalah mengira aplikasi Go crash atau kehabisan memori (OOM). Faktanya, 502 Bad Gateway pada momen deployment terjadi karena dua masalah sinkronisasi antara Kubernetes Control Plane, jaringan data plane (kube-proxy), dan siklus hidup proses Go Fiber.
1. Race Condition Deregistrasi Endpoint dan SIGTERM
Ketika Pod ditandai untuk dihapus (fase Terminating), dua proses berjalan secara asinkron tanpa saling menunggu:
- Endpoint De-registration (Data Plane): Kube-apiserver memperbarui objek
EndpointSlice/Endpoints. Node-node lokal melaluikube-proxymemperbarui aturaniptables/IPVS, dan Ingress Controller menghapus IP Pod lama dari pool upstream-nya. Proses propagasi jaringan ini membutuhkan waktu beberapa detik tergantung beban etcd dan latency cluster. - Kubelet Pod Termination (Local Node): Di saat yang bersamaan, kubelet langsung mengirim sinyal
SIGTERMke PID 1 kontainer Pod tersebut.
Jika kontainer menerima SIGTERM dan langsung mematikan proses web server sementara Ingress Controller belum selesai membuang IP Pod tersebut dari pool upstream, Ingress Controller akan tetap meneruskan traffic baru ke IP lama. Hasilnya adalah Connection refused (TCP RST) yang memicu error 502.
2. Penghentian Proses Abrupt Tanpa Graceful Shutdown
Banyak implementasi Go Fiber memanggil app.Listen(":3000") langsung di fungsi main() tanpa menangkap sinyal OS. Begitu sinyal SIGTERM tiba dari kubelet, proses Go Fiber terbunuh instan. Semua koneksi TCP yang sedang aktif (in-flight requests) diputus sepihak oleh OS node, menghasilkan error upstream prematurely closed connection.
Langkah Mitigasi Darurat (Rollback Cepat)
Jika insiden sedang berlangsung di cluster produksi dan degradasi layanan tidak bisa ditoleransi, segera eksekusi rollback sebelum memperbaiki kode:
# Batalkan deployment yang sedang berjalan
kubectl rollout undo deployment/api-fiber -n production
# Pantau hingga ReplicaSet kembali stabil
kubectl rollout status deployment/api-fiber -n productionSolusi 1: Implementasi Graceful Shutdown pada Go Fiber
Go Fiber menyediakan method ShutdownWithContext (atau Shutdown) untuk menutup listener server terlebih dahulu, menolak koneksi baru, dan menunggu hingga koneksi yang sedang berjalan diselesaikan dalam batas timeout tertentu.
Gunakan signal.NotifyContext dari library standar Go untuk menangani sinyal terminasi secara bersih:
package main
import (
"context""errors""log""net/http""os""os/signal""syscall""time""github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New(fiber.Config{
DisableKeepalive: false,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
})
app.Get("/healthz/live", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
app.Get("/healthz/ready", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
app.Get("/v1/orders", func(c *fiber.Ctx) error {
// Simulasi proses I/O
time.Sleep(200 * time.Millisecond)
return c.JSON(fiber.Map{"status": "success"})
})
// Tangkap sinyal OS: SIGINT (Ctrl+C) dan SIGTERM (Kubernetes)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := app.Listen(":3000"); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Gagal menjalankan server Fiber: %v", err)
}
}()
log.Println("Server berjalan di port :3000")
// Blok sampai sinyal terminasi diterima
<-ctx.Done()
log.Println("Menerima sinyal terminasi, memulai graceful shutdown...")
// Beri alokasi waktu untuk menyelesaikan in-flight requests
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := app.ShutdownWithContext(shutdownCtx); err != nil {
log.Printf("Error saat shutdown paksa: %v", err)
}
log.Println("Server Fiber berhasil dimatikan secara bersih.")
}Solusi 2: Konfigurasi Manifest Kubernetes (preStop Hook & Probes)
Graceful shutdown di level aplikasi saja tidak mencegah error jika traffic masih dikirim oleh Ingress Controller ke Pod yang sedang proses shutdown. Masalah ini diselesaikan dengan menyisipkan jeda di lifecycle kontainer melalui preStop hook dan memisahkan konfigurasi probe.
Konfigurasi Deployment Manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-fiber
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app: api-fiber
template:
metadata:
labels:
app: api-fiber
spec:
# Berikan total waktu cukup: preStop sleep (15s) + Fiber shutdown (15s) + buffer (10s)
terminationGracePeriodSeconds: 40
containers:
- name: fiber-app
image: internal-registry.example.com/api-fiber:v1.2.0
ports:
- containerPort: 3000
lifecycle:
preStop:
exec:
# Menunggu kube-proxy dan ingress controller selesai menderegistrasi endpoint
command: ["/bin/sleep", "15"]
livenessProbe:
httpGet:
path: /healthz/live
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz/ready
port: 3000
initialDelaySeconds: 2
periodSeconds: 5
timeoutSeconds: 2
successThreshold: 1
failureThreshold: 2Mengapa preStop Hook Diperlukan?
Ketika Pod masuk ke fase Terminating:
- Kubelet mengeksekusi hook
preStop(sleep 15). SinyalSIGTERMbelum dikirim ke aplikasi Fiber. - Selama 15 detik tersebut, kontrol plane Kubernetes menghapus IP Pod dari
Endpoints/EndpointSlice. - Ingress Controller dan kube-proxy menerima pembaruan tersebut dan berhenti meneruskan traffic baru ke Pod lama. Selama 15 detik ini, Pod lama masih melayani sisa traffic yang terlambat masuk secara normal.
- Setelah
sleep 15selesai, Kubelet mengirimSIGTERM. - Aplikasi Go Fiber memulai
ShutdownWithContext, menyelesaikan koneksi TCP yang masih aktif, lalu keluar dengan exit code 0 tanpa menjatuhkan koneksi satupun.
Catatan: Nilai
terminationGracePeriodSecondsharus selalu lebih besar dari durasipreStop sleepditambah batas waktushutdownCtxdi Go Fiber. Jika tidak, Kubelet akan mengirimSIGKILLsebelum aplikasi selesai menutup koneksinya.
Verifikasi Deployment Tanpa Koneksi Terputus
Gunakan benchmarking tool seperti hey atau k6 untuk membuktikan bahwa rolling deployment berjalan dengan 0% error 502.
Jalankan uji beban traffic konstan ke endpoint:
hey -z 45s -q 20 -c 10 https://api.internal/v1/ordersDi terminal lain, trigger rolling update:
kubectl rollout restart deployment/api-fiber -n productionHasil verifikasi yang benar tidak boleh mencatat status code selain 200:
Status code distribution:
[200] 900 responses
Error distribution:
(none)Postmortem Singkat
- Ringkasan Insiden: Error 502 terjadi selama deploy versi baru akibat koneksi diputus secara paksa saat traffic masih dialirkan ke pod yang sedang dihentikan.
- Faktor Penyebab: Ketiadaan penanganan sinyal
SIGTERMdi Go Fiber dan ketiadaan jeda pelepasan endpoint di level pod lifecycle Kubernetes. - Resolusi: Menambahkan graceful shutdown berbasis konteks pada Go Fiber, menambahkan
preStophook 15 detik pada spesifikasi kontainer, dan menaikkanterminationGracePeriodSecondsmenjadi 40 detik.
Checklist Preventif Rilis Produksi
- Graceful Shutdown: Aplikasi mengonsumsi sinyal OS (
SIGTERM,SIGINT) dan mengeksekusi penutupan HTTP server via context timeout. - PreStop Sleep: Manifest deployment memiliki
preStop: exec: command: ["/bin/sleep", "X"]di manaXdisesuaikan dengan latency propagasi jaringan cluster (umumnya 10–20 detik). - Grace Period Alignment:
terminationGracePeriodSeconds > preStop sleep + app shutdown timeout. - Separation of Probes:
livenessProbedanreadinessProbemenggunakan endpoint terpisah. Hindari meletakkan dependency check (DB ping, Redis ping) padalivenessProbeagar container tidak restart berulang saat dependency mengalami degradasi sementara. - Rolling Update Parameter:
maxUnavailablediset ke0untuk memastikan kapasitas Pod tidak berkurang di bawah baseline saat proses rollout berlangsung.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!