- Graceful shutdown là quy 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ạy và dọ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
Ý 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
preStopdùng chung đã cải thiện đáng kể tỷ lệ HTTP 503SIGTERMgiúp đơn giản hóa việc xử lý của ứng dụngKhi dùng
log.Fatal, nội dung trongdefersẽ không được chạylog.Fatalgọios.Exitnên thoát ngay lập tứcpanicthì nội dung trongdefersẽ được chạyKhi 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 điNế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ũ
Thiếu thảo luận về liveness
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
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
SIGTERM, nhưng đây không phải vấn đề lớn