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:
- Berhenti menerima koneksi baru.
- Biarkan request yang sedang berjalan selesai, dengan batas waktu.
- 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:
ListenAndServemengembalikanhttp.ErrServerClosedbegituShutdowndipanggil. Itu jalur normal, bukan error.- Context untuk shutdown dibuat dari
context.Background(), bukan darictx. Pada titik ituctxsudah dibatalkan; memakainya berarti server tidak diberi waktu sama sekali untuk menyelesaikan request. defer db.Close()berjalan setelahShutdownselesai, 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 stopdi 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.NotifyContextuntuk mengubahSIGTERMmenjadi pembatalan context. - Panggil
srv.Shutdowndengan context timeout yang baru; anggaphttp.ErrServerClosedsebagai sukses. - Tutup database setelah server selesai memproses request yang tersisa.
- Pakai context request di handler,
CMDbentuk exec, dan timeout yang lebih pendek dari milik platform.