Một trong những nguyên nhân phổ biến nhất khiến hệ thống phân tán sập dây chuyền là request treo vô hạn: service A gọi B, B chờ C, C kẹt — và A, B cứ giữ goroutine/connection chờ mãi, cạn tài nguyên, kéo sập cả chuỗi. gRPC có một cơ chế thanh lịch để chặn điều này: deadline gắn trong context. Client đặt một hạn chót; gRPC tự động gửi kèm nó theo request; mọi tầng phía sau biết "còn bao nhiêu thời gian" và dừng đúng lúc. Đây không phải một tính năng phụ — nó là nền tảng cho độ bền của cả hệ thống. Bài này (phần 5/12) đo thật cách deadline hoạt động trong go-lab.
Deadline đi kèm request, server dừng sớm
Khác với "timeout" cục bộ (chỉ client tự bỏ cuộc), deadline của gRPC được truyền sang server. Client đặt bằng context.WithTimeout:
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
resp, err := c.Do(ctx, req) // deadline đi KÈM request sang server
// hết 100ms chưa có kết quả -> err code = DeadlineExceeded
Quan trọng hơn: server đọc được deadline đó và nên chủ động dừng khi hết giờ, qua ctx.Done():
func (s *srv) Do(ctx context.Context, r *Req) (*Resp, error) {
select {
case <-time.After(work): // làm xong
return resp, nil
case <-ctx.Done(): // client huỷ / hết deadline -> DỪNG, trả lỗi
return nil, status.FromContextError(ctx.Err()).Err()
}
}

Hình 1: Client đặt deadline bằng context.WithTimeout; gRPC gửi kèm request. Server kiểm ctx.Done() để dừng sớm. Deadline lan truyền qua chuỗi gọi — mỗi tầng biết thời gian còn lại của toàn bộ chuỗi, giảm dần.
Đo thật: deadline quyết định thời điểm trả về
Mình chạy ba call với deadline khác nhau vào cùng một handler (server làm việc trong work_ms mili-giây):

Hình 2: Đo thật. Deadline 500ms/work 100ms → OK sau 101ms. Deadline 100ms/work 500ms → DeadlineExceeded sau đúng 100ms. Deadline 50ms/work 1000ms → DeadlineExceeded sau 50ms. Call trả về đúng lúc hết deadline, không chờ hết việc. Server log ctx.Done và dừng sớm.
Kết quả thật:
- Deadline đủ (500ms, việc 100ms): trả về
OKsau 101ms — việc xong trước hạn, mọi thứ bình thường. - Deadline không đủ (100ms, việc 500ms): trả về
DeadlineExceededsau đúng 100ms — không chờ hết 500ms. Client lấy lại quyền điều khiển ngay khi hết hạn, không bị treo. - Deadline rất ngắn (50ms, việc 1000ms):
DeadlineExceededsau 50ms.
Điểm mấu chốt thứ hai: server dừng sớm. Log server khi deadline 100ms/việc 500ms in ctx.Done: context deadline exceeded -> dừng xử lý sớm. Server không chạy nốt 500ms vô ích — nó nhận biết qua ctx.Done() rằng client đã hết giờ và dừng ngay. Trên hệ thống thật, đây chính là cơ chế ngăn một request quá hạn tiếp tục ngốn CPU, giữ kết nối DB, chiếm goroutine — thứ gây quá tải lan rộng (cascading failure) khi tải cao.
Deadline lan truyền qua cả chuỗi
Sức mạnh lớn nhất là lan truyền. Khi service A nhận request với deadline 100ms, nếu A dùng cùng ctx đó để gọi tiếp service B, thì B nhận deadline đã trừ đi thời gian A đã tiêu. Cả chuỗi cùng tôn trọng một hạn chót tổng — không tầng nào làm vượt quá thời gian mà client còn chờ. Đây là lý do nên luôn truyền ctx xuống mọi lời gọi downstream (gRPC, DB, HTTP), thay vì tạo context.Background() mới (cắt đứt deadline).
Đánh đổi cần cân nhắc
Deadline phải được đặt — gRPC không tự áp mặc định. Nếu client không đặt deadline, request có thể chờ vô hạn (hoặc tới timeout mạng rất dài). Đây là lỗi phổ biến: quên đặt deadline cho mọi call là mở cửa cho treo dây chuyền. Quy tắc: mọi call gRPC ở client production nên có deadline hợp lý theo SLA.
Server phải chủ động kiểm ctx — nếu không, dừng sớm không xảy ra. gRPC huỷ việc gửi response khi hết deadline, nhưng nếu handler của bạn là một vòng tính toán/truy vấn không kiểm ctx.Done(), nó vẫn chạy tới hết rồi mới phát hiện. Tài nguyên vẫn bị phí. Với công việc nặng/dài, phải kiểm ctx.Err() định kỳ và truyền ctx vào mọi thao tác con (DB query nhận ctx, v.v.).
Deadline quá ngắn gây lỗi giả. Đặt deadline chặt quá so với thời gian xử lý thật sẽ khiến các request bình thường cũng bị DeadlineExceeded khi hệ thống hơi chậm (GC, tải cao), tạo lỗi và retry dồn dập làm mọi thứ tệ hơn. Deadline nên dựa trên p99 thực đo được cộng biên an toàn, không phải con số đoán. Cân bằng giữa "bỏ cuộc sớm" và "cho đủ thời gian".
Ba ý mang về
- Deadline gắn trong context và đi kèm request sang server. Đo thật: call deadline 100ms vào handler làm 500ms trả về sau đúng 100ms với code DeadlineExceeded — client không treo chờ một việc đã quá hạn. Đặt bằng
context.WithTimeout. - Server dừng sớm qua ctx.Done() để không phí tài nguyên. Đo thật: server log
ctx.Donevà dừng ngay thay vì chạy nốt 500ms. Đây là cơ chế chống quá tải lan rộng — request quá hạn không tiếp tục ngốn CPU/DB/goroutine. - Luôn đặt deadline và luôn truyền ctx xuống downstream. gRPC không tự áp deadline (quên là treo vô hạn); server phải chủ động kiểm ctx cho việc dài; và deadline nên dựa trên p99 thật cộng biên — quá ngắn gây lỗi giả, quá dài mất tác dụng bảo vệ.
Nguồn
- gRPC — Deadlines: https://grpc.io/docs/guides/deadlines/
- gRPC Blog — Deadlines (gRPC and Go): https://grpc.io/blog/deadlines/
- Go — context package: https://pkg.go.dev/context
Phần sau ta thêm tầng middleware cho gRPC: interceptor — cách gắn logging, metrics, recover panic vào mọi RPC mà không sửa từng handler, cho cả unary lẫn streaming.