Kryo 和 FST 序列化
目录
- 序列化谈话
- 启用 Kryo 和 FST
- 注册要序列化的类
- 无参数构造函数和 Serializable 接口
- 序列化性能分析和测试
- 测试环境
- 测试脚本
- 比较 Dubbo RPC 中不同序列化生成的字节大小
- 比较 Dubbo RPC 中不同序列化的响应时间和吞吐量
- 未来
序列化谈话
dubbo RPC 是 dubbo 系统中核心高性能、高吞吐量的远程调用方式,我喜欢称之为多路复用 TCP 长连接调用。简单来说
- 长连接:避免每次都需要创建新的 TCP 连接,提高调用响应速度
- 多路复用:单个 TCP 连接可以交替传输多个请求和响应消息,减少连接的等待空闲时间,从而在相同并发数下减少网络连接数量,提高系统吞吐量。
dubbo RPC 主要用于两个 dubbo 系统之间的远程调用,特别适合高并发、小数据量的互联网场景。
序列化在远程调用的响应速度、吞吐量和网络带宽消耗方面也起着至关重要的作用,是我们提高分布式系统性能的最关键因素之一。
在 dubbo RPC 中,同时支持多种序列化方式,例如
- Dubbo 序列化:阿里还没有开发出成熟高效的 java 序列化实现,阿里不建议在生产环境中使用它
- Hessian2 序列化:Hessian 是一种跨语言高效的二进制序列化方式。但这里实际上不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite,是 dubbo RPC 默认启用的序列化方式
- JSON 序列化:目前有两种实现,一种是使用阿里的 fastjson 库,另一种是使用 dubbo 实现的 simple json 库,但实现并不特别成熟,json 的文本序列序列化性能普遍不如上面两种二进制序列化。
- Java 序列化:主要通过使用 JDK 自带的 Java 序列化实现,性能并不理想。
总的来说,四种主要序列化方式的性能从上到下依次降低。对于追求高性能远程调用的 dubbo RPC 来说,实际上只有 1 和 2 两种高效的序列化方式更适合,而第一种 dubbo 序列化还不太成熟,所以实际上只有 2 可用。所以 dubbo RPC 默认使用 hessian2 序列化。
但 hessian 是一个比较老的序列化实现,而且是跨语言的,所以没有针对 java 单独优化。实际上,dubbo RPC 是从 Java 到 Java 的远程调用。实际上,没有必要采用跨语言序列化(当然,不排除跨语言序列化)。
近年来,各种高效的序列化方式层出不穷,不断刷新着序列化性能的上限,最典型的包括
- 专门针对 Java 语言:Kryo、FST 等
- 跨语言:Protostuff、ProtoBuf、Thrift、Avro、MsgPack 等
这些序列化方式的性能大多明显优于 hessian2(甚至包括不成熟的 dubbo 序列化)。
鉴于此,我们为 dubbo 引入了两种高效的 Java 序列化实现,Kryo 和 FST,逐步替换 hessian2。
其中,Kryo 是一个非常成熟的序列化实现,已经在 Twitter、Groupon、Yahoo 和许多著名的开源项目(如 Hive 和 Storm)中得到广泛应用。而 FST 是一个比较新的序列化实现,目前还没有足够成熟的应用案例,但我认为它仍然很有前景。
在生产环境应用中,我建议目前优先选择 Kryo。
启用 Kryo 和 FST
使用 Kryo 和 FST 非常简单,只需先添加相应的依赖项:更多插件:Dubbo SPI 扩展
<dependency>
<groupId>org.apache.dubbo.extensions</groupId>
<artifactId>dubbo-serialization-kryo</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo.extensions</groupId>
<artifactId>dubbo-serialization-fst</artifactId>
<version>1.0.0</version>
</dependency>
然后在 dubbo RPC 的 XML 配置中添加一个属性
<dubbo:protocol name="dubbo" serialization="kryo"/>
<dubbo:protocol name="dubbo" serialization="fst"/>
注册要序列化的类
为了使 Kryo 和 FST 在 Dubbo 系统中充分发挥高性能,最好注册需要序列化的类。例如,我们可以实现以下回调接口
public class SerializationOptimizerImpl implements SerializationOptimizer {
public Collection<Class> getSerializableClasses() {
List<Class> classes = new LinkedList<Class>();
classes.add(BidRequest.class);
classes. add(BidResponse. class);
classes. add(Device. class);
classes. add(Geo. class);
classes. add(Impression. class);
classes.add(SeatBid.class);
return classes;
}
}
然后在 XML 配置中添加
<dubbo:protocol name="dubbo" serialization="kryo" optimizer="org.apache.dubbo.demo.SerializationOptimizerImpl"/>
注册这些类后,序列化性能可能会大大提高,特别是对于少量嵌套对象。
当然,在序列化一个类时,可能会有很多类级联,例如 Java 集合类。针对这种情况,我们已经自动注册了 JDK 中的常用类,因此您无需重复注册(当然,重复注册也没有效果),包括
Gregorian Calendar
InvocationHandler
BigDecimal
BigInteger
pattern
BitSet
URIs
UUID
HashMap
ArrayList
LinkedList
HashSet
TreeSet
Hashtable
date
Calendar
ConcurrentHashMap
SimpleDateFormat
vector
BitSet
StringBuffer
String Builder
object
Object[]
String[]
byte[]
char[]
int[]
float[]
double[]
由于注册要序列化的类只是为了性能优化,所以即使忘记注册某些类也没有关系。事实上,即使不注册任何类,Kryo 和 FST 的性能通常也优于 hessian 和 dubbo 序列化。
当然,有人可能会问为什么不使用配置文件来注册这些类?这是因为通常需要注册大量的类,导致配置文件过长;而且没有良好的 IDE 支持,编写和重构配置文件比编写 Java 类麻烦得多;最后,这些注册的类通常在项目编译打包后不需要进行动态修改。
此外,有些人也会认为手动注册序列化类是一项比较繁琐的工作,能否用注解标记,然后系统自动发现并注册。但注解在这里的局限性在于,它只能用来标记可以修改的类,而序列化中引用的很多类很可能是你无法修改的(比如第三方库或 JDK 系统类或其他项目的类)。此外,添加注解毕竟会稍微“污染”代码,使应用程序代码对框架的依赖性稍微高一些。
除了注解之外,我们还可以考虑其他自动注册序列化类的方法,比如扫描类路径,自动发现实现 Serializable 接口的类(甚至包括 Externalizable)并注册它们。当然,我们知道类路径上可能有很多 Serializable 类,所以我们也可以考虑使用包前缀来一定程度上限制扫描范围。
当然,在自动注册机制中,尤其需要考虑如何确保服务提供者和消费者以相同的顺序(或 ID)注册类,以避免错位。毕竟,两端可以发现和注册的类数量可能不同。
无参数构造函数和 Serializable 接口
如果要序列化的类不包含无参构造函数,Kryo 序列化的性能会大大下降,因为此时我们会使用 Java 的序列化在底层透明地替换 Kryo 的序列化。因此,建议尽可能为每个序列化类添加一个无参构造函数(当然,如果一个 Java 类没有自定义构造函数,它默认会拥有一个无参构造函数)。
此外,Kryo 和 FST 不需要序列化类实现 Serializable 接口,但我们仍然建议每个序列化类都实现它,因为这可以保持与 Java 序列化和 dubbo 序列化的兼容性。此外,它也使我们将来有可能采用上述一些自动注册机制。
序列化性能分析和测试
本文主要讨论序列化,但在进行性能分析和测试时,我们并没有单独处理每种序列化方法,而是将它们放在 Dubbo RPC 中进行比较,因为这更贴近实际情况。
测试环境
大致如下
- 两台独立服务器
- 四核 Intel(R) Xeon(R) CPU E5-2603 0 @ 1.80GHz
- 8G 内存
- 虚拟机之间的网络通过 100M 交换机
- CentOS 5
- JDK 7
- Tomcat 7
- JVM 参数 -server -Xms1g -Xmx1g -XX:PermSize=64M -XX:+UseConcMarkSweepGC
当然,测试环境有限,所以目前的测试结果可能不太权威和具有代表性。
测试脚本
保持与 Dubbo 自身基准测试的接近性
10 个并发客户端持续发出请求
- 传入一个嵌套的复杂对象(但单个数据量很小),不做任何处理,原样返回
- 传入 50K 个字符串,不做任何处理,原样返回(TODO:结果尚未列出)
运行 5 分钟的性能测试。(引用 Dubbo 自身的测试考虑:“主要考察序列化和网络 IO 的性能,因此服务器没有业务逻辑。采用 10 个并发的原因是考虑到 rpc 协议在高并发下可能存在较高的 CPU 使用率,从而导致瓶颈。”)
比较 Dubbo RPC 中不同序列化生成的字节大小
序列化生成的字节数的大小是一个相对确定的指标,它决定了远程调用的网络传输时间和带宽占用。
复杂对象的测试结果如下(数字越小越好)
序列化实现 | 请求字节 | 响应字节 |
---|---|---|
Kryo | 272 | 90 |
FST | 288 | 96 |
Dubbo 序列化 | 430 | 186 |
Hessian | 546 | 329 |
FastJson | 461 | 218 |
Json | 657 | 409 |
Java 序列化 | 963 | 630 |
Dubbo RPC 中不同序列化响应时间和吞吐量的比较
远程调用方法 | 平均响应时间 | 平均 TPS(每秒事务数) |
---|---|---|
REST: Jetty + JSON | 7.806 | 1280 |
REST: Jetty + JSON + GZIP | TODO | TODO |
REST: Jetty + XML | TODO | TODO |
REST: Jetty + XML + GZIP | TODO | TODO |
REST: Tomcat + JSON | 2.082 | 4796 |
REST: Netty + JSON | 2.182 | 4576 |
Dubbo: FST | 1.211 | 8244 |
Dubbo: kyro | 1.182 | 8444 |
Dubbo: dubbo 序列化 | 1.43 | 6982 |
Dubbo: hessian2 | 1.49 | 6701 |
Dubbo: fastjson | 1.572 | 6352 |
测试总结
就目前的结果来看,我们可以看到 Kryo 和 FST 与 Dubbo RPC 中的原始序列化方法相比有了显著的改进,无论是在生成的字节数大小、平均响应时间还是平均 TPS 方面。
未来
将来,当 Kryo 或 FST 在 Dubbo 中足够成熟时,我们可能会将 Dubbo RPC 的默认序列化从 hessian2 更改为其中之一。