三个特点

广义上来说,所有本地应用程序外的调用都可以归类到rpc,不管是分布式服务,第三方服务的http接口,还是redis的一次请求。从抽象的角度来讲,它们都一样上RPC,由于不在本地执行,都有如下三个特点。所有的RPC框架都是围绕这几个点做优化:

  1. 需要实现约定调用的语义,接口语法
  2. 需要网路传输
  3. 需要约定网络传输协议的内容格式

    如何用Protobuf组织内容

    要以更优的方案,达到更低的成本,更快的速度,内容编码方式就是一个非常重要的点,rpc调用的request和response内容在调用中有着不小的开销:

  4. 内容的序列化,反序列化,如果效率更高,则对cpu的消耗更小

  5. 内容会在网络中传输,有协议栈拷贝成本,带宽成本,GC等。体积越小,效率越高

在这方面,RPC框架都有一下几个目标:

  1. 能快速地序列化,反序列化
  2. 序列化体积越小越好
  3. 跨语言,和语言栈无关
  4. 简单,类型明确
  5. 易拓展,可以简单迭代,向后兼容

grpc对此的解决方案是丢弃json, xml这种传统策略,使用protobuf buffers。是google开发的一种跨语言,跨平台,可扩展的用于序列化数据的协议

  1. // XXXX.proto
  2. service Test {
  3. rpc HowRpcDefine (Request) returns (Response) ; // 定义一个RPC方法
  4. }
  5. message Request {
  6. //类型 | 字段名字| 标号
  7. int64 user_id = 1;
  8. string name = 2;
  9. }
  10. message Response {
  11. repeated int64 ids = 1; // repeated 表示数组
  12. Value info = 2; // 可嵌套对象
  13. map<int, Value> values = 3; // 可输出map映射
  14. }
  15. message Value {
  16. bool is_man = 1;
  17. int age = 2;
  18. }

以上是一个使用样例,包含方法定义,入参,出参,可以看出有几个明确的特点:

  1. 有明确的类型,支持的类型有多种
  2. 每个field会有名字
  3. 每个field有一个数字标号,一般按顺序排列。下文编解码会用到这个点
  4. 能表达数组,map映射等类型
  5. 通过嵌套message可以表达复杂的对象
  6. 方法,参数的定义落到一个proto文件中,依赖双方同时持有这个文件,并依此进行编码

但是proto文件并不是代码,不能执行。要想直接跨语言是不行的,需要要有对应语言的中间代码才行,中间代码需要有一下能力:

  1. 将message转换为struct, 例如golang里面的struct,需要各自语言表达后,才能被理解
  2. 需要有进行编解码的代码,能解码的内容为自己语言的对象,能将对象编码为对应的数据

这些代码手写是不行的,protobuf对此的解决方案是,提供一个统一的protoc工具,这是一个C++翻译工具,可以通过proto文件,生成某特性语言的中间代码,实现上面说的两个能力。

  1. // 依赖目录 生成golang中间代码 对应proto文件地址
  2. protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/XXX.proto
  3. protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/XXX.proto // 生成java中间代码

直观对比

为什么选择做protobuf, 而不是普及最广的json作为编码方案呢?可以做一个直观对比,以上文proto中的response为例,一次输出json的结果是:

  1. "{\"ids\":[123,456],\"info\":{\"is_man\":true,\"age\":20},\"values\":{\"110\":{\"is_man\":false,\"age\":18}}}"

所有内容被打包成了一个字符串,里面包含字段名,value,response很大时,体积消耗很大。主要浪费在三方面:

  1. 字段名,例如上面的ids, info等,如果json体积太大,则重复会更多
  2. 数字勇字符串表达了,例如123数字变成了“123”,这在编码后由一个字节变为三个字节
  3. 类型字符,如[],{}

但如果是protobuf呢?输出是一段人眼无法理解的字符串,里面

  1. 去掉了字段名,转而以字段标号替代,通过标号可以在proto中找到字段名
  2. 没有类型字符串
  3. 用二进制表达内容,不会将数字转为字符串
  4. 字段值按顺序依次排列

这使得protobuf的编码结果体积,通常是json编码后的十分之一。同时由于排列简单,其解析算法的时空复杂度远小于json,对cpu的消耗也小很多。这使得protobuf在大数据量,高频率的数据下,远胜于json,在大规模分布式RPC场景中广泛使用。

优缺点

首先,我们可以总结出pb的优点:

  1. 没有打包无用的数据,排列紧凑,体积小,利于传输
  2. 解析策略简单,序列化/反序列化速度快
  3. 能较好的兼容字段,不能解析到的会跳过

但是缺点也非常明显:

  1. 肉眼看不出value是什么,无法自描述,难以debug
  2. 需要proto文件才能知道如何解析,否则是天书,这在灵活性上不如json
  3. 其实本质上起差别在于,json是设计给别人看的,protobuf则是利于机器
  4. 适合的场景不同,各有利弊。作为工具,讨论其快,好,差是没有意义的,在合适的地方,用合适的工具才是最重要的

参考

gRPC系列(二) 如何用Protobuf组织内容