RPC序列化

为什么需要序列化?

平时编程时通常使用“对象”来进行数据

public class User {
    private Long id;
    private String name;
    private Integer age;
}

当需要对数据进行存储或者传输时,“对象”就不这么好用了,往往需要把数据转化成连续空间的“二进制字节流”。

一些典型的场景是:
(1) 数据库索引的磁盘存储:数据库的索引在内存里是b+树,但这个格式是不能够直接存储到磁盘上的,所以需要把b+树转化为连续空间的二进制字节流,才能存储到磁盘上;
(2) 缓存的KV存储:redis/memcache是KV类型的缓存,缓存存储的value必须是连续空间的二进制字节流,不能是对象;
(3) 数据的网络传输:socket发送的数据必须是连续空间的二进制字节流,不能是对象;

序列化(Serialization),就是将”对象”形态的数据转化为”连续空间二进制字节流”形态数据的过程。

有哪些常用的序列化?

文本类序列化方式:JSON、xml、csv
二进制类序列化方式:Hessian、Protobuf、thrift、Avro、

文本类序列化方式 优点就是可读性好,构造方便,调试也简单。不过缺点也明显,传输体积大,性能差。

实际上任何一种序列化框架,核心思想就是设计一种序列化协议,将对象的类型、属性类型、属性值一一按照固定的格式写到二进制字节流中来完成序列化,再按照固定的格式一一读出对象的类型、属性类型、属性值,通过这些信息重新创建出一个新的对象,来完成反序列化

文本类

JSON

二进制类序列化方式

Hessian

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。
Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。

Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持,比如:
Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复;
Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
Byte/Short 反序列化的时候变成 Integer。

Protobuf

Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。

Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类

优点是:
序列化后体积相比 JSON、Hessian 小很多;
IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器;
序列化反序列化速度很快,不需要通过反射获取类型;
消息格式升级和兼容性不错,可以做到向后兼容。

thrift

thrift TProtocol

序列化协议要考虑什么因素?

安全性、通用性、兼容性、性能、效率、空间开销

不管使用成熟协议xml/json,还是自定义二进制协议来序列化对象,序列化协议设计时都需要考虑以下这些因素。
(1)解析效率:这个应该是序列化协议应该首要考虑的因素,像xml/json解析起来比较耗时,需要解析doom树,二进制自定义协议解析起来效率就很高;
(2)压缩率,传输有效性:同样一个对象,xml/json传输起来有大量的xml标签,信息有效性低,二进制自定义协议占用的空间相对来说就小多了;
(3)扩展性与兼容性:是否能够方便的增加字段,增加字段后旧版客户端是否需要强制升级,都是需要考虑的问题,xml/json和上面的二进制协议都能够方便的扩展;
(4)可读性与可调试性:这个很好理解,xml/json的可读性就比二进制协议好很多;
(5)跨语言:上面的两个协议都是跨语言的,有些序列化协议是与开发语言紧密相关的,例如dubbo的序列化协议就只能支持Java的RPC调用;
(6)通用性:xml/json非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,上面自定义的二进制协议虽然能够跨语言,但每个语言都要写一个简易的协议客户端;

RPC 框架在使用时要注意哪些问题?

对象构造得过于复杂

属性很多,并且存在多层的嵌套,比如 A 对象关联 B 对象,B 对象又聚合 C 对象,C 对象又关联聚合很多其他对象,对象依赖关系过于复杂。序列化框架在序列化与反序列化对象时,对象越复杂就越浪费性能,消耗 CPU,这会严重影响 RPC 框架整体的性能;另外,对象越复杂,在序列化与反序列化的过程中,出现问题的概率就越高。

对象过于庞大

我经常遇到业务过来咨询,为啥他们的 RPC 请求经常超时,排查后发现他们的入参对象非常得大,比如为一个大 List 或者大 Map,序列化之后字节长度达到了上兆字节。这种情况同样会严重地浪费了性能、CPU,并且序列化一个如此大的对象是很耗费时间的,这肯定会直接影响到请求的耗时。

使用序列化框架不支持的类作为入参类

比如 Hessian 框架,他天然是不支持 LinkedHashMap、LinkedHashSet 等,而且大多数情况下最好不要使用第三方集合类,如 Guava 中的集合类,很多开源的序列化框架都是优先支持编程语言原生的对象。因此如果入参是集合类,应尽量选用原生的、最为常用的集合类,如 HashMap、ArrayList。

对象有复杂的继承关系

大多数序列化框架在序列化对象时都会将对象的属性一一进行序列化,当有继承关系时,会不停地寻找父类,遍历属性。就像问题 1 一样,对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。

参考资料

[1] RPC 实战与核心原理 - 03 | 序列化:对象怎么在网络中传输?
[2] 必须知道的RPC内核细节(值得收藏)!!!
[3] 消息队列高手课 - 12 | 序列化与反序列化:如何通过网络传输结构化的数据?