Graceful Shutdown trong Go — Signal, Context & bài học thực tế
Một trong những lỗi backend engineer thường gặp nhất là kill thẳng container khi deploy — dẫn đến request đang xử lý bị drop, database connection bị orphan, worker chết giữa chừng. Bài này mình sẽ chỉ cách implement graceful shutdown trong Go đúng chuẩn production.
Signal — "trời kêu ai nấy tắt"
Linux gửi signal cho process khi muốn nó dừng. Hai signal quan trọng nhất:
- SIGTERM (15): "Xin hãy dừng lại" — clean shutdown, đây là signal Docker gửi khi bạn
docker stop. - SIGINT (2): Ctrl+C — thường dùng dev.
- SIGKILL (9): Không thể catch, không thể ignore — kill thẳng.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigCh
log.Printf("Nhận signal %v, bắt đầu shutdown...", sig)
HTTP Server — Shutdown với context deadline
http.Server.Shutdown() chính là method bạn cần. Nó graceful drain connections cũ, chặn connection mới:
srv := &http.Server{Addr: ":8080"}
// Chạy server trong goroutine riêng
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
// Block chờ signal
<-sigCh
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Shutdown timeout: %v", err)
}
Điểm mấu chốt: timeout 30 giây. Nếu server không drain hết connection trong 30s, context cancel và server shutdown force. Luôn set timeout, đừng để chờ vô hạn.
Worker Pool — Graceful Shutdown với WaitGroup
Nếu bạn có background workers (xử lý queue, consume Kafka), pattern sẽ khác:
var wg sync.WaitGroup
stopCh := make(chan struct{})
// Worker loop
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case <-stopCh:
log.Printf("Worker %d dừng", id)
return
case job := <-jobCh:
processJob(job)
}
}
}(i)
}
// Chờ signal
<-sigCh
close(stopCh) // Báo tất cả worker dừng
wg.Wait() // Chờ workers finish job hiện tại
log.Println("All workers stopped cleanly")
Pattern kết hợp — Server + Workers
Production thường có cả HTTP server lẫn background workers. Pattern mình hay dùng:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Khởi tạo dependencies
db, _ := sql.Open("postgres", dsn)
defer db.Close()
srv := startHTTPServer()
workers := startWorkers(ctx, db)
<-ctx.Done() // Chờ signal
log.Println("Shutting down...")
// 1. Stop nhận job mới
close(workers.inputCh)
// 2. Shutdown HTTP (drain connections)
shutCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
srv.Shutdown(shutCtx)
cancel()
// 3. Chờ workers finish
workers.wg.Wait()
log.Println("Shutdown complete")
}
Bài học production từ deploy thực tế
-
Luôn test graceful shutdown trong CI. Viết integration test: start server, gửi request dài, gửi SIGTERM, kiểm tra response vẫn complete.
-
Health check + readiness probe. Load balancer cần biết server đang draining. Khi nhận SIGTERM, trả
503trên/readyendpoint — load balancer sẽ ngừng route traffic tới bạn. -
Database connection pool. Set
connMaxLifetimehợp lý, shutdown trước khisql.DB.Close(). Drain connection pool trước. -
Kubernetes
terminationGracePeriodSeconds. Mặc định 30s. Nếu bạn nghĩ worker cần 60s, set trong deployment YAML:
spec:
terminationGracePeriodSeconds: 60
Kết luận
Graceful shutdown không phải tính năng "có cũng được". Trong production, nó quyết định bạn có mất request, corrupt dữ liệu, hay không. Pattern cốt lõi: signal.NotifyContext + http.Server.Shutdown + sync.WaitGroup. Chỉ 3 thứ đó đã cover 90% use case.
Bắt đầu implement hôm nay — đừng để deploy đầu tiên mới nhận ra server mình tắt kiểu "cúp cầu dao".