状态:草稿(2026-10-17),部分为实测数据。 基准版本 grpc/grpc v1.84.0(文档),grpc/grpc-go v1.83.2(提交 030ee8becb20)。 实测数据通过在 2026-10-17 03 点左右(KST)的小集群中 仅读取 获得(查询语句见第 5 节)。 编写规则:以实测数据为准,未能测量的标记为“未实测”,无法确认的标记为“未确认”。
目录
- 1. 直观理解:用表格代替电话
- 2. 图解
- 3. 为什么不用 JSON over HTTP/1.1
- 4. 代码生成所做的工作
- 5. 实测:小集群中使用 gRPC 的组件
- 6. 通道的生命周期:长连接,短调用
- 7. 限制
- 8. 与小集群的连接
- 参考
1. 直观理解:用表格代替电话
拜托隔壁团队办事时,打电话虽然快但容易产生误解。因此,公司会制作 表格。如果规定好了栏位名称与顺序、哪一栏是数字,接收方只需看表格即可处理;即便表格稍有改动(比如多加一栏),使用旧表格的人也能继续合作。
gRPC 用 .proto 文件来编写这种表格,并从表格中自动提取出 两端语言的代码。调用方像编写 client.Hit(ctx, req) 这样调用普通的函数,而在底层,库会将请求转换为字节并通过 HTTP/2 连接发送,再将响应转换回对象返回。
其起点是 Google 将内部使用超过 10 年的基于 RPC 的技术(Stubby)的下一代版本于 2015 年 3 月开源,并于 2017 年 2 月 16 日进入 CNCF 孵化阶段(grpc.io/about, CNCF 项目页面).
| 部件 | 职责 | 在本系列中 |
|---|---|---|
| IDL (.proto) | 服务、方法、消息的契约。字段编号是保持向下兼容的关键 | 第 2 篇 |
| 代码生成(protoc + 插件) | 各语言的消息类型、客户端存根、服务器接口 | 第 1 篇第 4 节 |
| 通道 (ClientConn) | 名称解析、负载均衡、连接维护、重试、deadline | 第 4、5、7 篇 |
| HTTP/2 传输 | 一次调用 = 一个流、5 字节消息前缀、Trailer 中的状态码 | 第 3 篇 |
2. 图解
- 上一行是 构建时 的工作。将
.proto放入protoc和各语言插件中,就会生成消息类型、存根以及服务器接口。 - 下一行是 运行时 的工作。客户端存根只管对通道说“请把这些字节发送到这个方法路径”。在通道内部,名称解析器(resolver)生成地址列表,负载均衡策略选择子通道(针对单个地址的连接),并在该子通道的 HTTP/2 连接上打开一个新流。
- 一条粗线代表一个 TCP 连接。多个调用同时流经该连接。这就是第 5 篇中“负载倾斜到某个 Pod”问题的根源。
3. 为什么不用 JSON over HTTP/1.1
使用 REST 和 JSON 也可以进行远程调用。从契约、字节、连接、失败四个方面来看 gRPC 存在的原因,如下所示:
| 维度 | 常见的 REST/JSON | gRPC | 代价 |
|---|---|---|---|
| 契约 | OpenAPI 是可选的,容易与代码脱节 | .proto 即代码的源头 | 构建过程中包含代码生成步骤 |
| 字节 | 每次都以字符串形式发送字段名 | 字段编号 + 二进制值。第 2 篇示例中 21 字节对比 JSON 的 72 字节 | 无法让人眼直接阅读(需要第 8 篇中的工具) |
| 连接 | HTTP/1.1 每个连接只能有一个请求 | 通过 HTTP/2 流在单个连接中实现大量调用、双向流式传输 | L4 负载均衡以连接为单位,会导致倾斜(第 5 篇) |
| 失败 | HTTP 状态码和正文格式因服务而异 | 17 种状态码、deadline 传播、重试策略包含在库中 | 正确选择状态码的责任仍在于应用(第 3 篇) |
浏览器的 JavaScript API 无法直接操作 HTTP/2 帧和 Trailer,因此很难直接使用 gRPC。为此,有 gRPC-Web 或 HTTP/JSON 转换网关等独立桥梁(本系列不作探讨)。
4. 代码生成所做的工作
这是一个示例契约。我们在整个系列中都会使用这个服务(一个记录乒乓球拉球一次的虚拟服务)。
syntax = "proto3";
package tt.v1;
message Rally { int32 rally_id = 1; string server = 2; repeated int32 hits = 3; bool won = 4; sint32 delta = 5; }
message Score { int32 home = 1; int32 away = 2; }
service Scoreboard {
rpc Hit(Rally) returns (Score); // 단항
rpc Watch(Rally) returns (stream Score); // 서버 스트리밍
}
在这个契约中, Hit 的方法路径是 /tt.v1.Scoreboard/Hit 。第 3 篇的帧图解中也使用了这个路径。
生成的客户端存根所做的工作少得惊人。如果不直接运行 protoc,而是查看 grpc-go 仓库中包含的 实际生成的文件 (健康检查服务),就会发现单个方法是这样的:
const Health_Check_FullMethodName = "/grpc.health.v1.Health/Check"
func (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HealthCheckResponse)
err := c.cc.Invoke(ctx, Health_Check_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
来源:grpc/grpc-go v1.83.2 health/grpc_health_v1/health_grpc.pb.go(由 protoc-gen-go-grpc v1.6.2 生成),Apache-2.0
- 存根只是将 方法路径字符串以及请求和响应对象传递给通道的
Invoke。路径由/+ 包名.服务 +/+ 方法名组成, 这个字符串直接变成了 HTTP/2 的:path(规范 PROTOCOL-HTTP2.md 附录 A)。 - 服务器端生成的代码是一个名为
ServiceDesc的表。服务名称下列有一元方法列表(Methods)和流列表(Streams,指示是否为服务器/客户端流式传输),每项都指向一个处理函数。服务器通过传入的:path查找此表并调用处理程序(第 7 篇)。 - 生成的代码因语言而异,但传输层格式(Wire format)是相同的。这就是为什么 Go 客户端可以调用 C++ 服务器。
5. 实测:小集群中使用 gRPC 的组件
2026-10-17 03:00~03:04 (KST),向 Prometheus 发送了以下查询(通过 Kubernetes API 服务器的服务代理进行 GET,仅读取)。
count by (__name__, job) ({__name__=~"grpc_.*|.*_grpc_.*|rpc_.*"})
sum by (grpc_service, grpc_method, grpc_type) (grpc_client_started_total{job="apiserver"})
sum by (operation_type) (kubelet_runtime_operations_total)
指标中残留的 gRPC 服务名称汇总如下(具体数值见第 6 篇):
| 谁 → 谁 | gRPC 服务(指标标签) | 依据 |
|---|---|---|
| kube-apiserver → etcd | etcdserverpb.KV·Watch·Lease·Maintenance |
apiserver 的 grpc_client_* 指标 |
| kubelet → containerd | runtime.v1.RuntimeService·ImageService |
kubelet_runtime_operations_total 的 21 种 operation_type |
| 应用 → OTel Collector → Tempo | OTLP TraceService/Export |
Collector 的 transport="grpc",导出目标 :4317 |
| Tempo 内部 | /tempopb.Pusher/PushBytesV2, /grpc.health.v1.Health/Check |
tempo_request_duration_seconds 的 route 标签 |
| Argo CD UI·CLI → argocd-server | application.ApplicationService 等 114 个方法 |
grpc_server_started_total |
| 边车 → istiod | istio.v1.auth.IstioCertificateService/CreateCertificate |
istiod 的 grpc_server_* |
| hubble-relay → 各节点的 Hubble | observer.Observer·peer.Peer |
中继日志,gRPC 探针(第 5、6 篇) |
名称形如 etcdserverpb.KV 、类似于 proto 包名.服务 的原因,是第 4 节的路径规则直接下沉为了指标标签。
相反,kubelet 没有使用 gRPC 的公共指标,而是以 list_containers 这样的自定义名称进行统计。即使同为 gRPC,其观测方式也因实现而异。
6. 通道的生命周期:长连接,短调用
- 通道在 创建时不会建立连接。 grpc-go 的
NewClient以空闲(IDLE)状态启动,在第一次调用时解析名称并建立连接。如果没有调用,将在 30 分钟后重新回到空闲状态(默认值dialoptions.go:729). - 通道的状态共有五种:IDLE、CONNECTING、READY、TRANSIENT_FAILURE、SHUTDOWN(connectivity-semantics-and-api.md).
- 因此, 通道复用是原则。官方性能指南也建议复用通道和存根(Performance Best Practices). 如果每次调用都新建通道,就会每次都经历 TCP、TLS、HTTP/2 配置交换。正如第 5 篇中所见,kubelet 的 gRPC 探针正是这样运作的。
- 默认的名称解析器也因版本而异。
NewClient是dns,旧版Dial·DialContext是passthrough(dialoptions.go:730,clientconn.go的注释)。这就是为什么 Kubernetes 源码在从Dial迁移到NewClient时要附带passthrough:///的原因(第 5 篇第 6 节)。
7. 限制
- 我们没有公开其他语言实现(C++ Core、Java)的内部结构。本系列的代码阅读仅限于 grpc-go。
- 未涉及 gRPC-Web、JSON 转换网关以及 ALTS 等认证方式。
- 第 5 节的列表仅统计了 Prometheus 抓取的目标。不产生指标或未被抓取的 gRPC(例如:CSI 驱动程序的 Unix 套接字、kubelet 插件注册)并未包含在内(未实测)。
8. 与小集群的连接
即使每天都在接触集群,如果没有亲手编写过 gRPC,最好先确认一点:其实你每天已经在不知不觉中使用了数亿次。 控制节点 A 上的 etcd 自启动以来已处理了约 8333 万次 Range 请求,而 8 个 kubelet 仅统计 CRI 列表查询就调用了约 6900 万次(第 6 篇)。 下一篇将从这些调用中的每一个具体传输什么字节开始看起。
Titi 的小结。 表格只要制作得好,对方是 Go 还是 C++ 都无所谓。千万不要改变栏位编号。编号就是约定!
参考
所有资料均于 2026-10-17 查阅。
- grpc.io About, Performance Best Practices
- CNCF 项目页面:gRPC
- grpc/grpc v1.84.0:
doc/PROTOCOL-HTTP2.md,doc/connectivity-semantics-and-api.md - grpc/grpc-go v1.83.2:
clientconn.go,dialoptions.go,health/grpc_health_v1/health_grpc.pb.go - 引用代码:grpc-go v1.83.2, Apache License 2.0.