最近刷到一篇Netflix的技术博客,提到用 FieldMask 在grpc中实现类似 GraphQL 中的 field selectors 或 Json Api 标准中的 Sparse Fieldsets
文章中提到的痛点目前公司系统中也是存在的,但是没有那么明显,通过很不优雅的添加一个额外标记字段解决了99%的问题。如今遇到好的解决方案记录,学习以增加姿势储备
痛点
- 文章中提到,Netflix后端通信使用grpc。在处理请求时,如果能知道哪些响应字段是可忽略不返回的,是很有用的,毕竟某些字段可能需要经过复杂的下游请求或密集计算的结果
对于此痛点,在GraphQ / Json Api 标准中,分别有对应的解决方案 field selectors / Sparse Fieldsets,而在grpc场景下,他们选择了 FieldMask - 对于一个拥有众多属性的对象要进行更新操作时,本着职责单一的原则,一个需求会定义一个接口,会导致接口数量随着操作需求的增加而不断增加但这显然看起来是很凌乱的,实际上维护起来也是很困难的,如果要穷尽组合,那么接口数量将十分可观
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// separate RPC for every field, not recommended
service ProductionService {
rpc UpdateProductionFormat (UpdateProductionFormatRequest) {...}
rpc UpdateProductionTitle (UpdateProductionTitleRequest) {...}
rpc UpdateProductionSchedule (UpdateProductionScheduleRequest) {...}
rpc UpdateProductionScripts (UpdateProductionScriptsRequest) {...}
}
message UpdateProductionFormatRequest {...}
message UpdateProductionTitleRequest {...}
message UpdateProductionScheduleRequest {...}
message UpdateProductionScriptsRequest {...}
当然,也有只提供一个接口,然后update所有字段的操作,太骚了,把握不住不推荐
FieldMask
grpc使用protobuf作为 IDL (interface definition language, 接口定义语言)及接口序列化协议
FieldMask是field_mask.proto中定义的一个message,如下
1 | // ## Field Mask Verification |
Get 场景应用
博客中通过一个根据 production_id 获取 production 详情的例子来说明FieldMask在Get
场景下的作用,如下的proto定义
1 | // Contains Production-related information |
其中 ProductionScript/ProductionSchedule两个字段都是需要通过额外的grpc请求才能获取到数据
所以如果请求方并不需要这两个字段,那么这些额外的rpc请求就是被浪费的,不仅耗费更多的时间,也增加网络传输的成本,属于费力不讨好
然后也举例可以通过增加额外标记字段(我们仍在用,惭愧)的方式来判断是否需要返回这些字段
1 | // Request with one-off "include" fields, not recommended |
This approach requires adding a custom includeXXX field for every expensive response field and doesn’t work well for nested fields.
It also increases the complexity of the request, ultimately making maintenance and support more challenging.
但它显而易见的一堆劣势,额外的字段、嵌套结构不适用、提高复杂性、使用/维护困难。。然后进入正题,使用FieldMask解决问题
1 | // 记得确保本地已有或通过`-I`指定搜寻路径 |
1 | // FieldMask的定义在 google.golang.org/protobuf/types/known/fieldmaskpb/field_mask.pb.go |
具体使用可参考下方示例代码
Update 场景
为了解决Update场景下的痛点,需要客户端设置fieldmask,然后由服务端通过判断来决定如何操作
对于单个或多个字段的更新操作,可由一个接口来代替
服务端通过 fieldmask 信息,来判断需要处理哪些字段,不再是无脑全量更新或一个操作一个接口的模式,极大的提升接口的灵活性
对于 fieldmask,java中由 FieldMaskUtil 提供的工具十分方便,目前go的工具还比较少
可以考虑下 fieldmask-utils,或自行按需求封装轮子
API designers should aim for simplicity, but make their APIs open for extension and evolution.
It’s often not easy to keep APIs simple and future-proof.
Utilizing FieldMask in APIs helps us achieve both simplicity and flexibility.
具体使用可参考下方示例代码
示例代码,包含了Get及Update场景
1 | package main |
proto定义
1 | syntax = "proto3"; |
注意
缺省场景
没有设置fieldmask或为空时,应当处理所有的信息(如返回,update)
重命名
fieldmask 是利用字符串形式的字段名来匹配的,而protobuf传递过程中是忽略字段名而是传递序号,接收方通过需要解析出字段名
所以,如果服务端修改了字段名,而客户端未同步,那么不用fieldmask的情况下是没有问题的,但如果在fieldmask中使用了该字段,将导致无法解析
文中提到三个方案:
- 不运行重命名字段,但这似乎是不可能的
- 要求服务端支持所有旧字段名,向后兼容,这意味着额外的代码
- 废弃字段,然后创建一个新的字段(最优)
参考链接
Netflix 技术博客 1
Netflix 技术博客 2
field_mask.proto
fieldmask-utils
原文链接