跳到内容

第 7 卷 / 第 2 章 / 第 1 课

2.1 RPC 与序列化契约:远程调用不是本地函数 ​

RPC 用类似函数调用的接口封装网络通信,但它不能继承本地调用的可靠性。远程调用有延迟、序列化、独立部署、版本并存和结果未知,API 必须把这些差异暴露出来。

IDL 是跨进程契约 ​

下面是一份完整的 scoring.proto,后文的 Player 定义也可以追加到这个文件。

proto
syntax = "proto3";

package atlas.scoring.v1;

service ScoringService {
  rpc GetScore(GetScoreRequest) returns (GetScoreResponse);
  rpc WatchRanking(WatchRankingRequest) returns (stream RankingUpdate);
}

message GetScoreRequest {
  string player_id = 1;
}

message GetScoreResponse {
  string player_id = 1;
  int64 points = 2;
  int64 version = 3;
}

message WatchRankingRequest {
  string tournament_id = 1;
  optional int64 after_version = 2;
}

message RankingUpdate {
  int64 ranking_version = 1;
  repeated GetScoreResponse scores = 2;
}

消息生成器和 gRPC 插件可以生成消息类与 stub,但类型检查不会替你约定业务含义。这里约定 points 是整数积分,GetScoreResponse.version 是该选手积分版本;ranking_version 则是赛事榜单版本,两种版本不能互相比较。

WatchRanking 每条响应是该赛事的一份完整榜单快照,版本严格递增。不传 after_version 表示先取当前快照再订阅;传入时只返回大于该版本的快照。历史已过保留期时返回 FAILED_PRECONDITION,客户端重新请求完整快照。optional 让“没给续传位置”与“明确从版本 0 之后开始”能够区分。字段单位、错误、deadline、幂等和兼容政策都需要这样的文字契约。

Protobuf 编码依赖字段号和 wire type ​

在 Protobuf 二进制格式中,字段由编号识别,不传输源码中的字段名;ProtoJSON 使用字段名,因此改名仍可能破坏 JSON 客户端。整数常使用 varint,字符串和嵌套消息使用 length-delimited 编码。

这通常带来紧凑表示和快速解析,但不能脱离 payload、语言实现和压缩方式宣称固定倍率。JSON 的可读性、浏览器生态和调试便利仍可能更适合公开 API。

Schema 演进规则 ​

  • 已发布字段号不能改变含义;
  • 删除字段后 reserved 编号和名称;
  • 不把标量字段随意改成 wire/语义不兼容的类型;
  • 枚举保留明确的零值 UNSPECIFIED;
  • 新字段要让旧客户端忽略后仍得到安全行为;
  • 不依赖接收方理解未知字段完成关键业务逻辑;二进制解析通常会保留未知字段,但转成 JSON 或逐字段复制可能将其丢弃。
proto
enum PlayerStatus {
  PLAYER_STATUS_UNSPECIFIED = 0;
  PLAYER_STATUS_ACTIVE = 1;
  PLAYER_STATUS_SUSPENDED = 2;
}

message Player {
  reserved 4;
  reserved "legacy_rank";

  string player_id = 1;
  string display_name = 2;
  PlayerStatus status = 3;
}

新增字段或枚举值也可能造成语义破坏,例如旧客户端把不认识的状态错误地当作“允许参赛”。兼容测试要覆盖真实生成代码和业务分支。proto3 未声明 optional 的标量通常无法区分未传值与显式传入默认值;把缺失积分默认为 0 是否合理,应由业务明确决定。

四种 RPC 形态 ​

形态请求响应适合
Unary11普通查询与命令
Server streaming1多订阅更新、逐批返回
Client streaming多1批量上传、聚合
Bidirectional streaming多多双向独立流、交互会话

不要因为框架支持流就把所有分页查询改成长流。流式 RPC 需要定义背压、取消、断线续传、顺序、checkpoint 和最大消息大小。

边界模型不要复用内部实体 ​

把 ORM Entity 直接作为 RPC 消息会耦合数据库结构、延迟加载和公开契约。Protobuf 消息是传输模型,应在适配器边界映射到领域或应用模型。

同理,一个巨大的 Common.proto 会让所有服务共享同一演进节奏。只共享真正稳定的标识和公共协议,业务模型由拥有它的上下文发布。

协议选择 ​

维度JSON/HTTPgRPC/Protobuf
人类调试直接专用工具配合反射服务或本地 schema
浏览器/公共生态成熟常需代理或 Web 支持
强 IDL 与代码生成可由 OpenAPI 提供原生工作流成熟
流式 RPC可实现但形式多四种形态明确
二进制效率依赖 JSON 与压缩通常更紧凑
版本风险字段与语义字段号、schema 与语义

下一课处理 RPC 的运行语义:deadline、取消、重试和背压。

参考资料 ​

Built with VitePress | Software Systems Atlas