Morrison.J Android Dev Engineer

Protobuf(五):最佳实践

2023-03-19
Jasper

本文是protobuf best practices的概要性比较,整理了一下实际使用过程中比较常见的注意点和实践经验。

Proto Best Practices

  • Don’t Re-use a Tag Number
    别重用tag number ,这个会破坏反序列化的正确性。
  • Don’t Change the Type of a Field
    满足兼容性的field可以变更type,但在工程上,我们极力不推荐去做这个事。
  • Don’t Add a Required Field
    required field从长远来看,就是一个破坏兼容性的做法,proto3干脆直接去掉这个option。
  • Don’t Make a Message with Lots of Fields
    何为“多”?百个或者说几百个为多,C++里面,最多使用8个字节来存储字段的长度,但是,还需要存储一些其它辅助信息,比如字段是否被赋值过等等。具体多少是上限,官网没有给出来。
  • Enum相关的这里直接忽略;
  • Do Use Well-Known Types and Common Types
    推荐使用一些公开常用的公共类型,这些类型会给我们带来较多的便利;
  • Do Define Widely-used Message Types in Separate Files
    倡导将需要提供给外部“广泛使用”的Message,定义在独立的文件中,不要与工程内部的protobuf定义混淆在相同的文件中。
  • Do Reserve Tag Numbers for Deleted Fields
    注意将已删除的field number和 field names置为reserved。
  • Don’t Change the Default Value of a Field
    这里涉及field presence问题,我们认为,保持让工具本身设置默认值是最佳的兼容性做法。
  • Don’t Go from Repeated to Scalar
    标量与重复量之间转换要慎重,笔者认为,直接避免这种做法为好。
  • Never Use Text-format Messages for Interchange
    Text-format是仅供调试使用的,不要使用于消息交换。因为protobuf的兼容性保证并不包括field name。
  • Never Rely on Serialization Stability Across Builds
    这里的意思是proto编译工具的跨版本问题,跨版本编译工具得到的代码相互之间是不保证兼容性的。实际上,处理C++,相同Major版本的编译工具是保证兼容性的,官方有这样的承诺。
  • Don’t Generate Java Protos in the Same Java Package as Other Code
    这里意思是,最好你现有的java package的后面加上”.proto”作为protobuf所生成java代码的包名。

API Best Practices

这里不是特定语言(如:C/C++、 Java等)的API最佳实践,而是对上一个小节的补充,我们希望这个小节,可以帮助开发者开发出长期稳定、API兼容性极佳的protobuf程度。

  • Use Different Messages for Wire and Storage
    存储和交换使用不同的Message,这样会给兼容性开发带来极大的好处。
  • Don’t Include Primitive Types in a Top-level Request or Response Proto
    这个记录一下,提示:不要在Top-level的请求或者响应的proto中包含原始类型,可以保持请求或者响应proto不用频繁变更。
  • Rarely Use an Integer Field for an ID
    不要使用整型数作为对象的唯一标识,int64很诱人,但是不一定够。使用string反而可以保持较好的拓展性。
  • Don’t Encode Data in a String That You Expect a Client to Construct or Parse
    这是说,不要把你的结构型数据编码为string。我常常看到有同事将结构型数据编码为string,并为某种字符将其隔开,比如逗号、分号。
  • Make Repeated Fields Messages, Not Scalars or Enums
    这个的思路与前面的封装思想是一样的,在创建repeated字段时,不要使用原始类型,是应该创建一个message来包装这个原始类型,这样做的好处是,可以对这个message进行升级,而无需修改顶层结构。
  • Bound Request and Response Sizes
    这个很重要,我们应该限定proto message的大小,往repeated中传递数据时,应当先进行大小检查。
  • Returning Repeated Fields
    repeated field如果是empty的,接收端无法仅仅通过这个信息判断是否正确响应,有可能是发送端因出错而没有填充数据,也有可能是数据的确是empty的。建议的方案是使用一个mask字段来表示哪些字段已经正确填充或者故意留空。笔者认为,这个有点类似field presence问题,可以根据自己的业务实际情况,选择性使用field mask
  • Order Independence in Repeated Fields
    这是一个设计上的改进建议,我们应该保证重复字段的次序独立性。什么意思呢?就是说,我们不应该将repeated包含原始类型,而应该对原始类型封装一下。这样,repeated中的每一个具体的值,相互的关系有封装的类型决定,而不是它们的位置决定。这个设计理念也是重复出现了,可以根据实际应用来进行考量。
  • Performance Optimizations
    性能优化是实际中比较值得学习的地方。
    • 少量的字段肯定比大量的字段解析得更快;
    • 将大的message分割为多个小的message;
    • 声明延迟解析,“lazily parsed” with [lazy=true];
    • 直接将filed声明为bytes,并指定其类型,让解析端根据需要将其解析为对应的数据类型;但这是一个危险的做法,不建议做;

参考

https://protobuf.dev/programming-guides/dos-donts/
https://protobuf.dev/programming-guides/api/


Comments

Content