Graceful Shutdown trong Go — Tắt app không rớt một request nào
Bạn deploy một bản Go mới lên production. Lệnh đang chạy thì kill — và bất ngờ có một loạt request trả về 502. Người dùng báo app 'chập chờn'. Chuyện gì xảy ra?
Đó là vì process bị giết ngay lập tức, mọi goroutine đang xử lý (kể cả request đang dở, job đang chạy) đều bị bỏ rơi. Giải pháp là graceful shutdown — cho app thời gian hoàn tất công việc rồi mới tự tắt.
Vấn đề: signal và cái chết đột ngột
Khi bạn bấm Ctrl+C (SIGINT) hoặc deploy hệ thống gửi SIGTERM, OS sẽ gửi signal tới process. Nếu app không xử lý, nó chết ngay — giữa chừng.
// Không có gì — mặc định process chết liền khi nhận signal
Cách làm: bắt signal rồi shutdown từ từ
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Block cho tới khi nhận signal
<-ctx.Done()
log.Println("Đang tắt server...")
// Cho tối đa 10s để hoàn tất request đang dở
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Shutdown error: %v", err)
}
log.Println("Đã tắt sạch.")
}
Điểm mấu chốt là srv.Shutdown(ctx) — nó dừng nhận request mới, chờ các request đang chạy xong, và cho bạn timeout để không treo vĩnh viễn.
Còn goroutine nền thì sao?
Server HTTP xử lý xong, nhưng bạn còn background worker (job queue, consumer kafka...). Để chúng tắt gọn:
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
// Worker vừa xử lý vừa lắng nghe cancel
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return // nhận tín hiệu tắt, ngưng việc
case job := <-jobs:
process(job)
}
}
}()
}
<-sigCh // chờ signal
cancel() // báo tất cả worker dừng
wg.Wait() // chờ đủ 4 worker chạy xong việc dở
Workers phải lắng nghe context và tự thoát khỏi vòng lặp khi được báo. Đừng time.Sleep mù mà không kiểm tra context — nếu không, bạn sẽ chờ hụt.
Kinh nghiệm thực tế
- Đặt timeout hợp lý: 10–30s cho HTTP là chuẩn. Quá ngắn thì request dài bị cắt, quá dài thì deploy chậm.
- Shutdown theo thứ tự: trước hết chặn request mới (Ingress/LB), cho HTTP tắt, rồi mới dừng các consumer nền.
- Kết hợp với ReadHeaderTimeout: bảo vệ khỏi slow-client giữ connection mãi.
- Test thật: gửi request dài,
kill -TERM <pid>, xem request có hoàn tất không.
Lời kết
Graceful shutdown chỉ là vài chục dòng code, nhưng cứu cả một buổi tối on-call. Tiến lên production mà không có nó, khác nào cầu thủ bỏ về giữa hiệp — đồng đội (user) kẹt giữa chừng. Ráng 30 phút thêm vào, app của bạn sẽ chững chạc hơn hẳn.