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 header

Kueri 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) > 0

Root 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:

  1. Endpoint De-registration (Data Plane): Kube-apiserver memperbarui objek EndpointSlice/Endpoints. Node-node lokal melalui kube-proxy memperbarui aturan iptables/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.
  2. Kubelet Pod Termination (Local Node): Di saat yang bersamaan, kubelet langsung mengirim sinyal SIGTERM ke 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 production

Solusi 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: 2

Mengapa preStop Hook Diperlukan?

Ketika Pod masuk ke fase Terminating:

  1. Kubelet mengeksekusi hook preStop (sleep 15). Sinyal SIGTERM belum dikirim ke aplikasi Fiber.
  2. Selama 15 detik tersebut, kontrol plane Kubernetes menghapus IP Pod dari Endpoints / EndpointSlice.
  3. 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.
  4. Setelah sleep 15 selesai, Kubelet mengirim SIGTERM.
  5. Aplikasi Go Fiber memulai ShutdownWithContext, menyelesaikan koneksi TCP yang masih aktif, lalu keluar dengan exit code 0 tanpa menjatuhkan koneksi satupun.

Catatan: Nilai terminationGracePeriodSeconds harus selalu lebih besar dari durasi preStop sleep ditambah batas waktu shutdownCtx di Go Fiber. Jika tidak, Kubelet akan mengirim SIGKILL sebelum 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/orders

Di terminal lain, trigger rolling update:

kubectl rollout restart deployment/api-fiber -n production

Hasil 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 SIGTERM di Go Fiber dan ketiadaan jeda pelepasan endpoint di level pod lifecycle Kubernetes.
  • Resolusi: Menambahkan graceful shutdown berbasis konteks pada Go Fiber, menambahkan preStop hook 15 detik pada spesifikasi kontainer, dan menaikkan terminationGracePeriodSeconds menjadi 40 detik.

Checklist Preventif Rilis Produksi

  1. Graceful Shutdown: Aplikasi mengonsumsi sinyal OS (SIGTERM, SIGINT) dan mengeksekusi penutupan HTTP server via context timeout.
  2. PreStop Sleep: Manifest deployment memiliki preStop: exec: command: ["/bin/sleep", "X"] di mana X disesuaikan dengan latency propagasi jaringan cluster (umumnya 10–20 detik).
  3. Grace Period Alignment: terminationGracePeriodSeconds > preStop sleep + app shutdown timeout.
  4. Separation of Probes: livenessProbe dan readinessProbe menggunakan endpoint terpisah. Hindari meletakkan dependency check (DB ping, Redis ping) pada livenessProbe agar container tidak restart berulang saat dependency mengalami degradasi sementara.
  5. Rolling Update Parameter: maxUnavailable diset ke 0 untuk memastikan kapasitas Pod tidak berkurang di bawah baseline saat proses rollout berlangsung.