Graceful shutdown
Setiap deploy mematikan container lama. Tanpa penutupan yang rapi, setiap deploy juga memotong setiap permintaan yang sedang berjalan — dan di ECS itu terjadi puluhan kali sehari.
Intisari
srv.Shutdown(ctx)berhenti menerima koneksi baru, lalu menunggu yang sedang berjalan selesai.signal.NotifyContextmengubah SIGTERM jadi pembatalan context — satu baris, tanpa channel manual.- ECS mengirim SIGTERM, lalu SIGKILL setelah
stopTimeout(default 30 detik). - Tidur beberapa detik sebelum mulai menutup — ALB butuh waktu untuk berhenti mengirim permintaan baru.
- Urutan matinya penting: server HTTP dulu, lalu worker, baru koneksi database.
Bentuk lengkapnya
func main() {
// SIGINT (Ctrl-C) dan SIGTERM (ECS, Kubernetes) → pembatalan context.
ctx, hentikanSinyal := signal.NotifyContext(context.Background(),
os.Interrupt, syscall.SIGTERM)
defer hentikanSinyal()
srv := &http.Server{Addr: ":8080", Handler: handler, /* timeout ... */}
go func() {
if err := srv.ListenAndServe(); err != nil &&
!errors.Is(err, http.ErrServerClosed) {
log.Error("server berhenti tak terduga", "err", err)
hentikanSinyal()
}
}()
log.Info("server siap", "addr", srv.Addr)
<-ctx.Done() // menunggu sinyal
hentikanSinyal() // sinyal kedua = mati paksa
log.Info("sinyal berhenti diterima, menutup rapi")
// 1. Beri load balancer waktu menyadari kita mau berhenti.
siap.Store(false) // /siap mulai membalas 503
time.Sleep(5 * time.Second)
// 2. Tutup server HTTP: tolak koneksi baru, tunggu yang berjalan.
ctxMati, batal := context.WithTimeout(context.Background(), 20*time.Second)
defer batal()
if err := srv.Shutdown(ctxMati); err != nil {
log.Error("shutdown melebihi tenggat, memutus paksa", "err", err)
_ = srv.Close()
}
// 3. Hentikan pekerja latar, lalu tunggu mereka.
pekerja.Berhenti()
pekerja.Tunggu(ctxMati)
// 4. Terakhir: tutup koneksi keluar.
db.Close()
rdb.Close()
_ = penyedia.Shutdown(ctxMati) // OpenTelemetry: kirim sisa trace
log.Info("selesai")
}
Kenapa harus tidur dulu
t=0.0 ECS mengirim SIGTERM
t=0.0 ❌ Kalau langsung Shutdown():
ALB masih menganggap task ini sehat dan MASIH mengirim
permintaan baru → koneksi ditolak → pengguna melihat 502.
t=0.0 ✅ Dengan jeda:
/siap mulai membalas 503
t=2.0 ALB gagal health check, menandai target unhealthy
t=5.0 ALB berhenti mengirim permintaan baru → baru Shutdown()
t=5–25 Permintaan yang sedang berjalan diselesaikan
t=25 Proses keluar dengan kode 0
Ini penyebab paling umum "beberapa 502 setiap deploy". Aplikasi menutup diri dengan benar, tapi terlalu cepat — sebelum load balancer sempat tahu. Jedanya harus lebih besar dari (interval health check × ambang unhealthy). Untuk ALB dengan interval 5 detik dan ambang 2, jeda 5–10 detik sudah memadai.
Selaras dengan ECS
| Setelan ECS / ALB | Nilai | Harus |
|---|---|---|
stopTimeout container | 60 dtk | > jeda + Shutdown + worker |
| Deregistration delay target group | 30 dtk | ≥ durasi permintaan terpanjang |
| Health check interval | 5 dtk | — |
| Unhealthy threshold | 2 | interval × ambang < jeda tidur |
Kalau proses belum keluar saat stopTimeout habis, ECS mengirim SIGKILL — dan
SIGKILL tidak bisa ditangkap. Semua yang sedang berjalan mati di tengah: transaksi yang belum di-commit,
pesan antrean yang belum di-ack, berkas yang belum ditutup. Anggaran waktumu harus muat di dalamnya.
Dua endpoint kesehatan, bukan satu
// Liveness: "prosesnya masih hidup?" — JANGAN menyentuh database.
mux.HandleFunc("GET /sehat", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})
// Readiness: "boleh dikirimi lalu lintas?" — boleh memeriksa dependensi,
// dan inilah yang berubah saat shutdown dimulai.
mux.HandleFunc("GET /siap", func(w http.ResponseWriter, r *http.Request) {
if !siap.Load() {
http.Error(w, "sedang berhenti", http.StatusServiceUnavailable)
return
}
ctx, batal := context.WithTimeout(r.Context(), time.Second)
defer batal()
if err := db.PingContext(ctx); err != nil {
http.Error(w, "database tidak siap", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
Health check ALB harus menunjuk ke endpoint yang tidak menyentuh database. Kalau database sempat lambat, health check yang memeriksanya akan menyatakan semua task tidak sehat — ECS lalu mematikan semuanya, dan gangguan sebagian berubah jadi pemadaman total. Ini kesalahan konfigurasi yang mahal, dan muncul lagi di Fase 10.
Yang membuat shutdown menggantung
| Penyebab | Perbaikan |
|---|---|
| Koneksi hijacked (WebSocket) | Shutdown tidak menunggunya — tutup sendiri lewat RegisterOnShutdown |
Handler yang mengabaikan ctx | Teruskan context sampai lapisan terdalam |
| Worker tanpa sinyal berhenti | select dengan <-ctx.Done() (Fase 2) |
| Koneksi keep-alive menganggur | Ditangani Shutdown; pastikan IdleTimeout terset |
| SSE / long polling | Handler harus keluar saat r.Context() selesai |
Latihan: jalankan server dengan endpoint yang tidur 10 detik. Panggil endpoint itu, lalu tekan
Ctrl-C di terminal server saat permintaan masih berjalan. Buktikan permintaannya tetap selesai
dan servernya keluar setelahnya. Lalu ganti srv.Shutdown(ctx) jadi srv.Close()
dan ulangi — perhatikan koneksinya terputus di tengah.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.