Langsung ke konten
~/rifqi_abrory

Halaman

Beranda
Tentang
Stack
Pengalaman
Proyek
Tulisan
Kontak
Semua proyek

Proyek

Avrist PINTAR — Agent Portal
Avrist e-Application
Avrist Academy — LMS CMS
Avrist e-Payment
PRUHub
SIYAP

Artikel

Angular signals untuk halaman admin sehari-hari
Typed reactive forms di Angular, tanpa any
Error API yang konsisten di ASP.NET Core dengan ProblemDetails
Meneruskan CancellationToken sampai ke lapisan terbawah di ASP.NET Core
Graceful shutdown untuk service HTTP di Go
Idempotency key: membuat API pembayaran aman untuk di-retry
Form kontak yang tahan spam dengan Next.js Server Actions
Form yang ramah offline di React Native: draft dan antrean submit
Validasi request dan penanganan error di Spring Boot 3
Keyset pagination di SQL Server: ketika OFFSET mulai lambat

Aksi

Ganti ke mode gelap
Read in English
Salin alamat email
Semua artikel

Graceful shutdown untuk service HTTP di Go

Berhenti menerima request baru, selesaikan yang sedang berjalan, lalu tutup database: pola main.go sederhana untuk container dan deploy.

3 menit baca

Di halaman ini

Apa yang terjadi saat deploy

Setiap deploy, scale-down, atau restart container mengirim sinyal ke proses Anda, biasanya SIGTERM. Service Go yang tidak menanganinya akan langsung mati: request yang sedang diproses terputus, client melihat connection reset, dan penulisan database yang setengah jalan bisa di-rollback atau, lebih buruk lagi, diterapkan sebagian oleh retry berikutnya.

Graceful shutdown berarti tiga langkah, dengan urutan ini:

  1. Berhenti menerima koneksi baru.
  2. Biarkan request yang sedang berjalan selesai, dengan batas waktu.
  3. Tutup resource bersama seperti connection pool database.

Standard library sudah menyediakan semua yang dibutuhkan.

Polanya

package main

import (
	"context"
	"database/sql"
	"errors"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	_ "github.com/jackc/pgx/v5/stdlib"
)

func main() {
	// ctx is cancelled on Ctrl+C (SIGINT) or SIGTERM from Docker/Kubernetes.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	if err := run(ctx); err != nil {
		slog.Error("server stopped with error", "err", err)
		os.Exit(1)
	}
}

func run(ctx context.Context) error {
	db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
	if err != nil {
		return err
	}
	defer db.Close() // 3. runs last, after the server has drained

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           routes(db),
		ReadHeaderTimeout: 5 * time.Second,
	}

	serverErr := make(chan error, 1)
	go func() {
		slog.Info("listening", "addr", srv.Addr)
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			serverErr <- err
		}
		close(serverErr)
	}()

	select {
	case err := <-serverErr:
		return err // failed to start, e.g. port already in use
	case <-ctx.Done():
		slog.Info("shutdown signal received")
	}

	// 1 + 2. Stop accepting connections and wait for active requests, at most 20 seconds.
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()

	return srv.Shutdown(shutdownCtx)
}

Beberapa detail yang mudah terlewat:

  • ListenAndServe mengembalikan http.ErrServerClosed begitu Shutdown dipanggil. Itu jalur normal, bukan error.
  • Context untuk shutdown dibuat dari context.Background(), bukan dari ctx. Pada titik itu ctx sudah dibatalkan; memakainya berarti server tidak diberi waktu sama sekali untuk menyelesaikan request.
  • defer db.Close() berjalan setelah Shutdown selesai, sehingga handler tidak pernah melihat pool yang sudah ditutup.

Biarkan handler ikut menyadarinya

Shutdown menunggu handler tetapi tidak menghentikannya. Handler yang lama sebaiknya menghormati context request, yang dibatalkan ketika client memutus koneksi:

func listPolicies(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		rows, err := db.QueryContext(r.Context(),
			`SELECT number, status FROM policies ORDER BY updated_at DESC LIMIT 50`)
		if err != nil {
			http.Error(w, "could not load policies", http.StatusInternalServerError)
			return
		}
		defer rows.Close()
		// ... encode rows as JSON
	}
}

Memakai QueryContext(r.Context(), ...) alih-alih Query(...) berarti request yang ditinggalkan juga menghentikan query-nya.

Sesuaikan dengan masa tenggang platform

Timeout Anda harus lebih pendek dari waktu tunggu platform sebelum mematikan proses secara paksa:

  • docker stop di Docker menunggu 10 detik secara default (bisa diubah dengan --time / stop_grace_period).
  • Kubernetes menunggu terminationGracePeriodSeconds, 30 detik secara default.

Timeout shutdown 20 detik cocok untuk Kubernetes, tetapi tidak untuk default Docker. Atur keduanya dengan sengaja.

Pastikan sinyal sampai ke Go

Di dalam container, sinyal dikirim ke PID 1. Kalau Dockerfile memakai bentuk shell, shell-lah yang menjadi PID 1 dan belum tentu meneruskan SIGTERM:

# Shell form: /bin/sh is PID 1, your app may never see SIGTERM.
CMD ./server

# Exec form: the Go binary is PID 1 and receives the signal directly.
CMD ["./server"]

Readiness saat shutdown

Di belakang load balancer, ada jeda singkat di mana traffic masih diarahkan ke pod yang sudah mulai shutdown. Tambahan yang umum adalah endpoint readiness yang langsung mengembalikan 503 begitu sinyal diterima, lalu jeda singkat sebelum memanggil Shutdown, supaya load balancer berhenti mengirim request baru lebih dulu.

Ringkasan

  • Pakai signal.NotifyContext untuk mengubah SIGTERM menjadi pembatalan context.
  • Panggil srv.Shutdown dengan context timeout yang baru; anggap http.ErrServerClosed sebagai sukses.
  • Tutup database setelah server selesai memproses request yang tersisa.
  • Pakai context request di handler, CMD bentuk exec, dan timeout yang lebih pendek dari milik platform.