TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

gRPC 解决的问题与全貌:契约、代码生成、通道、HTTP/2

状态:草稿(2026-10-17),部分为实测数据。 基准版本 grpc/grpc v1.84.0(文档),grpc/grpc-go v1.83.2(提交 030ee8becb20)。 实测数据通过在 2026-10-17 03 点左右(KST)的小集群中 仅读取 获得(查询语句见第 5 节)。 编写规则:以实测数据为准,未能测量的标记为“未实测”,无法确认的标记为“未确认”。

目录

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. 图解

gRPC 한 번의 호출: .proto 에서 서버 함수까지 rally.proto service·message 정의 protoc·플러그인 언어별 코드 생성 클라이언트 스텁 서버 인터페이스 실행할 때: 클라이언트 프로세스 앱: client.Hit(ctx, req) 스텁: 직렬화 → Invoke 채널 (ClientConn) 이름 풀이(resolver) 부하 분산(LB 정책) 서브채널 1 HTTP/2 연결 서브채널 2 HTTP/2 연결 서버 프로세스 HTTP/2 서버 전송 스트림 하나 = 호출 하나 :path 로 메서드 찾기 역직렬화 → 핸들러 앱 코드 Hit() 응답 반환 굵은 선: TCP 하나 위의 HTTP/2 스트림. 요청은 POST, 응답 끝의 트레일러에 grpc-status 가 실린다. 채널은 오래 살려 두고 여러 호출이 연결을 나눠 쓴다.
图解:从 .proto 到服务器函数:代码生成与运行时的各层关系

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

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. 通道的生命周期:长连接,短调用

7. 限制

8. 与小集群的连接

即使每天都在接触集群,如果没有亲手编写过 gRPC,最好先确认一点:其实你每天已经在不知不觉中使用了数亿次。 控制节点 A 上的 etcd 自启动以来已处理了约 8333 万次 Range 请求,而 8 个 kubelet 仅统计 CRI 列表查询就调用了约 6900 万次(第 6 篇)。 下一篇将从这些调用中的每一个具体传输什么字节开始看起。

Titi 的小结。 表格只要制作得好,对方是 Go 还是 C++ 都无所谓。千万不要改变栏位编号。编号就是约定!

参考

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

登录后即可点赞

评论

还没有评论。

登录后即可发表评论