TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

gRPC 调试与可观测性:grpcurl、反射、channelz 以及指标陷阱

状态:草稿(2026-10-17),部分实测。 基准版本 grpc/grpc-go v1.83.2, grpc/grpc v1.84.0 文档, kubernetes v1.34.10, go-grpc-prometheus v1.2.0。 指标数据于 2026-10-17 03 点时段(KST)通过仅读取 Prometheus 获取。 未运行 grpcurl 和 channelz(未实测)。所需物品为 grpcurl 二进制文件 (或用于用 Go 构建的模块下载),以及开启了反射和 channelz 的测试服务器。因此,第 3、4 节中的命令是通过文档和代码确认的用法,未收录输出结果。

目录

1. 直觉:读取二进制信件的四种方法

JSON API 的 curl 只需一行即可由人类阅读。由于 gRPC 在字节中没有字段名称(第 2 篇),因此必须有契约才能读取。调试工具归根结底是在解决 从何处获取契约与 从何处查看连接状态的问题。

서버 하나를 들여다보는 네 가지 창 gRPC 서버 애플리케이션 서비스 서버 리플렉션 reflection.v1 grpcurl 로 조회 헬스 체크 health.v1 kubelet 프로브 channelz channelz.v1 채널·소켓 통계 메트릭 grpc_server_* Prometheus·OTel 위 셋은 같은 포트의 gRPC 서비스로 붙고, 메트릭은 별도 HTTP 로 긁는다.
图:反射、健康检查、channelz 和指标的位置
窗口 提供什么信息 如何连接
服务器反射 服务与方法列表、消息定义 同一端口上的 gRPC 服务 grpc.reflection.v1.ServerReflection
健康检查 是否 SERVING grpc.health.v1.Health(第 5 篇)
channelz 每个通道、子通道和套接字的调用次数、失败次数、最后调用时间、流与消息数 grpc.channelz.v1.Channelz
指标 按方法和状态码统计的次数、延迟 独立的 HTTP(/metrics)或 OTel 导出

2. 反射与 grpcurl

反射是一项可选功能,服务器通过它提供自身的 proto 定义,以便“客户端在没有存根(stub)的情况下也能在运行时构建请求” (server-reflection.md)。其典型的使用场景是命令行调试工具,而 grpcurl 就是这样一种工具。

在 grpc-go 中只需一行代码即可开启。 reflection.Register 会同时注册 v1 和旧版 v1alpha。 正如同一文件的 RegisterV1 注释所解释的,这是因为许多客户端仍然只知道 v1alpha。

使用 grpcurl 的大致操作如下(未运行,未实测)。

grpcurl -plaintext 서버:포트 list                                   # 서비스 목록(리플렉션 필요)
grpcurl -plaintext 서버:포트 describe tt.v1.Scoreboard              # 메서드와 메시지 정의
grpcurl -plaintext -d '{"rallyId":150}' 서버:포트 tt.v1.Scoreboard/Hit   # JSON 으로 요청, JSON 으로 응답
grpcurl -plaintext -import-path . -proto rally.proto 서버:포트 list   # 리플렉션 없이 .proto 를 직접 줌

3. channelz:深入通道内部

channelz(gRFC A14)将第 7 篇中的分层结构直接以统计数据展示出来。在顶层通道 → 子通道 → 套接字的树状结构中,它提供各节点的的状态(如 READY)、发起、成功和失败的调用次数、最后调用时间,以及套接字的流、消息和 keepalive 数量。

在 grpc-go 中,可以通过 channelz/service.RegisterChannelzServiceToServer 直接开启,或者通过 admin.Register 同时开启 channelz 和(如果使用 xDS 的话)CSDS。像第 6 篇中“apiserver 的 etcd 连接有 247 个”这类问题,原本正统的做法是用这个窗口来回答。由于 kube-apiserver 没有暴露 channelz,这次我们用 ss 代替了它(在 kube-apiserver 源码中未找到 channelz 注册,未确认)。

日志也可以成为一个窗口。grpc-go 通过环境变量 GRPC_GO_LOG_SEVERITY_LEVEL(info·warning·error)和 GRPC_GO_LOG_VERBOSITY_LEVEL(数字)来开启内部日志 (grpclog/loggerv2.go:65)。解析器更新、挑选器(picker)更换以及连接失败都会记录在这里。

4. 指标:两个家族

家族 名称示例 使用者
拦截器方式(go-grpc-prometheus 系列) grpc_server_started_total, grpc_server_handled_total{grpc_code}, grpc_client_msg_received_total,可选的 grpc_server_handling_seconds etcd, kube-apiserver(etcd 客户端), Argo CD, istiod
gRPC 自身的 OpenTelemetry 指标(gRFC A66) grpc.client.attempt.duration, grpc.client.call.duration, grpc.server.call.duration grpc-go stats/opentelemetry(client_metrics.go:291)
各自的名称 kubelet_runtime_operations_total, tempo_request_duration_seconds{route}, otelcol_* kubelet, Tempo, Collector

三个有用的组合:

以及 kubelet 和 Tempo 自身的直方图进行了替代。

5. 实测:两个陷阱

5.1 kube-apiserver 1.34 的接收消息指标统计的是“失败”

sum by (grpc_service, grpc_method, grpc_type) (grpc_client_msg_received_total{job="apiserver"})
sum by (grpc_method, grpc_code) (grpc_client_handled_total{job="apiserver", grpc_type="unary"})
在查看 apiserver 的 etcd 客户端指标时,我们遇到了奇怪的数字。自启动以来,Range 被调用了超过 1.6 亿次,但接收到的消息却只有 132 个。 方法msg_received) 接收到的消息(msg_sent) 发送的消息(
非 OK 结果的总和 132 162,817,697 Range 132
CANCELLED 15 + DEADLINE_EXCEEDED 2 + OUT_OF_RANGE 94 + UNAVAILABLE 21 = 39 58,967,709 26 + 4 + 9 = 39
Txn LeaseGrant·Status·Compact 无时间序列(从未统计过) 0

各 1.7 万~155 万 接收到的消息数与错误数完全一致。 apiserver 1.34 在 etcd 客户端中嵌入了 github.com/grpc-ecosystem/go-grpc-prometheus (etcd3.go:316),go.mod 中的版本是 v1.2.0。该版本的一元客户端拦截器如下:

func (m *ClientMetrics) UnaryClientInterceptor() func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		monitor := newClientReporter(m, Unary, method)
		monitor.SentMessage()
		err := invoker(ctx, method, req, reply, cc, opts...)
		if err != nil {
			monitor.ReceivedMessage()
		}
		st, _ := status.FromError(err)
		monitor.Handled(st.Code())
		return err
	}
}

来源:grpc-ecosystem/go-grpc-prometheus v1.2.0 client_metrics.go 第 108~120 行,Apache-2.0

err != nil 当条件为时,才算作“已接收”。条件被颠倒了。虽然同一仓库的 master 分支中已将其修复为 err == nil ,但此后一直没有发布新版本,且该仓库处于归档(archived)状态。 流(Watch)通过其他路径(RecvMsg 包装)进行统计,因此是正常的。同一集群中 etcd 服务器端的 grpc_server_msg_received_total 使用了其他库,所以是正常的(Compact 10,268 = 启动 10,268)。

5.2 Argo CD 服务器:1,938 个时间序列中仅有 13 个

count(grpc_server_handled_total{job="argocd-server-metrics"})          → 1,938
count(grpc_server_handled_total{job="argocd-server-metrics"} > 0)      → 13
count(count by (grpc_service,grpc_method) (grpc_server_started_total{job="argocd-server-metrics"}))  → 114

114 个方法 × 17 个状态码 = 1,938。基于拦截器方式的库具有一项功能,即 预先将所有方法与所有状态码的组合设为 0 (其目的是让“从未被调用过的方法”也会出现在比率计算中),Argo CD 服务器似乎开启了此功能(推测,未打开 Argo CD 源码)。结果导致仅这一个 job 的 时间序列就达到了 2,280 个,占据了 grpc_* 小型集群整体 时间序列 2,402 个的 grpc_* 95% 。其中有值的仅有 13 个。如果时间序列预算吃紧,可以考虑通过 metric_relabel_configs 丢弃值为 0 的组合。

6. 局限性

7. 贡献候选:文档与代码不符之处

这是在撰写本系列时发现的。目前尚未发布到任何地方,在发布前需要在最新的 main 分支上再次进行确认。

# 仓库 不一致之处 依据 规模
1 grpc/grpc doc/health-checking.md 的服务定义中缺少 List RPC gRFC A90 与 grpc-proto 的 health.proto 包含 List·HealthListRequest·HealthListResponse ,且 grpc-go 已在 v1.83.2 中实现(health/server.go:73, grpc-go#8155)。即使在 master 分支中,文档也依然只有 Check 和 Watch 两个(2026-10-17) 单个文档 PR
2 grpc/grpc doc/statuscodes.md 将 INVALID_ARGUMENT 描述为库不会生成的代码 grpc-go 服务器会对内容类型错误的请求发送 HTTP 415 与 grpc-status: 3 (internal/transport/http2_server.go:504)。需要先确认其他语言的实现是否也是如此 先通过 Issue 提问
3 kubernetes 由于 1.36 的依赖项更换, grpc_client_msg_received_total(apiserver→etcd,一元)的含义从“失败次数”变为“响应次数” 第 5.1 节实测(132 = 132),go-grpc-prometheus v1.2.0 client_metrics.go:113, PR #136227 补充发行说明或记录已知问题(效用较低,属于已过去的版本)
4 kubernetes apiserver 为每个资源(CRD)创建一个 etcd 客户端,导致连接数与资源数量成正比(节点 A 上有 247 个 TCP 连接) etcd3.go:74 中的 TODO,Issue #111476 已知问题。基本上是往问题中补充实测数据

8. 与小型集群的连接

Titi 的一句话。 记录的“接到 132 个球”其实是“漏掉 132 个球”。记分牌有时也会写反,如果数字觉得不对,就先打开计算公式看看!

参考

所有资料均于 2026-10-17 查阅。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论