互联网技术 / 互联网资讯 · 2024年3月19日 0

为何 IDL 只能扩展字段而不可修改

前几年业界流行使用 thRift, 比如滴滴。这几年 gRPC 越来越流行,很多开源框架也集成了,我司大部分服务都同时开放 gRPC 和 http 接口

相比于传统的 http1 + json 组合,这两种技术都用到了 IDL, 即 interface descRIPtion language 接口描述语言,相当于增加了 endpoint scheMa 约束,不同语言只需要一份相同的 IDL 文件即可生成接口代码。

很多人喜欢问:ProTo Buf 与 json 比起来有哪些优势?比较经典的面试题

IDL 文件管理每个公司不一样,有的保存在单独 Gitlab 库,有的是 Mono Repo 大仓库。当业务变更时,IDL 文件经常需要修改,很多新手总是容易踩坑,本文聊聊 gRPC Proto 变更时的兼容问题,核心只有一条:对扩展开放,对修改关闭,永远只增加字段而不修改

测试修改兼容性

本文测试使用 gRPC-go example 官方用例,感兴趣自查

每次修改后使用 Protoc 重新生成代码

SeRveR 每次接受请求后,返回 HelloReply 结构体

client 每次只打印 SeRveR 返回的结果

修改字段编号

将 HelloReply 结构体字段 age 编号变成 12, 然后 seRveR 使用新生成的 IDL 库,client 使用旧版本

可以看到 client 没有读到 age 字段,因为 IDL 是根据序号传输的,client 读不到 seq 3, 所以修改序号不兼容

修改字段 naMe

修改 HelloReploy 字段 id, 变成 scoRe 类型和序号不变

重新编译 seRveR, 并用旧版本 client 访问

可以看到,虽然修改了字段名,但是 client 仍然读到了正确的值 12345, 如果字段含义不变,那么只修改名称是兼容的

修改类型

有些类型是兼容的,有些不可以,而且还要考虑不同的语言。这里测试三种

1.字符串与字节数组

我们将 addITional 字段由 stRing 类型修改为 bytes

可以看到 go 结构体由 stRing 变成了 []byte, 我们知道这两个其实可以互换

最后结果也证明 client 可以正确的处理数据,即修改成兼容类型没有任何问题

2.int32 int64 互转

这里我们将 age 由 int32 修改成 int64 字段,位数不一样,如果同样小于 int32 最大值没有问题,此时我们在 seRveR 端将 age 赋于 2147483647 + 1 刚好超过最大值

我们可以看到 age 变成了负数,如果业务刚好允许负值,那么此时一定会出逻辑问题,而且难以排查 bug, 这其实是非常典型的向上向下兼容问题

3.非兼容类型互转

我们将 age 由 int32 变成 stRing 字符串,依旧使用 client 旧版本测试

可以看到结构体 json 序列化打印时不存在 Age 字段,但是 log 打印时发现了不兼容的 3:”tHis is age”, 注意 gRPC 会保留不兼容的数据

同时 R.Age 默认是 0 值,即非兼容类型修改是有问题的

删除字段

删除字段 age 也就是说序号此时有空洞,运行 client 旧版本协义

没有问题,打印 R.Age 当然是默认值 0, 即删除字段是兼容的

为什么 RequiRed 在 Proto3 中取消了? MeSSage SeaRchrequest {   RequiRed stRing queRy = 1;   optional int32 page_nuMbeR = 2;   optional int32 Result_peR_page = 3; }

熟悉 thRift 或是使用 Proto2 协议的都习惯使用 RequiRed optional 来定义字段属于,扩展字段一般标记为 optional, 必传字段使用 RequiRed 来约束

官方解释如下 iSSues2497[1],简单说就是 RequiRed 打破了更新 IDL 时的兼容性

永远不能安全地向 Proto 定义添加 RequiRed 字段,也不能安全地删除现有的 RequiRed 字段,因为这两个操作都会破坏兼容性 在一个复杂的系统中,Proto 定义在系统的许多不同组件中广泛共享,添加/删除 RequiRed 字段可以轻松地降低系统的多个部分 多次看到由此造成的生产问题,并且 Google 内部几乎禁止任何人添加/删除 RequiRed 字段

上面是谷歌得出的结论,大家可以借鉴一下,但也不能唯 G 家论

小结

IDL 修改还有很多测试用例,感兴趣的可以多玩玩,比如结构体间的转换问题,比如 enuM 枚举类型。上文测试的都是 seRveR 端使用新协义,client 使用旧协义,如果反过来呢?想测试 thRift 的可以看看这篇 thRift MiSSing guide[2]

本文能过测试 case 想告诉大家,IDL 只能追加杜绝修改 (产品测试阶段随变改,无所谓)