状态:草稿(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. 直觉:读取二进制信件的四种方法
- 2. 反射与 grpcurl
- 3. channelz:深入通道内部
- 4. 指标:两个家族
- 5. 实测:两个陷阱
- 6. 局限性
- 7. 贡献候选:文档与代码不符之处
- 8. 与小型集群的连接
- 参考
1. 直觉:读取二进制信件的四种方法
JSON API 的 curl 只需一行即可由人类阅读。由于 gRPC 在字节中没有字段名称(第 2 篇),因此必须有契约才能读取。调试工具归根结底是在解决
从何处获取契约与 从何处查看连接状态的问题。
| 窗口 | 提供什么信息 | 如何连接 |
|---|---|---|
| 服务器反射 | 服务与方法列表、消息定义 | 同一端口上的 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 를 직접 줌
- 如果以 JSON 格式编写请求,工具会查看契约并将其转换为第 2 篇中的二进制格式进行发送。响应也会反向转换并显示出来。
- 反射会 完整暴露服务结构。 通常的做法是在对外部开放的端口上将其关闭,仅在内部和管理端口上开启(这与第 5 篇第 6 节中 hubble-relay 将健康检查用的 4222 端口与主服务 4245 端口分开的想法相同)。
- 无需反射也可以进行健康检查。由于 grpcurl 未内置健康检查 proto,只需将 grpc 仓库中的
health.proto作为-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 |
三个有用的组合:
started_total − handled_total是 当前打开的流的数量(第 4 篇第 6.2 节中的 242 个 Watch)。handled_total将grpc_code除以- 就得到了第 3 篇的表格。不过,需要过滤掉诸如 Watch 的 CANCELLED 这种正常结束却带有错误码的情况。
grpc_server_handling_seconds延迟直方图通常默认是关闭的。在小型集群中,只有 istiod 会产生etcd_request_duration_seconds,因此本系列的延迟表格用 apiserver 的
以及 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)。
- Kubernetes 已将此依赖项更改为
go-grpc-middleware/providers/prometheus(PR #136227,2026-01-20 合并),并从 v1.36.0 开始包含在内(v1.35.0 中的同一文件仍在使用旧库)。新库的一元拦截器无论是否发生错误,都会将响应接收统计一次 (interceptors/client.go的UnaryClientInterceptor,providers/prometheus/reporter.go的PostMsgReceive). - 因此,在 从 1.35 升级到 1.36 时,该指标的值会跃升几个数量级(从数百跃升至数亿)。基于此指标创建仪表盘或告警的人可能会感到惊慌。 PR 正文仅记录了依赖项更新,并未提及指标含义的变化。
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. 局限性
- 未运行 grpcurl、channelz 和反射(未实测)。所需下载内容:grpcurl 发布二进制文件,或用于构建 grpc-go 示例服务器的 Go 模块。
- 第 5.1 节的判断是通过对比指标数字与库源码得出的。我们并未一路跟踪拦截器链去确认 apiserver 是否通过其他路径调用了更多
ReceivedMessage。 不过由于 132 = 132、39 = 39 完全吻合,我们认为不存在其他路径(推测)。 - 第 5.2 节中预创建功能是在 Argo CD 的哪项配置中开启的,这一点尚未核实(未确认)。
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 |
已知问题。基本上是往问题中补充实测数据 |
- 第 1 种方法最小也最稳妥。在文档的 proto 块中
List加上这两个消息,然后参照 A90 总结服务器行为段落(完整列表,如果服务很多则为 RESOURCE_EXHAUSTED)即可。 - grpc-go 和 grpc 组织有生成式 AI 政策。问题、PR 和评论必须由人工亲自编写,且提交者必须完全理解并审查 AI 生成的内容(第 7 篇第 7 节)。Kubernetes 也有 PR 正文告知等单独的规则。
8. 与小型集群的连接
- 如果是自己编写的 gRPC 服务器,请将健康检查和反射放在 管理端口上,并将 channelz
admin.Register挂载到同一管理端口上,这样就可以ss无需进行第 6 篇中的排查即可直接进行调查。 - 如果在仪表盘上使用
grpc_client_msg_received_total,请在升级到 Kubernetes 1.36 之前检查第 5.1 节。 - 查看时序预算时,请从
count by (job) ({__name__=~"grpc_.*"})开始看。在小型集群中,单个 job 占了 95%。
Titi 的一句话。 记录的“接到 132 个球”其实是“漏掉 132 个球”。记分牌有时也会写反,如果数字觉得不对,就先打开计算公式看看!
参考
所有资料均于 2026-10-17 查阅。
- grpc/grpc v1.84.0:
doc/server-reflection.md,doc/health-checking.md,doc/statuscodes.md; grpc-protogrpc/health/v1/health.proto - gRFC: A14 channelz, A66 OpenTelemetry Metrics, A90 Health Service List Method
- grpc/grpc-go v1.83.2:
reflection/serverreflection.go,channelz/service/service.go,admin/admin.go,grpclog/loggerv2.go,stats/opentelemetry/client_metrics.go,health/server.go,internal/transport/http2_server.go; PR grpc-go#8155 - go-grpc-prometheus: v1.2.0
client_metrics.go, masterclient_metrics.go - kubernetes: v1.34.10
etcd3.go, v1.36.0 相同文件, PR #136227, 问题 #111476 - 引用代码:go-grpc-prometheus v1.2.0,Apache License 2.0。