Khi hệ thống có hàng chục RPC, có những việc bạn muốn làm cho mọi request: ghi log method và latency, kiểm token xác thực, đếm metric, và — quan trọng nhất — bắt panic để một bug trong handler không làm sập cả server. Nhét những việc này vào từng handler là lặp code, dễ quên, và trộn lẫn logic hạ tầng với logic nghiệp vụ. gRPC có câu trả lời giống middleware của web framework: interceptor — một hàm bọc quanh handler, gắn một lần ở server, áp cho mọi RPC. Bài này (phần 6/12) đo thật interceptor trong go-lab, tập trung vào hai công dụng kinh điển: logging và recover panic.
Interceptor là hàm bọc quanh handler
Một unary interceptor có chữ ký nhận handler (phần xử lý thật) và tự quyết định làm gì trước và sau khi gọi nó:
func logInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
// ... làm gì đó TRƯỚC handler (log, auth, bắt đầu đo giờ)
resp, err := handler(ctx, req) // gọi handler thật (hoặc interceptor kế tiếp)
// ... làm gì đó SAU handler (log latency, đếm metric)
return resp, err
}
// đăng ký: grpc.NewServer(grpc.ChainUnaryInterceptor(A, B))
Có hai loại: unary interceptor (cho RPC một-lần, như trên) và stream interceptor (cho các RPC streaming ở bài grpc-03). Cơ chế tương tự, chỉ khác chữ ký. Khi khai nhiều interceptor bằng ChainUnaryInterceptor(A, B), chúng lồng nhau như vỏ hành: A ở ngoài, B ở trong.

Hình 1: Interceptor là hàm bọc quanh handler — làm việc trước và sau khi gọi handler(). Chain nhiều cái lồng nhau như vỏ hành (A ngoài, B trong; vào xuôi, ra ngược, giống defer). Dùng cho logging, auth, metrics, recover — áp cho mọi RPC.
Đo thật: chain, recover và sống sót
Mình dựng server với chain hai interceptor — log (ngoài) và recover (trong) — rồi chạy ba kịch bản:

Hình 2: Đo thật. (1) RPC thường: chain chạy log VÀO → recover VÀO → handler → recover RA → log RA, client nhận "Xin chao Minh". (2) RPC panic: recover bắt panic, client nhận code=Internal, server không sập. (3) RPC thường lần nữa: server vẫn sống, "Xin chao Lan".
Kết quả thật:
- ① RPC bình thường — thấy thứ tự chain: log in
-> VÀO, rồi recover-> VÀO, handler chạy, recover<- RA, cuối cùng log<- RAkèm latency (0.01ms). Đúng thứ tự vỏ hành: interceptor khai trước (log) bọc ngoài cùng, ra sau cùng. Client nhận"Xin chao Minh". - ② RPC panic — recover cứu: handler cố tình
panic(). Interceptor recover bắt được quadefer/recover(), inBẮT PANICvà trả vềstatus.Errorf(codes.Internal, ...). Client nhậncode=Internal, msg="loi noi bo (da recover)"— một lỗi gọn gàng, thay vì connection chết đột ngột hay server crash. - ③ RPC bình thường lần nữa — server sống sót: gọi lại, client nhận
"Xin chao Lan"bình thường. Server vẫn chạy sau panic.
Điểm ③ là cốt lõi: trong Go, một panic không được recover trong một goroutine sẽ làm sập cả tiến trình. Nếu handler gRPC panic mà không có recover interceptor, một bug trong một request có thể kéo sập server, giết mọi request khác đang chạy. Interceptor recover biến một bug lập trình thành một lỗi đơn lẻ, cô lập — request đó thất bại với Internal, phần còn lại không hề hấn. Đây là lưới an toàn bắt buộc cho server production.
Đánh đổi cần cân nhắc
Thứ tự chain quan trọng — đặt đúng chỗ. Recover nên ở trong cùng (gần handler nhất) để bắt panic của chính handler; nếu đặt recover ngoài logging, một panic trong interceptor logging sẽ không được bắt đúng cách. Tương tự, interceptor auth nên chạy sớm (ngoài) để chặn request không hợp lệ trước khi tốn công cho các tầng trong. Suy nghĩ thứ tự như xếp lớp phòng thủ.
Recover che panic — đừng để nó nuốt mất dấu vết. Bắt panic để server sống là đúng, nhưng nếu chỉ trả Internal mà không ghi lại stack trace, bạn mất manh mối debug. Interceptor recover tốt phải log đầy đủ (stack, request-id) rồi mới trả lỗi gọn cho client. Panic vẫn là bug cần sửa, không phải thứ để giấu — recover chỉ ngăn nó lan rộng.
Interceptor chạy cho MỌI RPC — giữ nó nhẹ. Vì áp cho mọi request, một interceptor chậm (ví dụ ghi log đồng bộ ra đĩa, gọi mạng kiểm auth mỗi lần) sẽ cộng độ trễ vào toàn bộ traffic. Giữ interceptor nhẹ: log bất đồng bộ, cache kết quả auth, đo metric bằng bộ đếm trong bộ nhớ. Một interceptor nặng là nút thắt cổ chai cho cả service.
Ba ý mang về
- Interceptor là middleware của gRPC: gắn một lần, áp mọi RPC. Một hàm bọc quanh handler, làm việc trước/sau khi gọi nó — cho logging, auth, metrics, recover. Đo thật: chain
log+recoverchạy lồng nhau như vỏ hành (vào xuôi, ra ngược). - Recover interceptor giữ server sống sau panic. Đo thật: handler panic → recover bắt → client nhận
Internalgọn gàng → server phục vụ request tiếp theo bình thường. Không có nó, một panic có thể sập cả tiến trình và giết mọi request khác. - Thứ tự chain và độ nhẹ quyết định chất lượng. Đặt recover trong cùng, auth ngoài để chặn sớm; luôn log stack trong recover để không mất dấu bug; và giữ interceptor nhẹ vì nó cộng chi phí vào mọi request của service.
Nguồn
- gRPC Go — Interceptors: https://github.com/grpc/grpc-go/blob/master/examples/features/interceptor/README.md
- gRPC — Guides: Interceptors khái niệm: https://grpc.io/docs/guides/interceptors/
- go-grpc-middleware — bộ interceptor sẵn (recovery, logging, auth): https://github.com/grpc-ecosystem/go-grpc-middleware
Phần sau ta chuẩn hoá cách báo lỗi: status code của gRPC, so với HTTP status, và rich error details — cách trả về lỗi có cấu trúc để client xử lý đúng thay vì đoán từ chuỗi text.