Protobuf 让 Schema 演进而不破坏编码
向后兼容来自两点:字段编号绝不重用;新增字段保持可选,未知字段被跳过而不是报致命错误。 编号就锁定了含义 protobuf 字段在被分配编号的那一刻,就把这个编号当作线上身份的契约;名字反而可以被随意改,wire 类型不能动。tag 号到名字的映射要小心看护,绝不要在同一号上换个含义——否则,还拿着旧定义的老进程,在看到同一编号时就会把它静默当成别的含义去解读。 .proto 里声明顺序只是写给人的便利,真正在线路上传输的是一串 (字段编号、wire 类型、值) 的三元组。只要两边的编号一致,就能安全地跳过自己看不懂的字段。这就是 protobuf 向前兼容的来源:在字段真正出现之前,先用 reserved 把一个位子占住,等于预先销出了一个稳定命名空间。 新旧双方共用一条线路 要做到前后向兼容,靠的是一组朴素的规则。向后兼容指新版程序能读旧数据写入的字节:所以旧字段要一直在,不需要时也要 reserved 起来。向前兼容指旧版本程序能容忍新 schema 写出来的数据:因为未知字段会被保留并原样写回。加一个新号字段这两个方向都兼容,改已有字段的 wire type 则不兼容。 真正的坑往往不在字段本身,而在编号的复用。删除一个编号然后换个含义,会让所有还带着旧定义、拿着旧库的老客户端读到全错的东西。所以习惯是:用 reserved 占位,不删除再复用;同一个号永远别改义;基本标量类型保持稳定;至于取值非法这类问题,要在数据进入系统的边界处用校验挡住。 未知字段跳过而非失败 让演进显得容易的特性,其实藏在编码里。未知字段会被保存下来、原样透传,于是某个不完全了解新 schema 的中间进程(网关、日志管道),也可以照常帮忙转发,而不会把新字段弄丢。这也意味着当 schema 变化时,你不是非要立刻把整条链路都升级。 要让这些好碰上,还得在同一位面持有纪律:全团队共用同一套 .proto 生成码、维护一个 schema 注册表、把最老与最新的客户端放一起做交叉版本测试、把字段编号列成 lint 让人看得见,以及一旦类型必须改,就用「新字段 + 废弃旧字段」来处理;这样新版旧版并行地主保持可读,而不是静默地把兼容性留给下一次部署去赌。 ...