Timeout chỉ giới hạn thời gian một lời gọi; deadline propagation biến giới hạn đó thành ngân sách chung cho toàn bộ request. Nếu gateway chờ tối đa hai giây nhưng service phía sau vẫn tiếp tục truy vấn, gọi API và render báo cáo sau khi client đã rời đi, hệ thống đang tiêu CPU, connection và quota cho một kết quả không còn người nhận. Khi lưu lượng tăng, phần việc “mồ côi” này có thể kéo dài hàng đợi và tạo quá tải dây chuyền.
Bài viết trình bày cách đặt time budget ở biên hệ thống, truyền thời gian còn lại qua nhiều service, chia ngân sách cho từng dependency, hủy tác vụ theo cơ chế cooperative cancellation, xử lý database và queue đúng ranh giới, quan sát nguyên nhân hết hạn và kiểm thử các tình huống race. Ví dụ dùng pseudocode cùng Go để nhấn mạnh nguyên tắc, không phụ thuộc một framework cụ thể.
1. Phân biệt timeout, deadline và cancellation
Timeout là khoảng thời gian tối đa một thao tác được phép chạy, chẳng hạn 300 ms. Deadline là thời điểm tuyệt đối mà toàn bộ công việc phải kết thúc. Cancellation là tín hiệu thông báo công việc nên dừng vì deadline đã hết, client ngắt kết nối, request cha thất bại hoặc người dùng chủ động hủy.
Trong một tiến trình, timeout và deadline có thể chuyển đổi cho nhau. Nhưng qua nhiều hop, việc truyền một timeout cố định làm ngân sách vô tình được làm mới. Request đã tiêu 700 ms tại service A mà A vẫn gọi B với timeout hai giây thì tổng thời gian có thể vượt cam kết ban đầu. Truyền thời gian còn lại, hoặc dùng cơ chế deadline của RPC framework, giữ được ý nghĩa end-to-end.
| Cơ chế | Trả lời câu hỏi | Lỗi thiết kế thường gặp |
|---|---|---|
| Timeout | Một thao tác được chạy tối đa bao lâu? | Mỗi hop tự cấp lại cùng một khoảng thời gian |
| Deadline | Request phải hoàn tất trước thời điểm nào? | Truyền timestamp qua máy có clock skew mà không chuẩn hóa |
| Cancellation | Vì sao và khi nào công việc nên dừng? | Phát tín hiệu nhưng code không kiểm tra hoặc dependency không nhận tín hiệu |
| Time budget | Còn bao nhiêu thời gian cho toàn bộ chuỗi? | Dùng hết ngân sách cho hop đầu, không chừa thời gian trả response |
2. Đặt ngân sách ở điểm vào đáng tin cậy
Deadline gốc nên được quyết định tại boundary hiểu mục tiêu sản phẩm: API gateway, BFF hoặc service nhận request công khai. Giá trị cần dựa trên SLO, hành vi người dùng, latency thực đo và chi phí của operation. Endpoint đọc chi tiết không nhất thiết có cùng ngân sách với export báo cáo; tác vụ dài nên chuyển sang mô hình asynchronous thay vì giữ HTTP connection rất lâu.
Nếu client được phép gửi deadline, server cần áp dụng giới hạn tối thiểu và tối đa. Deadline quá ngắn có thể tạo tải vô ích vì operation không thể hoàn tất; deadline quá dài có thể chiếm tài nguyên. Một quy tắc thực dụng là chọn deadline hiệu lực bằng giá trị nhỏ hơn giữa deadline hợp lệ của caller và giới hạn server, rồi từ chối sớm khi thời gian còn lại dưới mức tối thiểu để khởi động an toàn.
effectiveDeadline = min(validatedClientDeadline, now + routeMaximum)
remaining = effectiveDeadline - monotonicNow()
if remaining < minimumUsefulBudget:
return deadlineExceeded()
Dùng monotonic clock để đo duration trong tiến trình vì wall clock có thể thay đổi do đồng bộ thời gian. Khi truyền qua mạng, ưu tiên cơ chế framework chuyển deadline thành thời gian còn lại. Tài liệu gRPC mô tả việc trừ phần thời gian đã tiêu trước khi truyền timeout tiếp, giúp tránh phụ thuộc trực tiếp vào đồng hồ tuyệt đối giữa hai máy.
3. Truyền context qua toàn bộ call graph
Deadline phải đi cùng request context từ handler xuống service, repository, RPC client và thư viện I/O. Không tạo context nền mới ở giữa luồng vì thao tác con sẽ mất liên kết với request cha. Một child có thể đặt deadline ngắn hơn, nhưng không được nới deadline của parent. Khi parent bị hủy, mọi child cần nhận tín hiệu.
func GetDashboard(ctx context.Context, userID string) (Dashboard, error) {
profile, err := profileClient.Get(ctx, userID)
if err != nil { return Dashboard{}, err }
child, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
defer cancel()
orders, err := orderClient.Recent(child, userID)
if err != nil { return Dashboard{}, err }
return assemble(profile, orders), nil
}
Trong Go, tài liệu chuẩn yêu cầu truyền Context tường minh vào hàm cần nó và gọi hàm cancel để giải phóng timer cùng liên kết cha-con. Ngôn ngữ khác có thể dùng cancellation token, abort signal hoặc RPC context. Tên API khác nhau nhưng invariant giống nhau: mọi thao tác chặn phải có đường nhận deadline hoặc tín hiệu hủy.
4. Chia time budget thay vì sao chép nguyên xi
Không phải dependency nào cũng được dùng toàn bộ thời gian còn lại. Handler cần chừa ngân sách để tổng hợp kết quả, serialize, ghi telemetry và gửi response. Với các nhánh song song, mỗi nhánh có thể dùng cùng deadline cha vì chúng chạy đồng thời; với chuỗi tuần tự, cần phân bổ theo mức quan trọng và latency đã đo.
Giả sử request còn 900 ms. Service có thể giữ 100 ms cho response, cấp tối đa 450 ms cho truy vấn chính và 250 ms cho lời gọi bổ trợ; phần còn lại là biên dao động. Đây không phải tỷ lệ cố định cho mọi request. Hãy dùng histogram theo route và dependency, xem p95/p99, sau đó điều chỉnh. Deadline quá chặt làm tăng lỗi giả; quá rộng làm failure chậm và giữ tài nguyên lâu.
- Trừ connection setup, queue wait và thời gian đã xử lý khỏi ngân sách còn lại.
- Đặt per-attempt timeout nhỏ hơn deadline tổng nếu có retry.
- Không bắt đầu retry khi thời gian còn lại không đủ cho một attempt hữu ích.
- Chừa response reserve để upstream nhận lỗi có cấu trúc thay vì socket bị cắt.
5. Cancellation là hợp tác, không phải kill cưỡng bức
Phần lớn runtime không thể an toàn dừng một hàm tùy ý tại bất kỳ instruction nào. Cancellation thường là cooperative: runtime đóng một channel, đặt token hoặc hoàn tất promise; code ứng dụng và thư viện phải kiểm tra tín hiệu tại các điểm phù hợp. Vòng lặp CPU dài cần checkpoint, I/O phải nhận context, và goroutine hoặc task con phải được join hoặc dọn dẹp.
for batch in batches:
if context.isCancelled():
releaseTemporaryResources()
return cancelled
process(batch)
Checkpoint quá thưa làm hủy chậm; kiểm tra trong từng phép tính nhỏ tạo overhead không cần thiết. Chọn ranh giới sau một batch, trước một I/O mới và trước khi tạo side effect. Cleanup phải có giới hạn riêng: hủy request không có nghĩa cleanup được phép treo vô hạn. Những thao tác bắt buộc như trả connection về pool nên ngắn, idempotent và không phụ thuộc context đã hủy nếu runtime yêu cầu một context cleanup riêng.
6. Database: hủy câu lệnh không đồng nghĩa rollback business
Driver database nên nhận context để có thể hủy query hoặc ngừng chờ connection. Tuy vậy, khả năng hủy thật sự phụ thuộc driver và database; cần kiểm chứng bằng integration test. Nếu deadline hết giữa transaction, ứng dụng phải rollback, không tái sử dụng transaction ở trạng thái không rõ và không trả response thành công chỉ vì một statement trước đó đã chạy.
Side effect đã commit không thể được “hủy” chỉ bằng token. Ví dụ thanh toán đã ghi nhận nhưng client hết deadline trước khi nhận response. Luồng ghi cần idempotency key, trạng thái có thể truy vấn lại và quy trình reconcile. Với nhiều hệ thống, transactional outbox giúp commit dữ liệu cùng ý định phát sự kiện; cancellation chỉ ngăn công việc chưa bắt đầu hoặc chưa commit, không đảo ngược lịch sử.
Hãy xem cancellation là quyền ngừng công việc tương lai, không phải lời hứa hoàn tác mọi side effect đã xảy ra.
7. HTTP, gRPC và ranh giới protocol
gRPC có khái niệm deadline và trạng thái DEADLINE_EXCEEDED; tài liệu chính thức cũng lưu ý server application phải chủ động dừng hoạt động đã tạo ra. Với HTTP nội bộ, tổ chức có thể truyền remaining budget qua một header được gateway kiểm soát, nhưng phải chuẩn hóa đơn vị, giới hạn giá trị, bỏ header không đáng tin từ internet và không dùng timestamp thô như một bằng chứng bảo mật.
Khi bridge giữa protocol, hãy ánh xạ cả deadline lẫn nguyên nhân. Client disconnect khác dependency timeout và khác server overload. Không nên biến tất cả thành HTTP 500. Response public cần ổn định và không lộ topology; telemetry nội bộ có thể chi tiết hơn với các nhãn như client_cancelled, deadline_exceeded, dependency_timeout và local_budget_rejected.
8. Fan-out, tác vụ song song và kết quả từng phần
Một aggregator gọi mười service song song phải hủy các nhánh còn lại khi kết quả bắt buộc đã thất bại hoặc deadline chung hết. Nếu sản phẩm cho phép partial response, cần phân loại trường nào bắt buộc, trường nào tùy chọn và trả metadata rõ ràng. Không âm thầm dùng dữ liệu thiếu rồi khiến client hiểu đó là kết quả đầy đủ.
Dùng structured concurrency khi runtime hỗ trợ: task con thuộc một scope, lỗi và cancellation có đường truyền xác định, handler không kết thúc khi task con còn chạy lạc. Khi triển khai kiểu “first successful response”, hủy các bản sao thua cuộc và theo dõi xem dependency có thực sự dừng hay vẫn tiếp tục xử lý.
9. Queue và job nền cần ranh giới khác request đồng bộ
Không nên truyền nguyên deadline vài giây của HTTP vào một job dự kiến chạy nhiều phút rồi coi job hết hạn ngay khi request đóng. Request đồng bộ chỉ cần bảo đảm enqueue thành công; job có lifecycle, TTL và cancellation policy riêng. Message nên mang expires_at nếu kết quả mất giá trị sau một thời điểm, còn worker kiểm tra trước khi bắt đầu và giữa các phase tốn kém.
Nếu người dùng bấm hủy export, lưu trạng thái cancellation bền vững để worker đọc được; một token chỉ tồn tại trong process không đủ qua retry hoặc restart. Xác định rõ phase nào có thể dừng, phase nào phải hoàn tất để dữ liệu nhất quán, và artifact tạm nào cần xóa. Acknowledge message chỉ sau khi trạng thái cuối đã được ghi an toàn.
10. Retry phải nằm trong deadline tổng
Retry không được làm mới ngân sách. Trước mỗi attempt, tính remaining budget, trừ backoff dự kiến và chỉ chạy khi còn đủ thời gian. Giới hạn số attempt, thêm jitter và chỉ retry lỗi transient với operation an toàn hoặc idempotent. Nếu attempt đầu dùng gần hết deadline, attempt thứ hai thường chỉ tăng tải mà không có cơ hội thành công.
for attempt in policy:
remaining = deadline.remaining()
if remaining < minimumAttempt + responseReserve:
return deadlineExceeded
result = call(timeout=min(perAttemptLimit, remaining-responseReserve))
if result.ok or !isRetryable(result): return result
wait(jitteredBackoffWithin(remaining))
Circuit breaker, load shedding và concurrency limit bổ trợ cho deadline: deadline giới hạn thời gian một request; các cơ chế kia ngăn cả hệ thống nhận quá nhiều công việc hoặc tiếp tục gọi dependency đang lỗi. Không cơ chế nào thay thế hoàn toàn cơ chế còn lại.
11. Observability phải chỉ ra ngân sách bị tiêu ở đâu
Mỗi trace nên ghi deadline hoặc remaining budget ở entry, queue time, thời điểm bắt đầu dependency và kết quả cancellation. Span không nên chỉ báo “timeout”; hãy phân biệt timeout kết nối, timeout đọc, deadline cha, client disconnect và từ chối sớm vì ngân sách không đủ. Log có request ID, route và dependency nhưng không ghi token hoặc dữ liệu nhạy cảm.
- Đo tỷ lệ request bị hủy theo route và nguyên nhân.
- Đo lượng công việc tiếp tục chạy sau khi parent đã hủy.
- Theo dõi connection, goroutine hoặc task không được giải phóng.
- So sánh latency thành công với deadline để tìm cấu hình quá chặt hoặc quá rộng.
- Cảnh báo khi một service nhận nhiều request với remaining budget gần bằng 0.
Không dùng status cancellation làm bằng chứng duy nhất rằng dependency đã ngừng. Kết hợp trace với metric tài nguyên và test fault injection để xác nhận CPU, query và outbound call thực sự giảm sau khi hủy.
12. Kiểm thử deadline như một tính chất end-to-end
Unit test với fake clock giúp kiểm tra phép chia budget mà không làm test chậm. Integration test cần dependency cố ý trì hoãn, client ngắt kết nối, query dài, race giữa response và cancellation, cùng trường hợp cleanup lỗi. Đo rằng handler trả đúng mã, task con kết thúc, transaction rollback và pool không mất connection.
- Gửi request với deadline đã hết và xác nhận fail fast, không gọi dependency.
- Tiêu một phần budget ở service đầu rồi xác nhận service sau chỉ nhận phần còn lại.
- Hủy client trong lúc fan-out và xác nhận mọi nhánh được dừng hoặc hoàn tất có kiểm soát.
- Cho side effect commit sát deadline và kiểm tra idempotency cùng trạng thái truy vấn lại.
- Fault injection vào DNS, connection pool và database để phân biệt từng loại timeout.
13. Checklist triển khai theo từng bước
- Boundary: đặt deadline theo route, SLO và giới hạn server; từ chối giá trị client bất hợp lý.
- Propagation: truyền context xuyên handler, service, repository và RPC; không thay bằng background context.
- Budget: cấp per-operation timeout nhỏ hơn remaining time và giữ response reserve.
- Cancellation: thêm checkpoint, truyền tín hiệu vào I/O, dọn task con và timer.
- Consistency: rollback transaction; dùng idempotency và reconcile cho side effect đã commit.
- Async: tách TTL/cancellation của job khỏi vòng đời HTTP và lưu trạng thái hủy bền vững.
- Telemetry: phân loại nguyên nhân, đo orphan work và kiểm tra tài nguyên thực sự được giải phóng.
- Tests: dùng fake clock, fault injection và integration test qua nhiều hop.
Một hệ thống đáng tin cậy không chỉ trả lỗi đúng lúc; nó còn ngừng tiêu tài nguyên cho kết quả đã mất giá trị. Khi deadline trở thành ngân sách end-to-end và cancellation được thiết kế xuyên mọi tầng, latency dễ dự đoán hơn, retry có kỷ luật hơn và sự cố dependency ít có cơ hội lan thành quá tải toàn hệ thống.




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