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.

Ảnh chụp đoạn mã nền tối minh hoạ interceptor middleware cho gRPC gắn một lần áp cho mọi RPC, logging auth metrics recover panic không nhét vào từng handler mà bọc quanh tất cả bằng interceptor. Interceptor là một hàm bọc quanh handler func logInterceptor ctx Context req any info UnaryServerInfo handler UnaryHandler any error làm gì đó trước handler log auth bắt đầu đo giờ resp err bằng handler ctx req gọi handler thật hoặc interceptor kế làm gì đó sau handler log latency đếm metric return resp err đăng ký grpc.NewServer grpc.ChainUnaryInterceptor A B. Chain nhiều interceptor lồng nhau như vỏ hành A log vào B recover vào handler B ra A ra khai ChainUnaryInterceptor A B A ở ngoài B ở trong thứ tự vào xuôi thứ tự ra ngược giống defer stack. Dùng để làm gì cho cả unary và stream logging tracing in method cộng latency cộng request-id mọi RPC auth kiểm token trong metadata chặn sớm nếu sai metrics đếm số call lỗi độ trễ theo method Prometheus recover bắt panic trong handler trả lỗi server không sập

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:

Ảnh chụp bảng kết quả đo thật chain interceptor recover panic server sống sót output thật go-lab grpc-go v1.67.1 ChainUnaryInterceptor log recover. Một RPC bình thường thấy thứ tự chain vào ra log vào svc.Greeter Hello recover vào recover ra bình thường log ra svc.Greeter Hello 0.01ms err nil client nhận msg Xin chao Minh chain lồng như vỏ hành log ngoài vào recover trong vào handler recover ra log ra. Hai RPC panic interceptor recover server không sập log vào recover vào recover bắt panic handler nổ tung trả lỗi Internal log ra err code Internal client nhận code Internal msg loi noi bo da recover handler panic recover bắt lại client nhận lỗi Internal gọn gàng thay vì connection chết đột ngột. Ba RPC bình thường lần nữa server vẫn sống sau panic client nhận msg Xin chao Lan OK err nil nếu không có recover interceptor một panic trong handler sẽ làm cả goroutine và có thể cả server sập kéo theo mọi request khác interceptor recover biến một bug lập trình thành một lỗi đơn lẻ cô lập

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 <- RA kè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 qua defer/recover(), in BẮT PANIC và trả về status.Errorf(codes.Internal, ...). Client nhận code=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ề

  1. 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+recover chạy lồng nhau như vỏ hành (vào xuôi, ra ngược).
  2. Recover interceptor giữ server sống sau panic. Đo thật: handler panic → recover bắt → client nhận Internal gọ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.
  3. 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

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.