3 điểm bởi GN⁺ 2025-05-06 | 1 bình luận | Chia sẻ qua WhatsApp
  • Graceful shutdownquy trình chặn yêu cầu mới, hoàn tất các yêu cầu đang xử lý và dọn dẹp tài nguyên sau khi ứng dụng nhận tín hiệu kết thúc
  • Trong Go, có thể dùng gói os/signal để tự xử lý trực tiếp các tín hiệu kết thúc như SIGINT, SIGTERM, đồng thời có thể dùng signal.NotifyContext để điều khiển việc kết thúc dựa trên context
  • Khi tắt HTTP server, trước khi gọi Server.Shutdown() nên chặn traffic bằng cách để readiness probe thất bại, đợi vài giây rồi mới thực hiện shutdown để ổn định hơn
  • Mọi handler đều phải có khả năng phát hiện tín hiệu kết thúc từ context và dừng lại, và có thể xử lý việc này một cách thống nhất thông qua BaseContext hoặc middleware
  • Sau khi nhận tín hiệu kết thúc, cần chủ động dọn dẹp các tài nguyên bên ngoài như cơ sở dữ liệu, message broker, cache, và nếu đăng ký bằng defer thì sẽ dễ quản lý thứ tự shutdown hơn

Graceful Shutdown là gì?

  • Graceful shutdown là quá trình khi ứng dụng kết thúc sẽ trải qua các bước chặn yêu cầu mới, chờ hoàn tất các yêu cầu đang chạydọn dẹp tài nguyên
  • Bài viết này chủ yếu nói về HTTP server và môi trường container, nhưng đây là khái niệm có thể áp dụng cho mọi ứng dụng

1. Xử lý tín hiệu kết thúc

  • Trên các hệ thống họ Unix, SIGTERM, SIGINT, SIGHUP thường được dùng làm tín hiệu kết thúc
  • Go runtime mặc định sẽ kết thúc ứng dụng khi nhận SIGTERM, SIGINT, nhưng cũng có thể tự xử lý bằng os/signal.Notify
  • Dùng channel có buffer (dung lượng 1) có thể giúp tránh mất tín hiệu trong lúc khởi tạo
  • Từ Go 1.16 trở đi, signal.NotifyContext giúp việc điều khiển tín hiệu dựa trên context trở nên thuận tiện hơn

2. Nhận thức về thời gian kết thúc

  • Trong Kubernetes, mặc định sẽ có 30 giây thời gian gia hạn kết thúc (terminationGracePeriodSeconds)
  • Để kết thúc an toàn, nên chừa khoảng đệm 20% và hoàn tất việc shutdown trong vòng 25 giây

3. Ngừng nhận yêu cầu mới

  • http.Server.Shutdown() sẽ chặn kết nối mới và chờ cho đến khi các yêu cầu hiện có hoàn tất
  • Trong môi trường Kubernetes, nên để readiness probe thất bại trước để chặn lưu lượng đi vào, sau đó chờ một chút rồi mới shutdown
  • Trong readiness handler, có thể dùng biến toàn cục để xác định trạng thái kết thúc và cấu hình trả về HTTP 503

4. Hoàn tất xử lý yêu cầu

  • Cần đặt timeout phù hợp cho context dùng để shutdown (context.WithTimeout)
  • Khi shutdown context hết hạn, các kết nối còn lại sẽ bị buộc đóng
  • Mọi handler cần được thiết kế để dùng context.Context nhằm phát hiện tín hiệu kết thúc và có thể dừng lại
  • Để làm vậy, có thể tiêm shutdown context vào mọi request thông qua middleware hoặc BaseContext

5. Dọn dẹp tài nguyên

  • Nếu vừa nhận tín hiệu kết thúc đã đóng tài nguyên ngay thì có thể gây vấn đề cho các handler đang xử lý
  • Chỉ sau khi shutdown hoàn tất mới nên dọn dẹp kết nối cơ sở dữ liệu, message broker, cache...
  • Dùng defer trong Go sẽ cho phép chạy các routine shutdown theo thứ tự ngược với lúc khởi tạo, giúp quản lý dependency dễ hơn
  • Ngoài các tài nguyên như bộ nhớ hay file descriptor được OS tự dọn, vẫn còn những tài nguyên cần kết thúc tường minh như flush dữ liệu, rollback transaction

Tóm tắt ví dụ tổng thể

  • Nhận tín hiệu kết thúc bằng signal.NotifyContext
  • Triển khai endpoint readiness /healthz
  • Dùng BaseContext để tiêm shutdown context vào mọi request
  • Chờ readiness 5 giây rồi thực hiện shutdown
  • Bao gồm fallback buộc dừng nếu gọi server.Shutdown thất bại

Tài liệu tham khảo và tài nguyên liên quan

1 bình luận

 
GN⁺ 2025-05-06
Ý kiến trên Hacker News
  • Trong Kubernetes, đôi khi việc cập nhật IP mục tiêu của load balancer mất nhiều thời gian. 90% vấn đề là xác nhận xem lưu lượng có thực sự được drain hay không

    • Việc thêm thời gian chờ 15 giây vào hook preStop dùng chung đã cải thiện đáng kể tỷ lệ HTTP 503
    • Tạo khoảng thời gian giữa việc hủy đăng ký load balancer và gửi SIGTERM giúp đơn giản hóa việc xử lý của ứng dụng
  • Khi dùng log.Fatal, nội dung trong defer sẽ không được chạy

    • log.Fatal gọi os.Exit nên thoát ngay lập tức
    • Nếu dùng panic thì nội dung trong defer sẽ được chạy
  • Khi endpoint Prometheus /metrics được scrape định kỳ, các metric được ghi lại giữa lần scrape cuối và lúc tiến trình kết thúc có thể không được truyền đi

    • Có thể mất log của vài giây cuối khi dịch vụ tắt
    • Có thể phát sinh race condition khi tệp log được một tiến trình sidecar theo dõi
  • Nếu hệ thống phân tán phụ thuộc vào việc client shutdown đúng cách thì hệ thống có thể gặp lỗi nghiêm trọng

  • Thiếu giải thích về cách khởi động lại ứng dụng mà không ngắt kết nối, khi một instance dịch vụ mới nhận socket từ instance cũ

    • Trên systemd, việc triển khai tương đối đơn giản
    • nginx đã hỗ trợ việc này hơn 20 năm
    • Kubernetes và Docker không hỗ trợ việc này
  • Thiếu thảo luận về liveness

    • Đã nhiều lần thấy các ứng dụng dùng cùng một endpoint cho liveness/readiness
  • Nếu chương trình không thể xử lý sạch sẽ các lệnh như ctrl c thì đó là chương trình được viết kém

  • Elixir thiết kế tiến trình dưới dạng các tiến trình VM nhỏ, nên không cần cố ý tạo routine shutdown mềm

  • Đã tạo một thư viện nhỏ trong dự án để xử lý graceful shutdown

    • Cung cấp API để tích hợp các dịch vụ có nhiều cơ chế khởi động và dừng khác nhau
  • Sau khi cập nhật readiness probe, nên chờ vài giây để hệ thống không gửi yêu cầu mới nữa

    • Pod đang tắt sẽ không ở trạng thái sẵn sàng
    • Service sẽ đánh dấu endpoint là đang tắt
    • Dù vẫn có thể tồn tại một khoảng thời gian nhỏ sau SIGTERM, nhưng đây không phải vấn đề lớn
    • Điều quan trọng là không nhận kết nối mới và kết thúc bình thường các kết nối hiện có