服务升级时,新旧客户端和服务端混着跑,哪些改动是安全的?哪些会悄悄把数据弄错?这篇用动态构造的两个版本的 .proto,在字节层面逐项对比,并用真实 gRPC 验证了“加字段经过旧服务端不丢”。
新版本写出 0a0568656c6c6f10072209e696b0e5ad97e6aeb5(20 字节);旧版本读到:{msg=hello, n=7} + 未知字段 11 字节 2209e696b0e5ad97e6aeb5
旧版本原样写回(20 字节),新版本再读:{msg=hello, n=7, extra=新字段}
真实 gRPC:新版客户端(带 extra)→ 用旧版生成代码的服务端 → 服务端把收到的请求原样放进响应:err=<nil>;新版客户端解出 {msg=hi, n=1, extra=新字段}protobuf 二进制只写字段编号和值,不写名字。编号和线上类型(wire type)决定兼容性,名字只影响 JSON 与代码。
二进制:旧写 0a05616c696365,新读:{full_name=alice}
JSON:旧写 {"name":"alice"},新版 protojson 解析的结果:{};err=proto: (line 1:2): unknown field "name"安全:旧版本会保留不认识的字段,转发或写回时原样带出,经过旧服务端也不丢。
旧写 n=42 的 0a0178102a,新读:{msg=x} + 未知字段 2 字节 102a二进制兼容,但 JSON 按名字,改名对 JSON 消费方(网关、日志、前端)是破坏性的。
int32 → int64 线上 0805,读到:{v=5}
int32 → int64(值为 -1) 线上 08ffffffffffffffffff01,读到:{v=-1}
int32 → sint32 线上 0805,读到:{v=-3}
int32 → sint32(值为 -1) 线上 08ffffffffffffffffff01,读到:{v=-2147483648}
int32 → bool 线上 0805,读到:{v=true}
int32 → string 线上 0805,读到:{} + 未知字段 2 字节 0805
int32 → fixed32 线上 0805,读到:{} + 未知字段 2 字节 0805
string → bytes 线上 0a03616263,读到:{v=[97 98 99]}改编号或复用已删字段的编号,会悄悄丢数据或读到含义错位的值,不一定有报错。删字段后要 reserved 编号。
旧写 0a0178102a,新读(编号 2 当 string):{msg=x} + 未知字段 2 字节 102a
如果复用成另一个 int32 字段(含义完全不同):{msg=x, label=42} ← 读到的是旧字段的值,但含义已不同,不会有任何报错编码相同的类型可以互改(int32/int64、string/bytes);换成 sint32、bool、fixed32、string 会读出错值或变成未知字段。
新写 0803(枚举值 3),旧读:{s=(未知枚举值 3)}
旧版本原样写回,新版本再读:{s=RETRY}proto3 的枚举是开放的,新值保留数值,旧代码要有 default 分支。
标量 5 → repeated:线上 1005,读到:{nums=[5]}
repeated [1 2 3] → 标量:线上 1203010203,读到:{} + 未知字段 5 字节 1203010203标量可以改成 repeated(旧发送方的数据仍可读),反过来不行(packed 的 repeated 读不进标量)。