Bỏ qua để đến nội dung
Knowledge Hub

Timeout budget: một request có bao nhiêu thời gian để hoàn thành? Trung cấp

Thời gian đọc ước tính: 6 phút

Request tạo đơn đi qua gateway, service đơn hàng, inventory và payment. Mỗi tầng tự đặt timeout 2 giây. Nhìn từng call thì an toàn; nhìn cả chuỗi, client đã bỏ cuộc sau 1,5 giây mà service đơn hàng vẫn gọi payment. Client retry tạo thêm thao tác, còn server giữ connection, worker và transaction cho việc không ai chờ.

Vấn đề không nằm ở con số “2 giây” mà ở ranh giới trách nhiệm về thời gian: mỗi tầng cần biết request còn bao lâu và có nên bắt đầu bước tiếp theo không.

Khái niệm Ý nghĩa Ví dụ
Timeout Giới hạn một thao tác Gọi inventory tối đa 300 ms
Deadline Thời điểm muộn nhất toàn request còn ý nghĩa 15:00:01.500
Remaining budget Deadline trừ hiện tại; chỉ giảm Còn 330 ms
Cancellation Tín hiệu “đừng làm tiếp nếu có thể” AbortSignal

Timeout thực tế của mỗi call là min(timeout riêng, remaining - margin). Hết budget thì không khởi động call mới.

Ngân sách thời gian giảm dần qua các bước của request
Ngân sách thời gian giảm dần qua các bước của requestBước 1/4: Client bắt đầu request với budget 500 ms. API ghi nhận deadline trước khi xử lý.ClientAPIDependency500 ms
Client → API500 ms
Bước 1/4

Client bắt đầu request với budget 500 ms. API ghi nhận deadline trước khi xử lý.

Truyền qua mạng: gRPC không gửi thời điểm tuyệt đối mà đổi deadline thành timeout đã trừ thời gian trôi qua, tránh clock skew; Java và Go propagate mặc định, C++ cần bật (gRPC deadlines). Nếu tự xây HTTP header, định nghĩa rõ đơn vị, cap tối đa, xử lý header giả mạo và cách trừ thời gian. Trong process, dùng đồng hồ monotonic (performance.now()), không dùng wall clock.

Target 800 ms. Gateway tốn 100 ms. Service đơn hàng giữ 80 ms cuối cho ghi response và dọn dẹp. Trần từng bước: inventory 300 ms, payment 400 ms (tổng trần lớn hơn budget là chủ ý).

Bước Bắt đầu Trần riêng Còn lại trước bước (trừ 80 ms dự trữ) Timeout áp dụng Thực tế dùng
Gateway 0 - - - 100 ms
Inventory 100 ms 300 620 min(300, 620) = 300 290 ms
Payment 390 ms 400 330 min(400, 330) = 330 timeout ở 330 ms
Dự trữ 720 ms - - - 80 ms, kết thúc đúng 800 ms

Nếu payment giữ nguyên trần 400 ms, request vượt deadline 70 ms. Payment timeout thì API lưu idempotency key, trả lỗi theo contract (problem+json) và cho client tra cứu kết quả; không được tự tạo payment mới.

Timeout phải co lại khi đi vào trong: client lớn hơn gateway, gateway lớn hơn service, service lớn hơn dependency. Nếu đảo lại, tầng ngoài bỏ cuộc trước khi tầng trong kịp trả lỗi tử tế.

Tầng Loại timeout Mặc định đã xác minh
nginx proxy_connect_timeout Thiết lập connection tới upstream 60 s (thực tế không vượt 75 s)
nginx proxy_read_timeout Giữa hai lần đọc liên tiếp, không phải cả response 60 s
AWS ALB connection idle timeout Connection không có dữ liệu 60 s (range 1-4000 s)
Application Keep-alive/idle của server Đặt lớn hơn idle timeout của LB, nếu không LB có thể gửi vào connection vừa đóng và trả 502 (khuyến nghị của AWS)

Mặc định 60 s thường lớn hơn nhiều so với budget API 800 ms, nên chúng chỉ là lưới an toàn cuối; deadline ứng dụng mới quyết định hành vi thật. Phân biệt ba loại: connect (nên ngắn), read/response (theo budget), idle (cho connection pooling).

AbortSignal.timeout() có từ Node 17.3/16.14; AbortSignal.any() có từ Node 20.3 / 18.17.

const remaining = deadlineMs - performance.now();
if (remaining <= 50) throw new Error('deadline exceeded');
const signal = AbortSignal.any([
callerSignal, // client ngắt kết nối
AbortSignal.timeout(Math.min(400, remaining - 50)), // trần dependency và budget
]);
const res = await fetch(paymentUrl, { signal });

abort() chỉ hủy phía local; downstream có thể đã commit. Phía server, cũng cần dừng việc khi client đã bỏ đi (gRPC tự cancel với CANCELLED; HTTP thì lắng nghe close của request). deadlineMs phải sinh từ cùng monotonic clock trong process; truyền qua process khác bằng remaining ms có cap, đừng truyền performance.now(). 50 ms margin chỉ minh họa, hãy đo.

Dùng deadline propagation Chỉ cần timeout từng call
Chuỗi từ hai hop trở lên Một call, không fan-out
Có retry trong cùng request Job nền không có người chờ
Request đồng bộ với SLO latency Batch dài, có cơ chế resume riêng

Chạy mô phỏng thuần TypeScript:

Terminal window
npx --prefix examples ts-node examples/src/timeout-budget/budget-demo.ts

Demo cố định timestamp: request 500 ms, auth 150 ms, downstream tối đa 350 ms; một retry sau đó chỉ còn 50 ms và sau deadline thì không được bắt đầu. Đổi thời gian auth thành 480 ms để thấy phần còn lại gần như hết.

  1. Đặt deadline tại biên hệ thống, từ p95/p99 đo được, không từ cảm tính.
  2. Mỗi outbound call có timeout min(trần dependency, remaining - margin).
  3. Retry nằm trong cùng deadline gốc; kiểm tra budget sau backoff.
  4. Timeout ngoài lớn hơn timeout trong; kiểm tra cả LB và proxy.
  5. Truyền remaining timeout qua service, giới hạn giá trị nhận từ client.
  6. Server dừng việc khi client hủy; không giữ DB transaction trong lúc chờ remote call.
  7. Log remaining_ms trước/sau call và loại lỗi (caller cancel, local timeout, downstream).

Lợi ích

  • Giới hạn tài nguyên bị giữ bởi request đã quá hạn; latency dễ dự đoán.
  • Mỗi service biết còn bao nhiêu thời gian.
  • Retry có giới hạn tổng thay vì nhiều timeout độc lập.

Chi phí và giới hạn

  • Deadline quá chặt tăng lỗi giả; cần đo và hiệu chỉnh.
  • Truyền deadline qua nhiều giao thức cần contract.
  • Cancellation không thu hồi side effect đã commit.

Tự kiểm tra: (1) Inventory dùng 290 ms, payment được tối đa bao nhiêu? (2) Vì sao app idle timeout phải lớn hơn của ALB? (3) abort() có thu hồi side effect ở downstream không?