Lập trình · 21/09/2026

Graceful Shutdown cho Backend và Worker: Dừng an toàn khi deploy, scale và sự cố

Một tiến trình dừng đúng không chỉ là bắt tín hiệu rồi gọi exit(0). Trong vài giây chuyển tiếp, backend có thể vẫn nhận request mới, giữ kết nối keep-alive, chạy transaction, gửi sự kiện hoặc xử lý job đã lấy khỏi hàng đợi. Nếu tiến trình biến mất quá sớm, người dùng gặp lỗi giữa chừng, job bị thực thi lặp, log chưa kịp ghi và rolling deployment vốn được kỳ vọng không downtime lại tạo ra một đợt lỗi ngắn.

Graceful Shutdown cho Backend và Worker: Dừng an toàn khi deploy, scale và sự cố

Một tiến trình dừng đúng không chỉ là bắt tín hiệu rồi gọi exit(0). Trong vài giây chuyển tiếp, backend có thể vẫn nhận request mới, giữ kết nối keep-alive, chạy transaction, gửi sự kiện hoặc xử lý job đã lấy khỏi hàng đợi. Nếu tiến trình biến mất quá sớm, người dùng gặp lỗi giữa chừng, job bị thực thi lặp, log chưa kịp ghi và rolling deployment vốn được kỳ vọng không downtime lại tạo ra một đợt lỗi ngắn.

Graceful shutdown là giao thức phối hợp giữa load balancer, orchestrator, ứng dụng và các dependency. Mục tiêu là ngừng nhận việc mới, cho công việc đang chạy một khoảng thời gian hữu hạn để hoàn tất, đóng tài nguyên theo thứ tự và buộc dừng khi vượt deadline. Bài viết này trình bày cách áp dụng cùng một mô hình cho HTTP backend và worker xử lý hàng đợi, kèm các failure mode thường bị bỏ sót.

1. Phân biệt shutdown có kiểm soát với chờ vô hạn

Graceful không có nghĩa là chờ mọi công việc bằng mọi giá. Một request treo, truy vấn không có timeout hoặc job bị deadlock có thể giữ tiến trình mãi mãi. Shutdown tốt luôn có hai nhánh: nhánh mềm cố hoàn tất công việc hợp lệ trong một ngân sách thời gian; nhánh cứng buộc đóng khi ngân sách hết để hệ thống có thể tiếp tục deploy, scale hoặc phục hồi.

Kiểu dừngHành viRủi ro chính
Dừng đột ngộtThoát ngay khi nhận tín hiệuCắt request, bỏ transaction, mất log đệm và tạo xử lý lặp
Chờ không giới hạnNgừng nhận việc mới nhưng không có deadlineDeploy bị kẹt, orchestrator cuối cùng vẫn cưỡng bức dừng
Dừng có deadlineDrain, chờ hữu hạn, đóng tài nguyên rồi cưỡng bức nếu cầnCần thiết kế timeout và tính lặp an toàn từ trước

Tiến trình cũng không thể bảo đảm luôn nhận được cơ hội dọn dẹp. Mất điện, kernel panic, lỗi phần cứng, hết bộ nhớ nghiêm trọng hoặc tín hiệu cưỡng bức có thể chấm dứt nó ngay. Vì vậy graceful shutdown giúp giảm lỗi trong các tình huống có thể phối hợp như deploy và scale-down, nhưng không thay thế transaction, idempotency, retry có giới hạn và cơ chế phục hồi sau crash.

2. Vòng đời dừng chuẩn cho một HTTP backend

Một backend nên chuyển qua các trạng thái rõ ràng thay vì chỉ có chạy và tắt. Khi đang ready, instance được phép nhận traffic. Khi nhận yêu cầu dừng, nó chuyển sang draining: readiness báo không sẵn sàng, listener ngừng nhận kết nối mới, còn request đang chạy được theo dõi. Sau khi số request về không hoặc hết deadline, ứng dụng đóng dependency và thoát.

  1. Nhận tín hiệu dừng một lần và ghi log có cấu trúc với lý do, thời điểm, deadline.
  2. Đánh dấu instance không còn ready để bộ định tuyến loại nó khỏi tập đích.
  3. Ngừng nhận connection hoặc request mới ở tầng HTTP.
  4. Chờ request đang chạy hoàn tất trong khoảng thời gian hữu hạn.
  5. Dừng scheduler, consumer và tác vụ nền nằm trong cùng tiến trình.
  6. Flush telemetry có deadline, đóng database pool, message client và tài nguyên khác.
  7. Thoát thành công nếu drain xong; thoát lỗi và ghi metric nếu phải cưỡng bức.

Thứ tự rất quan trọng. Nếu đóng database pool trước khi request cuối cùng hoàn tất, ứng dụng tự phá những công việc mà nó đang cố bảo vệ. Nếu ngừng listener nhưng readiness vẫn xanh, load balancer có thể tiếp tục chọn instance và tạo connection thất bại. Nếu flush telemetry không có giới hạn, chính thư viện quan sát có thể làm shutdown treo.

3. Readiness, deregistration và khoảng trễ của load balancer

Readiness là tín hiệu định tuyến, không phải báo cáo sức khỏe tổng quát. Khi bắt đầu drain, endpoint readiness nên thất bại nhanh để instance không nhận request mới. Tuy nhiên việc loại endpoint thường không tức thời: probe chạy theo chu kỳ, control plane cần cập nhật và proxy có thể giữ connection đã mở. Ứng dụng phải tính cả khoảng lan truyền này trong ngân sách shutdown.

Trong Kubernetes, Pod chấm dứt theo một chuỗi có thời hạn: kubelet khởi động shutdown, có thể chạy lifecycle hook, runtime gửi tín hiệu dừng tới tiến trình chính và cuối cùng cưỡng bức các tiến trình còn lại khi hết grace period. Không nên xem preStop chỉ là một lệnh sleep cố định để che race condition. Hook có thể hữu ích cho deregistration hoặc hành động phối hợp cụ thể, nhưng readiness và khả năng drain của ứng dụng vẫn phải đúng.

Ngân sách của orchestrator phải lớn hơn tổng thời gian lan truyền định tuyến, thời gian hoàn tất công việc hợp lệ và thời gian đóng tài nguyên, đồng thời vẫn chừa khoảng đệm cho dao động thực tế.

Với keep-alive và HTTP/2, một connection có thể mang nhiều request. Server cần ngừng nhận công việc mới theo khả năng của giao thức và framework, nhưng vẫn cho request đã bắt đầu hoàn tất. Hãy kiểm tra hành vi thật của server, ingress và client thay vì suy luận từ tên một hàm như close hoặc shutdown; mỗi thư viện có thể xử lý connection nhàn rỗi và connection hoạt động khác nhau.

4. Bắt tín hiệu đúng và giữ handler đơn giản

Trên môi trường container, ứng dụng thường nhận SIGTERM để bắt đầu dừng và sau đó có thể bị SIGKILL nếu không thoát đúng hạn. Tiến trình ứng dụng phải thực sự là tiến trình nhận tín hiệu hoặc có init phù hợp để chuyển tiếp tín hiệu và thu gom tiến trình con. Một shell wrapper sai cách có thể giữ vị trí PID 1 và không chuyển tín hiệu đến runtime.

Signal handler không nên thực hiện toàn bộ công việc dọn dẹp phức tạp. Nó chỉ cần bảo đảm shutdown được khởi động đúng một lần, lưu nguyên nhân và đánh thức luồng điều phối. Tín hiệu thứ hai có thể được định nghĩa là yêu cầu buộc dừng để hỗ trợ vận hành. Mọi bước phải idempotent vì framework, hệ điều hành hoặc code ứng dụng có thể kích hoạt shutdown từ nhiều nguồn.

let stopping = false;

async function beginShutdown(reason) {
  if (stopping) return;
  stopping = true;
  const deadline = Date.now() + 25_000;

  readiness.markNotReady();
  await httpServer.stopAccepting();
  await inFlight.waitUntilEmpty({ deadline });
  await backgroundTasks.stop({ deadline });
  await telemetry.flush({ deadline });
  await databasePool.close({ deadline });
}

process.on('SIGTERM', () => beginShutdown('SIGTERM'));
process.on('SIGINT', () => beginShutdown('SIGINT'));

Đây là pseudocode, không phải API dùng chung cho mọi runtime. Khi triển khai, cần hiểu promise hoặc callback nào báo listener đã đóng, cơ chế nào đếm request đang chạy, timeout có hủy I/O thật hay chỉ dừng chờ, và lỗi trong từng bước được ghi nhận ra sao. Luôn đặt bộ đếm giờ cưỡng bức ở mức cao nhất để một bước thất bại không nuốt toàn bộ deadline.

5. Quản lý request đang chạy và deadline xuyên suốt

Đếm in-flight ở middleware ngoài cùng để bao phủ cả nhánh thành công, lỗi và request bị client hủy. Tăng bộ đếm khi ứng dụng bắt đầu xử lý và giảm trong finally. WebSocket, server-sent events, streaming upload và streaming response không giống request ngắn; chúng cần chính sách riêng, chẳng hạn thông báo reconnect, từ chối session mới và đóng session cũ sau một thời gian.

Mỗi request vẫn cần deadline độc lập. Nếu grace period là 30 giây nhưng endpoint có thể chạy 10 phút, rolling deployment không thể vừa nhanh vừa luôn chờ endpoint đó. Hãy chuyển tác vụ dài sang job bất đồng bộ, chia nhỏ công việc hoặc cung cấp cơ chế tiếp tục ở instance khác. Deadline của request phải được truyền xuống truy vấn database, HTTP downstream và message operation để việc hủy có hiệu lực xuyên suốt.

  • Không bắt đầu retry mới khi thời gian còn lại không đủ cho một lần thử hợp lệ.
  • Không giữ transaction mở trong lúc chờ dịch vụ ngoài hoặc drain connection.
  • Không trả thành công trước khi trạng thái bắt buộc đã bền vững.
  • Phân biệt client hủy, deadline hết và server shutdown trong log và metric.

Đối với request ghi dữ liệu, phản hồi bị cắt không cho client biết transaction đã commit hay chưa. API cần idempotency key hoặc khóa nghiệp vụ để client retry mà không tạo đơn hàng, thanh toán hay tác vụ trùng. Graceful shutdown làm cửa sổ không chắc chắn nhỏ hơn, nhưng không thể xóa nó khỏi hệ thống phân tán.

6. Worker và hàng đợi cần giao thức khác HTTP

Worker không có listener HTTP để đóng; bước tương đương là ngừng reserve hoặc poll job mới. Sau đó worker xử lý job đang giữ theo quy tắc của hệ thống hàng đợi. Nếu job hoàn tất trong deadline, nó ghi kết quả bền vững rồi acknowledge. Nếu không thể hoàn tất, worker nên hủy an toàn hoặc để lease/visibility timeout hết để job được giao lại.

  1. Chuyển worker sang trạng thái draining và ngừng lấy batch mới.
  2. Theo dõi job đang chạy cùng deadline và lease của từng job.
  3. Hoàn tất commit kết quả trước khi acknowledge message.
  4. Nếu không đủ thời gian, hủy tại checkpoint an toàn hoặc trả job về hàng đợi nếu nền tảng hỗ trợ.
  5. Đóng consumer connection sau khi không còn job đang xử lý.

Thứ tự commit rồi acknowledge tránh mất việc, nhưng crash giữa hai bước có thể khiến job được giao lại. Vì vậy handler phải có tính lặp an toàn: dùng unique constraint, idempotency record, state machine hoặc transactional outbox tùy bài toán. Không đặt niềm tin vào giả định “queue chỉ giao đúng một lần” nếu contract thực tế là at-least-once.

Prefetch hoặc batch reserve quá lớn làm drain khó hơn vì một worker giữ nhiều message chưa kịp xử lý. Cấu hình concurrency, prefetch và visibility timeout phải phù hợp với thời gian job. Job dài nên có checkpoint hoặc tách thành các bước nhỏ; nếu một job không thể bị gián đoạn trong grace period, đó là vấn đề thiết kế vòng đời chứ không chỉ là thiếu vài giây cấu hình.

7. Đóng dependency theo thứ tự phụ thuộc

Sau khi không còn công việc ứng dụng, mới đóng các tài nguyên phục vụ công việc đó. Dừng scheduler trước để nó không sinh tác vụ mới. Flush producer trước khi đóng kết nối message broker. Chờ telemetry trong một khoảng nhỏ rồi bỏ qua nếu hệ thống quan sát không phản hồi. Database pool thường đóng gần cuối vì request và job cần nó để hoàn tất.

Tài nguyênCâu hỏi cần trả lờiLỗi thường gặp
HTTP serverCó từ chối connection mới và chờ request đang chạy không?Chỉ đóng socket lắng nghe nhưng bỏ quên keep-alive
Database poolCó chờ connection mượn ra được trả lại không?Đóng pool trước request cuối cùng
Message consumerCó ngừng fetch trước khi chờ job không?Vẫn reserve job trong lúc drain
Message producerCó flush message đã đệm với timeout không?Thoát trước khi broker xác nhận
TelemetryCó flush hữu hạn và không chặn vô tận không?Mất dấu chính sự kiện shutdown hoặc làm tiến trình treo

Mỗi thao tác close cần được quan sát và có timeout nhỏ hơn deadline toàn cục. Khi một bước lỗi, tiếp tục đóng những tài nguyên còn lại nếu an toàn, ghi lỗi và đặt exit code phù hợp. Gom lỗi thay vì dừng ở exception đầu tiên giúp tránh để lại nhiều connection hoặc tiến trình con không cần thiết.

8. Tính ngân sách shutdown từ SLO và workload

Không nên sao chép một grace period phổ biến rồi coi là hoàn tất. Bắt đầu từ thời gian request và job hợp lệ ở percentile cao, thời gian cập nhật endpoint, thời gian đóng dependency và mục tiêu tốc độ deploy. Một ngân sách minh họa có thể gồm 5 giây cho lan truyền định tuyến, 15 giây cho request đang chạy, 3 giây flush telemetry và 2 giây đệm. Các con số phải đến từ đo lường của hệ thống cụ thể.

termination budget = routing propagation
                   + maximum allowed drain
                   + dependency cleanup
                   + safety margin

Grace period quá ngắn tạo forced termination thường xuyên. Quá dài làm rollout và scale-down chậm, đồng thời che request bất thường. Hãy đặt timeout ứng dụng ngắn hơn giới hạn orchestrator để ứng dụng còn thời gian ghi trạng thái cuối và thoát. Dashboard nên cho thấy phân phối drain duration, không chỉ một giá trị trung bình.

9. Quan sát shutdown như một phần của production

Log bắt đầu và kết thúc shutdown với instance ID, phiên bản, tín hiệu, số request/job đang chạy, deadline, thời gian từng bước và kết quả. Metric tối thiểu gồm tổng lần shutdown theo nguyên nhân, drain duration, số công việc còn lại, forced termination, request bị từ chối trong drain và job bị giao lại. Trace có thể gắn thuộc tính shutdown để phân biệt latency deploy với lỗi dependency.

  • Drain thường xuyên chạm deadline: tìm endpoint dài, job không hủy được hoặc dependency close bị treo.
  • Lỗi tăng trước khi instance biến mất: kiểm tra readiness propagation và connection keep-alive.
  • Job giao lại tăng khi deploy: xem prefetch, thời gian job, thứ tự commit/ack và grace period.
  • Không có log kết thúc: kiểm tra forced kill, OOM, wrapper tín hiệu hoặc telemetry flush.

Exit code cũng là tín hiệu vận hành. Một shutdown theo kế hoạch hoàn tất có thể thoát thành công; một shutdown vượt deadline hoặc gặp lỗi mất dữ liệu tiềm ẩn nên được phân loại rõ. Tránh restart loop do liveness probe coi trạng thái draining có chủ đích là một tiến trình hỏng.

10. Kiểm thử bằng rollout và fault injection

Unit test chỉ xác nhận state machine nội bộ. Cần integration test với server thật, proxy hoặc ingress gần giống production, database và queue. Gửi traffic liên tục, kích hoạt shutdown giữa request ghi dữ liệu, xác nhận không có response thành công giả, không tạo bản ghi trùng và request mới chuyển sang instance khỏe. Với worker, dừng giữa job rồi kiểm tra job hoàn tất đúng một hiệu ứng sau khi được giao lại.

  1. Test tín hiệu đầu tiên khởi động drain đúng một lần và tín hiệu thứ hai buộc dừng theo thiết kế.
  2. Test request nhanh hoàn tất, request vượt deadline bị hủy và socket mới bị từ chối đúng cách.
  3. Test rolling deployment dưới tải đều, tải burst và connection keep-alive dài.
  4. Test database chậm, broker mất kết nối và telemetry endpoint không phản hồi trong lúc dừng.
  5. Test job crash trước commit, sau commit nhưng trước acknowledge và trong lúc gia hạn lease.
  6. Đo tỷ lệ lỗi thực tế ở client thay vì chỉ xem trạng thái Pod hoặc process.

Một bài kiểm thử quan trọng là cố tình đặt deadline rất ngắn để đi qua nhánh cưỡng bức. Nhánh này thường ít được chạy nhưng quyết định hệ thống có phục hồi hay mắc kẹt. Sau đó trả cấu hình về giá trị thực tế và xác nhận rollout không tạo slug lỗi tương đương của hạ tầng: instance cũ biến mất đúng lúc, instance mới chỉ nhận traffic sau khi thật sự ready.

Checklist triển khai

  • Readiness chuyển sang không sẵn sàng ngay khi bắt đầu drain.
  • HTTP server ngừng nhận việc mới và theo dõi chính xác request đang chạy.
  • Worker ngừng reserve job trước khi chờ các job hiện tại.
  • Mọi request, query, job và thao tác close đều có timeout hữu hạn.
  • Grace period lớn hơn ngân sách ứng dụng và có khoảng đệm đo được.
  • Database pool, broker và telemetry được đóng theo thứ tự phụ thuộc.
  • Handler ghi dữ liệu và job chịu được retry, giao lại hoặc kết quả không chắc chắn.
  • Metric và log phân biệt drain thành công với forced termination.
  • Rolling deployment và các điểm crash quan trọng đã được kiểm thử dưới tải.

Tài liệu tham khảo

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.