Golang中的gRPC拦截器之性能监控 Go语言记录每个RPC方法的耗时

4次阅读

最轻量耗时统计是在serverinterceptor开头用time.now()打点、返回前计算差值;方法名取info.fullmethod,状态码用status.convert(err).code()解析,成功时为codes.ok;推荐prometheus.histogramvec做聚合分析,避免log.printf高开销;interceptor中只透传span context,不startspan或endspan。

Golang中的gRPC拦截器之性能监控 Go语言记录每个RPC方法的耗时

grpc ServerInterceptor 怎么加耗时统计

直接在 ServerInterceptor 里用 time.Now() 打点,返回前算差值,是最轻量、最可控的方式。别碰 context.WithTimeout 或其他中间件式包装,那会干扰业务逻辑的 deadline 行为。

常见错误是把计时逻辑写在 defer 里但没传入开始时间,导致取到的是 defer 执行时刻的时间——结果全是 0ms 或负数。

  • 必须在拦截器函数开头就调用 time.Now(),存成局部变量
  • 不要用 ctx.Done()select{} 做耗时判断,那是超时控制,不是监控
  • 如果用了 grpc.UnaryServerInterceptor,注意它只对 unary 方法生效;stream 方法得另配 grpc.StreamServerInterceptor

怎么拿到方法名和状态码做维度打点

gRPC 的方法全名格式是 /package.Service/Method,从 info.FullMethod 拿最稳;状态码不能靠 recover 或 panic 捕获,得等 handler 返回后,从 err 解析出 status.Code

容易踩的坑:有人用 fmt.Sprintf("%v", err) 去匹配字符串 “DEADLINE_EXCEEDED”,这不可靠——不同版本 grpc-go 返回的 Error 实现可能变,status.Convert(err).Code() 才是唯一正确路径。

立即学习go语言免费学习笔记(深入)”;

  • 方法名从 info.FullMethod 取,别 parse ctx 或 request body
  • 状态码必须用 status.Convert(err).Code(),不是 err.(Interface{ Code() codes.Code }).Code()
  • 成功时 err == nil,此时状态码是 codes.OK,别漏这个分支

log.Printf 和 prometheus.HistogramVec 哪个更适合

单纯看单次耗时,log.Printf 足够调试;但要做聚合分析(P95、QPS、错误率),必须上 prometheus.HistogramVec。两者不互斥,可以共存:日志保原始明细,指标做聚合视图。

性能影响很实际:每秒万级请求下,log.Printf 吞吐掉一半以上,而 HistogramVec.Observe() 是原子操作,压测实测开销稳定在 50ns 内。

  • 开发期用 log.Printf("[%s] %vms %s", fullMethod, elapsed.Milliseconds(), statusCode) 快速验证
  • 上线必须切到 histogram.WithLabelValues(fullMethod, statusCode.String()).Observe(elapsed.Seconds())
  • label 不要塞 request ID 或参数值,会爆炸性增加 metric cardinality

为什么 interceptor 里不能直接调用 trace.StartSpan

因为 gRPC 的 ServerInterceptor 不保证与底层网络层的 span 生命周期对齐——比如流式 RPC 中,一次 FullMethod 可能对应多次 Recv()Send(),而 interceptor 只进一次。硬塞 trace.StartSpan 会导致 span 提前 finish,或漏埋点。

真正该埋点的地方是 transport 层(如 http2 server)或自定义 codec,但成本高;更务实的做法是:只在 interceptor 记耗时 + 状态,把 span context 从 ctx 里提取出来,透传给后续业务 handler,由 handler 内部决定何时 start/finish。

  • interceptor 里只做 span := trace.SpanFromContext(ctx),不 StartSpan
  • 别在 interceptor 里调 span.End(),handler 才知道什么时候真结束
  • 如果用了 OpenTelemetry,优先用 otelgrpc.UnaryServerInterceptor 这类官方适配器,别自己造轮子

最麻烦的其实是流式方法的状态聚合——一次 stream 可能有几十次读写,但你只 intercept 到一次开始和一次结束。这时候耗时数字本身意义有限,得配合 stream 内部的细粒度埋点才能看清瓶颈在哪。

text=ZqhQzanResources