Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。本文简单介绍Dubbo底层数据传输模型,以Dubbo协议+Hessian2序列化协议为例,分析调用过程中,消费者和服务提供者间传输对象时底层数据的编解码细节。编解码是实现高性能服务框架的一个关键要素,本文还提出了一些可以提高效率的改进点以供参考。
Dubbo的网络传输层(Transport)依赖数据序列化层(Serialize),负责单向消息传输。
当消费者调用服务提供者的服务时,要向服务提供者传递方法名称和参数等信息,服务提供者执行后,也会将执行结果返给消费者,这些信息以二进制方式传输,两者需要统一的协议对数据进行编码和解码,而这部分逻辑就包含在网络传输层中。在远程调用过程中,依赖网络传输层的上层模型可以不关注传输细节,只处理对象即可。
Dubbo的网络传输层支持多种传输协议和对象序列化方式,主要有以下几种:
1、传输协议:Dubbo、RMI、Hessain、WebService、Http等
2、序列化方式:Hessian2、dubbo、JSON、Java原生等
Dubbo默认使用Dubbo协议, Hessian2序列化方式,适合传入传出参数数据包较小,消费者比提供者个数多的场景。
Dubbo协议采用经典定长包头+变长包体的协议设计,包头记录了数据的序列化方式,请求状态,数据长度等信息,包体是请求/响应对象序列化后的二进制数据。格式见下图:
网络传输层传输的就是这种二进制数据,基本流程如下:
用Netty作为传输框架时,图中的Client和Server分别对应NettyClient和NettyServer,在创建的时候会指定encoder和decoder完成对象的编码和解码。Dubbo协议默认DubboCountCodec为编解码器,该类的成员DubboCodec封装了Dubbo协议的编解码逻辑,同时完成对象的序列化或反序列化。
以Dubbo协议+Hessian2序列化协议+Netty为例。消费者发送请求参数时,参数会经过InternalEncoder进行编码:
服务提供者收到数据时通过InternalDecoder进行解码。InternalDecoder每次收到的数据包不一定是完整的dubbo协议包,所以InternalDecoder内部创建了一个ChannelBuffer,可以把多个数据包合并成一个完整的dubbo协议包。
InternalDecoder的解码流程如下:
DubboCodec.decode方法一次只会解析一个完整的dubbo协议包,但Netty每次收到的数据不一定是完整的dubbo协议包,也可能是多个dubbo协议包,对此InternalDecoder内部创建了一个ChannelBuffer来处理这些情况。
收到不完整的dubbo协议包
messageReceived在decode之前,会记录当前的读索引readerIndex,decode方法在解析不完整的dubbo协议包时,会返回NEED_MORE_INPUT,messageReceived收到该返回值时,就把读索引回滚到之前保存的位置,然后将待处理的数据赋给InternalDecoder的内部buffer。Netty下次收到数据包时,同样会调用messageReceived方法,此时新的数据会追加在内部buffer后面。调用decode方法时会传入合并后的数据,完成dubbo数据包的解码。收到多个dubbo协议包
messageReceived是循环调用decode方法进行解码,每次decode会处理一个dubbo协议包。当数据中有多个dubbo协议包时,messageReceived会循环解码,直到所有dubbo协议包处理完成,解码结束;或者遇到一个不完整的dubbo协议包,按第一种情况处理。
3 Hessian2序列化协议
Hessian2序列化协议 ( http://hessian.caucho.com/doc/hessian-serialization.html ) 是由caucho提供的一种开源协议。Dubbo的com.alibaba.com.caucho.hessian.io包实现了Hessian2协议。DubboCodec在生成/解析完Dubbo协议包头之后,会使用Hessian序列化/反序列化传输对象。Hessian2Output类负责对象的序列化,内部有一个byte[]缓存,序列化对象时先向缓存中写入数据,当缓存满或者序列化完成时,将数据写入到输出流中。Hessian2Input类负责对象的反序列化,内部也有一个byte[]缓存,大小为256。反序列化时,先处理缓存中的数据,如果处理完成,从输入流中复制待处理的数据到缓存中,直到反序列化完成。
在序列化后的二进制数据中,字符串二进制数据占了很大一部分,接下来分析一下Hessian协议对字符串的处理。
Hessian2中的字符都用UTF-8编码,长一点的字符串序列化时会被分割为多个块(chunk),x53 (‘S’) 开头代表终止块, x52 (‘R’) 开头代表非终止块,每个标记字节后面都跟有一个16位的无符号整数来表示块长度。这个长度是字符数,而不是编码后的二进制数组长度。
协议说明:
3.1.2 Hessian2字符串处理
Java中的char使用UTF-16编码,Hessian2协议中的字符都是UTF-8编码,序列化时需要进行一次转换。Hessian2Output中的printString方法循环遍历字符串中的每个字符,将编码后的字符写入缓存。
反序列化字符串时,Hessian2Input中的readString方法首先解析标记字节,获得字符串中的字符个数,然后循环调用parseChar处理数据,将二进制数组解码。parseChar实际调用parseUTF8Char,而parseUTF8Char就是上面printString编码逻辑的逆向操作。parseUTF8Char每次从缓存中读入一个字节,根据编码规则判断是否需要读入新字节,最后完成单个字符的解码。
Dubbo的InternalDecoder内部在解码数据包时,会把数据包复制到一个ChannelBuffer中,这个ChannelBuffer会传给HessianInput进行反序列化。HessianInput内部也维护了一个缓存,大小为256字节,反序列化时首先从缓存中读入数据, 读完后再从ChannelBuffer里复制最多256字节的数据。HessianInput默认数据来源是InputStream,增加内部缓存的本意是减少IO,提高反序列化效率。但是对ChannelBuffer来说,其内部数据已经是完整的序列化数组,HessianInput增加缓存反而导致了额外的数据复制,降低了效率。如果需要提高Dubbo反序列化的效率,可以去掉HessianInput内部的缓存,直接使用ChannelBuffer进行反序列化,省掉额外的数据复制操作。
Netty4中引入了ByteToMessageDecoder,封装了通用的数据解码流程,不同的协议实现只要继承ByteToMessageDecoder,实现自己的解析逻辑即可。ByteToMessageDecoder内部创建了一个ByteBuf类型的缓存cumulation,在解码底层数据时,ByteToMessageDecoder会循环调用子类的decode方法解析收到的数据,decode方法处理后剩余的数据会留在cumulation中,等下一次收到数据时共同处理。cumulation默认支持堆外内存,读取网络传输数据时会减少一次内存复制,提高处理性能。因此,Dubbo的InternalDecoder可以基于ByteToMessageDecoder实现,不但代码更简洁,效率也会更好。
Hessian2序列化协议还支持数据压缩,降低网络占用。用线上一个订单数据测试,内容包括数字,中英文字符。使用Java序列化后的数据为10320字节,使用Hessian2序列化后的数据为6844字节,使用Hessian2的Deflation类压缩后的数据为3915字节,压缩后的数据占普通Hessian2序列化数据的57%。但是开启压缩会明显增加序列化耗时。将同一个订单分别进行10000次Hessian2的普通序列化和压缩序列化,普通序列化平均耗时0.15ms,压缩序列化平均耗时0.33ms。如果网络传输比较慢,可以考虑使用Hessian2的压缩,结合实际场景进行优化。
Hessian2是一个比较旧的跨语言序列化协议,官网的Java实现在2013年后就没有更新了。最近几年又出现了多种序列化方式,性能优秀,包括:
Java语言:Kryo,FST等
跨语言:ProtoBuf,Thrift等
这些新序列化方式多数优于Hessian2,因此提升Dubbo的性能可以将Hessian2序列化方式替换为其他高性能序列化方式。当当开源的dubbox就引入了Kryo和FST两种序列化实现,取代默认的Hessian2,以提升Dubbo的性能。
多种序列化方式性能测试( https://dangdangdotcom.github.io/dubbox/serialization.html )
Dubbo-2.5.4 ( https://github.com/alibaba/dubbo.git )
http://dubbo.io/Protocol+Reference-zh.htm
http://hessian.caucho.com/doc/hessian-serialization.html
https://dangdangdotcom.github.io/dubbox/