Featured image of post ProtoBuf & gRPC 实战:proto3 时代的跨语言 RPC 与数据建模

ProtoBuf & gRPC 实战:proto3 时代的跨语言 RPC 与数据建模

2017 年 8 月 gRPC 1.0 正式 GA、protoc-gen-validate 同月发布、Envoy 1.0 紧随其后——以 proto3 为中心,本文系统讲清 ProtoBuf 的数据建模、字段类型、嵌套、Map、service、stream 关键字,并结合 gRPC / K8s / Pulsar / Envoy 等现代基础设施展示 ProtoBuf 在跨语言 RPC、配置定义、流式协议中的真实用法。

ProtoBuf & gRPC 实战:proto3 时代的跨语言 RPC 与数据建模

TL;DR:ProtoBuf 是一类语言无关、平台无关、可扩展的二进制结构化数据序列化方法,比 XML 小 3 ~ 10 倍、快 20 ~ 100 倍。2017 年是它真正"工业化"的一年——gRPC 1.0(2017-08 GA)、protoc-gen-validate(2017-08 开源)、Envoy 1.0(2017-09)先后落地,proto3 也从 2016-07 GA 后成为默认语法。本文用 5 个心智模型 + 一组完整示例,讲清 proto3 的数据建模、字段类型、嵌套、Map、service 与四种流模式。

一、为什么需要 ProtoBuf(When to use)

在微服务、跨端通信、持久化序列化场景中,传统 JSON / XML 是人类友好但机器低效的格式:

  • 体积:JSON 里一个 int 字段最少 111 字节,ProtoBuf 用变长编码通常 15 字节
  • 速度:XML 解析要遍历 DOM,ProtoBuf 直接按 schema 二进制切片
  • 类型安全:JSON 拿到的是字符串,ProtoBuf 拿到的是强类型对象
  • 演进能力:ProtoBuf 通过**字段编号(field number)**支持前向/后向兼容,新增字段不会破坏旧客户端

典型适用场景(2017 视角):

  • 微服务间 RPC——gRPC 1.0(2017-08)以 ProtoBuf 作为 IDL 和默认序列化器,跨语言、强类型、双向流
  • 消息队列载荷——Kafka 早已支持 ProtoBuf,Pulsar(2016-09 在 Yahoo! 开源、2017 年逐步成熟)也提供 schema registry 与 ProtoBuf 集成
  • K8s 内部通信——Kubernetes 1.0(2015-07 GA)以来,所有 API 资源(Pod、Service、Deployment…)的 schema 都用 ProtoBuf 定义,apiserver 与 kubelet、scheduler、controller-manager 之间走 ProtoBuf over HTTP/2
  • Service Mesh 数据面——Envoy(2017-09 发布 v1.0)的 xDS(LDS/RDS/CDS/EDS)API 全部基于 ProtoBuf,Istio 等控制面通过 xDS 下发配置
  • 跨语言 SDK 数据交换——前端、移动端、后端用同一份 .proto 各自生成代码
  • 移动端与后端通信——节省流量、电量

不适用场景

  • 需要人类可读的配置(用 YAML / TOML)
  • Web 前端到后端的标准通信(JSON 更通用)
  • 一次性、小数据量、无演进需求的传输

二、ProtoBuf 速览

一个数据交换的协议

protocol buffers(ProtoBuf)是一种语言无关、平台无关、可扩展的序列化结构数据的方法,可用于(数据)通信协议、数据存储等。

是一种灵活、高效、自动化机制的结构数据序列化方法——可类比 XML,但比 XML 更小(3 ~ 10 倍)、更快(20 ~ 100 倍)、更简单

JSON / XML 都是基于文本格式,ProtoBuf 是二进制格式

三、proto3 速览:与 proto2 的关键差异

2016-07 proto3 正式 GA,并成为 protoc 的默认语法。和 proto2 相比,proto3 的核心简化是:

维度proto2proto3
默认语法需显式 syntax = "proto2"2016-07 后默认为 proto3
字段是否存在区分 optional / required / 默认没有 required,字段默认"存在与否"用 optional 关键字显式标注(proto3 默认字段可以被设默认值以"不编码")
默认值0 / 空字符串同上,但未设置字段不会被序列化(节省体积)
枚举第一个值必须为 0同上,但首成员必须为 0,且枚举值在 wire format 中是 varint
未知字段解析时默认保留解析时不保留未知字段(演进更严格)
map 支持旧版本不支持原生支持 map<K, V>
Any 类型google/protobuf/any.proto 扩展官方内置 google.protobuf.Any
时间戳同上同上,但官方推荐 google.protobuf.Timestamp
JSON 映射需插件官方 proto3 <-> JSON 映射规范

实战建议:新项目一律 syntax = "proto3";维护老服务再保留 proto2。

四、第一个 .proto:消息定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 指定 proto3 语法(2016-07 后是默认,但显式声明更稳)
syntax = "proto3";

package foo.bar;            // 包名避免同名 Message 冲突

// option 可用于 proto、message、enum、service 的 scope

// Message ≈ Java 的 class / C 的 struct
message Response {
  // 字段 = 类型 + 名字 + 序号(序号是 wire format 的"身份证",绝不能改)
  string data   = 1;
  int32  status = 2;
}

关键约定

  • package:避免命名空间冲突
  • message:数据结构的最小单元
  • 字段序号 = 1, = 2, ...一旦发布就不可修改、不可复用——这是 ProtoBuf 演进能力的根基
  • syntax = "proto3":2017 主流;不写时 2017 之后的 protoc 默认按 proto3 解析

五、编译器:.proto → 目标语言

官方编译器 protoc 仓库:https://github.com/protocolbuffers/protobuf/releases

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 安装 protoc 后,生成 Java 代码到当前目录
protoc --java_out=. response.proto

# 生成 Go 代码
protoc --go_out=. --go_opt=paths=source_relative response.proto

# 生成 Python 代码
protoc --python_out=. response.proto

# 生成 C++ 代码
protoc --cpp_out=. --proto_path=. response.proto

# 生成 gRPC 桩代码(protoc + 插件 grpc-plugin,gRPC 1.0 GA 后官方推荐)
protoc --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` response.proto

小贴士:生成的代码不要手动改——下次 protoc 会被覆盖。业务代码继承/组合生成的类即可。

六、数据类型映射表

proto 类型说明java 类型golang 类型
double双精度浮点doublefloat64
float单精度浮点floatfloat32
int32变长编码,对负值效率低,负值请用 sint32intint32
uint32变长编码intuint32
uint64变长编码longuint64
sint32变长编码,负值比 int32 高效intint32
sint64变长编码,有符号longint64
fixed32固定 4 字节,值域常大于 2²⁸ 时比 uint32 高效intuint32
fixed64固定 8 字节,值域常大于 2²⁵⁶ 时比 uint64 高效longuint64
sfixed32固定 4 字节intint32
sfixed64固定 8 字节longint64
bool布尔booleanbool
string必须 UTF-8 或 7-bit ASCIIStringstring
bytes任意二进制(文件、图片)ByteString[]byte

实战口诀:负数 → sint32/sint64;大正整数 → fixed32/fixed64;其余 → int32/uint32

七、枚举、数组、嵌套、导入

7.1 枚举

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
syntax = "proto3";

enum PhoneType {            // 枚举首成员必须为 0,且值唯一
  MOBILE = 0;
  HOME   = 1;
  WORK   = 2;
}

message PhoneNumber {
  string    number = 1;
  PhoneType type   = 2;
}

7.2 数组(repeated)

1
2
3
4
5
message Msg {
  repeated int32       arrays = 1;   // 整数数组
  repeated string      names  = 2;   // 字符串数组
  repeated PhoneNumber phones = 3;
}

7.3 嵌套消息 vs 跨文件导入

1
2
3
4
5
6
7
8
9
// 嵌套:把 Result 写进 SearchResponse 内部
message SearchResponse {
  message Result {
    string         url      = 1;
    string         title    = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 跨文件导入:把 Result 抽到 result.proto
// result.proto
syntax = "proto3";
package foo.bar;
message Result {
  string         url      = 1;
  string         title    = 2;
  repeated string snippets = 3;
}

// search_response.proto
syntax = "proto3";
package foo.bar;
import "result.proto";
message SearchResponse {
  repeated Result results = 1;
}

proto3 中不允许直接嵌套枚举(嵌套 enum 必须先 message 包一层),这是与 proto2 的小差异。

八、Map / 时间戳 / Any

8.1 Map 类型

1
2
3
4
5
6
7
syntax = "proto3";

message Product {
  string name = 1;
  // key 不能是浮点 / bytes / enum;value 不能是另一个 map
  map<string, string> attrs = 2;   // 商品属性 K/V
}

Map 字段不能repeated 修饰;map 不能迭代顺序依赖。

8.2 时间戳

1
2
3
4
5
6
7
syntax = "proto3";

import "google/protobuf/timestamp.proto";

message MyMessage {
    google.protobuf.Timestamp my_field = 1;
}

简单场景可直接用 int64 时间戳(毫秒/秒),看团队约定。

8.3 Any 任意类型

1
2
3
4
5
// Any 可以承载 .proto 未定义的任意内置类型
message ErrorStatus {
  string                       message = 1;
  repeated google.protobuf.Any details = 2;
}

K8s API 的 runtime.RawExtension 字段底层就是 Any——apiserver 把不识别的对象原样塞进去。

九、定义服务

proto3 原生支持 service 定义,与 gRPC 1.0 配合即可生成跨语言 RPC 桩代码:

1
2
3
service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

gRPC 1.0(2017-08)默认走 HTTP/2 + ProtoBuf,这是 ProtoBuf 在 RPC 领域最主流的落地形态

十、stream 关键字:四种流模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 客户端推 → 服务端(client-side streaming)
rpc GetStream (StreamReqData) returns (stream StreamResData) {}

// 服务端推 → 客户端(server-side streaming)
rpc PutStream (stream StreamReqData) returns (StreamResData) {}

// 双向流(bidirectional streaming)
rpc AllStream (stream StreamReqData) returns (stream StreamResData) {}

// 单调用(非流式)
rpc Unary (UnaryReq) returns (UnaryRes);

典型场景

  • 客户端流:IoT 设备上报(go2rtc 这类轻量级流媒体代理 / RTSP-over-WebRTC 设备采集后的元数据上报就属于这一类)
  • 服务端流:股票行情、消息推送、K8s watch API
  • 双向流:聊天、协同编辑、Envoy xDS(控制面 ↔ 数据面长连接推送配置)
  • 单调用:普通 RPC

Envoy 1.0 的 xDS(LDS/RDS/CDS/EDS)正是 gRPC 双向流的典型案例——控制面(Istio Pilot 等)通过 gRPC stream 主动推送配置变更给数据面(Envoy)。

十一、数据校验:protoc-gen-validate(2017-08)

ProtoBuf 本身不包含字段值校验(最小值、最大值、长度、枚举合法性等)。2017-08 开源的 protoc-gen-validate(Envoy 项目出品)补齐了这一点:

仓库:https://github.com/envoyproxy/protoc-gen-validate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
syntax = "proto3";

package foo.bar;

import "validate/validate.proto";

message CreateUserRequest {
  // 用户名:3~32 字符,正则限制
  string username = 1 [
    (validate.rules).string = {
      min_len: 3
      max_len: 32
      pattern: "^[a-zA-Z0-9_]+$"
    }
  ];

  // 年龄:0~150
  int32 age = 2 [(validate.rules).int32 = {gte: 0, lte: 150}];

  // 邮箱:必须是 email 格式
  string email = 3 [(validate.rules).string = {email: true}];

  // 列表元素非空、长度限制
  repeated string tags = 4 [
    (validate.rules).repeated = {
      min_items: 1
      max_items: 10
      items: {string: {min_len: 1, max_len: 32}}
    }
  ];
}
1
2
3
4
5
6
7
# 安装 protoc-gen-validate 后,protoc 会生成带校验逻辑的桩代码
protoc \
  -I . \
  -I /path/to/protoc-gen-validate \
  --go_out=. \
  --validate_out=lang=go,paths=source_relative:. \
  create_user.proto

相比 JSR 303 Bean Validation(Java 生态),protoc-gen-validate 的优势是校验规则写在 .proto 里、跨语言、跨 RPC 框架一致——这正是 2017 年 gRPC + Envoy 生态推崇的"单一来源"做法。

十二、现代基础设施里的 ProtoBuf

2017 年这个时间点,ProtoBuf 已经不只是"序列化格式",而是分布式系统的事实 IDL

12.1 Kubernetes:所有 API 资源都是 ProtoBuf

Kubernetes 1.0(2015-07 GA)以来,所有 API 资源(Pod、Service、Deployment…)的 schema 都定义在 k8s.io/api 仓库的 .proto 文件中:

  • apiserverkubelet / scheduler / controller-manager:通过 HTTP/2 + ProtoBuf 通信
  • CRD / Operator:用户自定义资源也走 .proto(或 OpenAPI 自动生成)
  • kubectl:本地构造 ProtoBuf 对象 → 走 apiserver 序列化

典型 .proto 片段(简化版):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// k8s.io/api/core/v1/generated.proto
syntax = "proto3";
package k8s.io.api.core.v1;

import "k8s.io/apimachinery/pkg/runtime/generated.proto";

message Pod {
  map<string, string> labels       = 1;
  map<string, string> annotations  = 2;
  PodSpec            spec          = 3;
  PodStatus          status        = 4;
}

12.2 gRPC:ProtoBuf 跨语言 RPC 的事实标准

gRPC 1.0(2017-08 GA)由 Google 开源(2015 年从内部 Stubby 演进,移到 GitHub),以 ProtoBuf 作为 IDL 和默认序列化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// user.proto
syntax = "proto3";

package user;

service UserService {
  // 单调用
  rpc GetUser (GetUserRequest) returns (User);
  // 服务端流
  rpc ListUsers (ListUsersRequest) returns (stream User);
  // 客户端流
  rpc CreateUsers (stream User) returns (CreateUsersResponse);
  // 双向流
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message User { string id = 1; string name = 2; }

gRPC 关键特性

  • HTTP/2 多路复用
  • ProtoBuf 二进制序列化
  • 双向流、四种流模式
  • 自动生成 10+ 语言桩代码(Java/Go/C++/Python/Ruby/Node/PHP/C#/Dart/Android-Java)
  • 截止时间(deadline)、取消、metadata 等通用拦截语义

12.3 Pulsar:消息总线里的 ProtoBuf

Apache Pulsar(2016-09 在 Yahoo! 开源、2017 年逐步孵化成 Apache 顶级项目)原生支持 ProtoBuf schema:

  • 内置 Schema.PROTOBUF,producer / consumer 用 .proto 自动反序列化
  • SchemaRegistry 配合做 schema 演进
  • 消息载荷紧凑、跨语言、多租户隔离

12.4 Envoy / xDS:Service Mesh 的数据面 API

Envoy(Lyft 开源,2017-09 发布 v1.0)的 xDS 协议族是 gRPC + ProtoBuf 的工业级范本:

  • LDS(Listener Discovery Service):下发监听器配置
  • RDS(Route Discovery Service):下发路由配置
  • CDS(Cluster Discovery Service):下发集群配置
  • EDS(Endpoint Discovery Service):下发端点配置

控制面(Istio Pilot 等)通过 gRPC 双向流把 ProtoBuf 消息增量推送给数据面(Envoy),实现配置热更新。

这套 xDS 协议后来成为 Istio(2017-05 由 Google/IBM/Lyft 联合开源)的核心数据面 API。

12.5 IoT / 视频流场景

ProtoBuf 在 IoT 与视频流领域的应用同样广泛——比如轻量级流媒体代理在设备元数据、控制信令、配置同步上都会用 .proto 定义设备能力集,避免重复造轮子。go2rtc 这类工具虽然面世更晚,但同样遵循"ProtoBuf 作为控制面信令、媒体流走 RTSP/WebRTC"的分层思路。

十三、协议升级的视角:IPv4→IPv6 给我们的启示

2017 年前后正值 IPv6 规模部署讨论。把网络协议序列化协议放一起看,能发现共通的设计哲学:

协议升级对现有程序的影响(参考 IPv4→IPv6)

  1. 地址格式的硬编码问题——视频监控、配置文件、程序中字面量硬编码
  2. 网络 API / 库的协议版本依赖——Java 库要使用更通用的 InetAddress;Go 视频播放客户端优先 net.LookupIP 同时解析 A 与 AAAA
  3. 地址解析与 DNS 交互逻辑——程序需支持"双 DNS 查询"(同时请求 AAAAA 记录)
  4. 操作系统层面支持
  5. 运行时 / 容器——Java 需确保 java.net.preferIPv6Addresses=true;libc 解析器需开启 getaddrinfoAI_ADDRCONFIG;Docker / K8s 需配置 IPv6 网络模式(--ipv6 flag、CNI 插件)
  6. 防火墙规则——IPv4 规则需重新配置 IPv6 专属规则

升级建议路径

阶段目标
第一阶段支持双栈兼容(IPv4/IPv6 并存)
第二阶段修复IPv6 专属问题(DNS、MTU、链路本地地址)
第三阶段全量 IPv6,下线 IPv4

类比到 ProtoBuf:

  • 第一阶段(双栈):旧字段保留 + 新字段加 optional 标注
  • 第二阶段(修复兼容问题):使用 reserved 关键字冻结废弃字段编号(防止误用)
1
2
3
4
5
6
7
message User {
  reserved 5, 7, 9 to 11;          // 保留字段编号,未来也不能用
  reserved "old_name", "old_email"; // 保留字段名

  string name  = 1;
  string email = 2;
}
  • 第三阶段(全量):灰度下架旧字段、提供 schema 校验工具

十四、常见 5 个坑

#现象对策
1修改已发布字段的序号旧客户端解码新数据字段错位、值乱跳一旦发布,序号就是合约;只能加新字段 + 用 reserved 废弃旧的
2误用 int32 存负数负数被编码为 10 字节超大无符号数负数统一用 sint32 / sint64
3string 字段塞非 UTF-8反序列化报错、跨语言崩溃文本类字段用 string;二进制用 bytes
4repeated 字段不设上限大消息体 OOM、带宽爆炸Service 入口拦截层加 Collection.size() 校验;或用 protoc-gen-validaterepeated.min_items / max_items 限制
5proto2 / proto3 混用wire format 行为不同,反序列化失败团队统一 syntax = "proto3";proto2 仅在维护老服务时保留

十五、核心 6 点速记

  1. 字段编号 = 合约:一旦发布就不可改;用 reserved 冻结废弃编号
  2. 负数用 sint:变长编码对负值有专门优化
  3. stream 关键字:四种流模式决定 ProtoBuf 自带 RPC 通信方向——gRPC 1.0 把它变成跨语言标准
  4. 校验靠 protoc-gen-validate:2017-08 开源,跨语言、.proto 单一来源,比 JSR 303 更适合 gRPC 时代
  5. 现代基础设施统一用 .proto 作为 IDL:K8s API、Pulsar schema、Envoy xDS、gRPC、CRD
  6. 演进式升级:双栈 → 修复 → 全量,与 IPv4→IPv6 的协议升级哲学一致

十六、参考资料

使用 Hugo 构建
主题 StackJimmy 设计