Deploy backend không chỉ là build image mới rồi để Kubernetes rolling update. Với hệ thống production có request đang chạy, connection pool, worker queue, message consumer hoặc WebSocket/gRPC stream, một pod bị kill sai thời điểm có thể tạo ra 502/503, request bị cắt giữa chừng, double processing, mất event hoặc retry storm.

Graceful shutdown là cách để service ngừng nhận traffic mới, hoàn tất phần việc đang xử lý trong một ngân sách thời gian rõ ràng, đóng tài nguyên đúng thứ tự, rồi mới thoát process. Trong Kubernetes, graceful shutdown không phải một option “nice to have”; nó là một phần của contract vận hành giữa application, Service endpoint, kubelet, load balancer và dependency phía sau.

Sơ đồ graceful shutdown cho backend trên Kubernetes từ SIGTERM readiness fail drain request đến đóng tài nguyên và exit
Shutdown đúng không chỉ là bắt SIGTERM, mà là cả một contract drain traffic, chờ in-flight work và đóng tài nguyên theo thứ tự an toàn.

Vì sao backend hay mất request khi rolling deploy?

Một rolling deploy nhìn trên dashboard có thể rất “xanh”: Deployment available, pod mới Ready, pod cũ Terminating. Nhưng user vẫn thấy lỗi ngắn trong vài chục giây. Lý do thường nằm ở khoảng lệch giữa các thành phần: Kubernetes gửi SIGTERM, endpoint bị gỡ dần khỏi Service, ingress/load balancer còn propagation lag, còn application có thể đã thoát quá nhanh hoặc đóng DB pool khi request cũ chưa xong.

Nếu app nhận SIGTERM rồi exit(0) ngay, request đang xử lý bị cắt. Nếu app chỉ sleep 30s mà không fail readiness, pod vẫn có thể nhận request mới trong lúc chờ. Nếu app chờ vô hạn, rollout bị kẹt và kubelet cuối cùng vẫn gửi SIGKILL khi hết grace period.

Graceful shutdown tốt phải đạt ba mục tiêu cùng lúc:

  • Không nhận thêm request hoặc job mới sau khi bắt đầu shutdown.
  • Cho phần việc đang chạy thời gian hoàn tất trong giới hạn an toàn.
  • Thoát dứt khoát để rollout, autoscaling hoặc node drain không bị treo.

Kubernetes termination lifecycle: thứ tự thật sự quan trọng

Khi pod bị xóa, scale down, rolling update hoặc node drain, luồng termination đi theo logic: pod chuyển Terminating, preStop hook chạy nếu có, kubelet gửi SIGTERM, EndpointSlice dần bỏ pod khỏi danh sách endpoint, kubelet chờ tối đa terminationGracePeriodSeconds, rồi gửi SIGKILL nếu process chưa thoát.

Timeline Kubernetes termination cho thấy preStop SIGTERM drain app và giới hạn SIGKILL trong terminationGracePeriodSeconds
preStop và phần drain trong app cùng tiêu thụ chung một ngân sách grace period.

Điểm dễ sai là preStop và thời gian app xử lý SIGTERM dùng chung grace period. Nếu đặt terminationGracePeriodSeconds: 30 nhưng preStop ngủ 20 giây, app chỉ còn khoảng 10 giây để hoàn tất request, flush telemetry và đóng tài nguyên.

Ngoài ra, pod bị gỡ khỏi endpoint không có nghĩa traffic dừng ngay lập tức. Ingress controller, external load balancer, service mesh, keep-alive connection và client retry vẫn có thể gửi request thêm một khoảng ngắn. Vì vậy application phải có trạng thái draining: fail readiness, từ chối request mới có kiểm soát, nhưng cho request cũ hoàn tất theo deadline.

Shutdown contract cho backend service

Một backend service production nên có contract shutdown rõ ràng, không chỉ “bắt signal”. Tối thiểu gồm 5 pha:

  1. Nhận tín hiệu shutdown: lắng nghe SIGTERMSIGINT; PID 1 phải forward signal đúng.
  2. Đổi readiness sang fail: báo instance không nên nhận traffic mới.
  3. Ngừng nhận connection hoặc new work: đóng listener HTTP/gRPC, dừng poll message mới ở worker.
  4. Chờ in-flight work: track request/job đang chạy và cho chúng thời gian hoàn tất trong deadline.
  5. Đóng tài nguyên theo thứ tự an toàn: chỉ đóng DB/Redis/Kafka sau khi in-flight work xong hoặc đã bị cancel.

Đây là cùng một kiểu tư duy production với Deploy Backend lên Production Checklist: contract rollout phải rõ và đo được, không phải chỉ “thử xem ổn không”.

Vì sao readiness chưa đủ?

Readiness nên trả lời câu hỏi “instance này có nên nhận traffic mới không?”. Khi bắt đầu shutdown, app nên chuyển /readyz sang fail ngay. Nhưng readiness fail không tự động làm biến mất mọi request mới trong cùng milli giây. Vẫn có propagation lag ở EndpointSlice, ingress, proxy và keep-alive connection.

Vì vậy cần thêm policy ứng dụng:

  • Request mới đến trong lúc draining có thể trả 503 Service Unavailable kèm Connection: close.
  • Request đã bắt đầu thì được cho hoàn tất nếu còn trong deadline.
  • /metrics hoặc /livez vẫn có thể hoạt động để hỗ trợ quan sát.

Với microservices chịu tải, graceful shutdown còn phối hợp với backpressure và load shedding: khi pod rút khỏi service, hệ thống phải giảm tải có kiểm soát thay vì tạo retry storm.

preStop hook: dùng cẩn thận, đừng biến thành sleep vô nghĩa

Nhiều team thêm cấu hình kiểu sleep 10-20 giây trong preStop. Cách này đôi khi giảm lỗi ngắn hạn vì cho load balancer thời gian gỡ endpoint, nhưng nếu chỉ sleep mà app không biết mình đang draining, pod vẫn có thể nhận request mới qua connection còn mở. Tệ hơn, sleep ăn vào grace period và làm phần shutdown thật của app bị thiếu thời gian.

preStop hữu ích hơn khi gọi một endpoint drain nội bộ để app:

  • fail readiness;
  • bật cờ draining;
  • ngừng nhận job mới;
  • trả về nhanh, không ngủ dài.

Tuy vậy, app vẫn phải xử lý SIGTERM. Đừng phụ thuộc hoàn toàn vào preStop.

Readiness, liveness và startup probe trong shutdown

Ba probe nên trả lời ba câu hỏi khác nhau:

  • startupProbe: app đã khởi động xong chưa?
  • livenessProbe: process có bị kẹt và cần restart không?
  • readinessProbe: instance này có nên nhận traffic không?

Khi shutdown bắt đầu, readiness nên fail. Liveness thường không nên fail chỉ vì service đang draining; nếu fail liveness trong lúc shutdown, kubelet có thể giết/restart process theo hướng khó kiểm soát hơn.

Timeout budget: shutdown phải khớp với request timeout

Sơ đồ timeout budget cho graceful shutdown gồm client ingress app database và termination grace period
Timeout giữa client, ingress, app, database và grace period phải có thứ tự hợp lý; nếu không, request sẽ bị cắt ở layer ngoài trước khi app drain xong.

Graceful shutdown dễ thất bại khi timeout ở các layer không khớp nhau. Ví dụ ingress timeout 30s, app request timeout 60s nhưng grace period chỉ 30s. Trong cấu hình này, request dài sẽ bị cắt bởi ingress hoặc kubelet trước khi app tự hoàn tất.

client timeout >= ingress/proxy timeout >= app request timeout >= DB statement timeout
terminationGracePeriodSeconds > app request timeout + drain buffer + cleanup buffer

Không phải service nào cũng cần grace period dài. API read nhanh có thể chỉ cần 20-30 giây. Endpoint export report, batch worker, payment callback hoặc gRPC stream cần policy riêng: async job, checkpoint, cancel/resume hoặc tách deployment.

HTTP keep-alive, gRPC, WebSocket và worker queue

Đừng chỉ nghĩ về request ngắn.

  • HTTP/1.1 keep-alive: client có thể giữ connection cũ và gửi request mới; server nên đóng listener và khiến connection đóng dần khi draining.
  • HTTP/2/gRPC: nên dùng graceful stop/GOAWAY tương ứng để không mở stream mới trên connection cũ.
  • WebSocket hoặc long-lived stream: cần close frame/policy rõ, không chờ vô hạn.
  • Worker/consumer: ngừng poll message mới, ack/commit chỉ sau khi side effect hoàn tất, nếu gần hết deadline thì để message redeliver.
Luồng graceful shutdown cho worker queue dừng poll message mới xử lý message hiện tại ack sau side effect hoặc redeliver
Worker shutdown an toàn là chống double processing: dừng poll trước, chỉ ack sau khi side effect bền vững.

Đây là chỗ graceful shutdown nối trực tiếp với Outbox Patternidempotency. Nếu side effect không idempotent hoặc ack quá sớm, pod bị kill giữa chừng có thể làm mất event hoặc tạo dữ liệu nửa vời.

Observability: đo shutdown như một production flow

Nếu không đo, graceful shutdown chỉ là niềm tin. Nên có metric/log cho:

  • shutdown_started_total theo signal hoặc reason;
  • thời điểm readiness chuyển fail;
  • số request/job đang chạy tại lúc shutdown;
  • drain duration thực tế;
  • số request bị reject do draining;
  • số lần vượt deadline hoặc bị force kill;
  • số job bị cancel/redeliver.

Nếu đã có tracing, gắn event shutdown vào span hoặc log correlation để lúc rollout có lỗi 5xx có thể nối giữa deployment event, ingress error, app shutdown log và latency spike. Bài Distributed Tracing cho Microservices là nền tốt để làm việc đó.

Checklist triển khai graceful shutdown cho backend production

  • App nhận SIGTERM đúng trong container; PID 1 không nuốt signal.
  • Khi shutdown bắt đầu, app chuyển sang draining state và fail /readyz.
  • HTTP/gRPC server dùng shutdown API chính thức để ngừng nhận request mới.
  • Request/job đang chạy được track và chờ trong deadline, không dựa vào sleep mù.
  • Request mới trong lúc draining bị reject có kiểm soát, không treo vô hạn.
  • Worker/consumer dừng fetch message mới trước khi đóng tài nguyên.
  • Ack/commit chỉ sau khi side effect hoàn tất; redelivery phải an toàn nhờ idempotency.
  • DB/Redis/Kafka chỉ đóng sau khi in-flight work đã xong hoặc bị cancel.
  • preStop không sleep quá dài và không ăn hết grace period.
  • Timeout giữa client, ingress, app và DB không mâu thuẫn.
  • Có metric/log cho shutdown start, drain duration, remaining work và forced cancel.
  • Đã test bằng rollout restart, delete pod, scale down và node drain simulation.

Kết luận

Graceful shutdown là một phần của thiết kế backend production, không phải vài dòng YAML. Kubernetes chỉ cung cấp lifecycle và grace period; application vẫn phải biết cách drain, ngừng nhận work mới, chờ in-flight work, đóng tài nguyên đúng thứ tự và thoát trong deadline.

Nếu team đang gặp lỗi 502/503 ngắn khi deploy, hãy audit theo thứ tự: signal handling, readiness khi draining, server shutdown API, timeout budget, worker ack/commit, terminationGracePeriodSeconds, rồi mới đổ lỗi cho Kubernetes hay ingress. Một rollout “zero downtime” không đến từ may mắn; nó đến từ shutdown contract được thiết kế và test như mọi production flow khác.