Dubbo Hession反序列化导致CPU占用飙高用例分析

2,714 阅读6分钟

一、Dubbo简介

Dubbo是 阿里开源的分布式远程服务调用框架。

1、Dubbo协议
Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多种协议,Dubbo官网是推荐
我们使用Dubbo协议的,默认也是用的dubbo协议。

介绍几种常见的协议:
1)dubbo协议
   缺省协议,使用基于mina1.1.7+hessian3.2.1的tbremoting交互。 
   连接个数:单连接 
   连接方式:长连接 
   传输协议:TCP 
   传输方式:NIO异步传输 
   序列化:Hessian二进制序列化 
   适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供
   者,尽量不要用dubbo协议传输大文件或超大字符串 
   适用场景:常规远程服务方法调用
   1、dubbo默认采用dubbo协议,dubbo协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务
   调用,以及服务消费者机器数远大于服务提供者机器数的情况 
   2、他不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。
2)rmi协议
   Java标准的远程调用协议。 
   连接个数:多连接 
   连接方式:短连接 
   传输协议:TCP 
   传输方式:同步传输 
   序列化:Java标准二进制序列化 
   适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。 
   适用场景:常规远程服务方法调用,与原生RMI服务互操作
   RMI协议采用JDK标准的java.rmi.*实现,采用阻塞式短连接和JDK标准序列化方式。

3)hessian协议
   基于Hessian的远程调用协议。 
   连接个数:多连接 
   连接方式:短连接 
   传输协议:HTTP 
   传输方式:同步传输 
   序列化:表单序列化 
   适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入
   参数,暂不支持传文件。 
   适用场景:需同时给应用程序和浏览器JS使用的服务。
   1、Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo
   缺省内嵌Jetty作为服务器实现。 
   2、Hessian是Caucho开源的一个RPC框架:http://hessian.caucho.com,其通讯效率高于
   WebService和Java自带的序列化。 
4)http 协议
   基于http表单的远程调用协议。参见:[HTTP协议使用说明] 
   连接个数:多连接 
   连接方式:短连接 
   传输协议:HTTP 
   传输方式:同步传输 
   序列化:表单序列化 
   适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL
   传入参数,暂不支持传文件。 
   适用场景:需同时给应用程序和浏览器JS使用的服务。
5)webservice协议
   基于WebService的远程调用协议。 
   连接个数:多连接 
   连接方式:短连接 
   传输协议:HTTP 
   传输方式:同步传输 
   序列化:SOAP文本序列化 
   适用场景:系统集成,跨语言调用

二、序列化

序列化是将一个对象变成一个二进制流就是序列化,反序列化是将二进制流转换成对象。

为什么要序列化?
1. 减小内存空间和网络传输的带宽
2. 分布式的可扩展性
3. 通用性,接口可共用。

Dubbo序列化支持java、compactedjava、nativejava、fastjson、dubbo、fst、hessian2、kryo,
其中默认hessian2。其中java、compactedjava、nativejava属于原生java的序列化。

1)dubbo序列化
   阿里尚未开发成熟的高效java序列化实现,阿里不建议在生产环境使用它。
2)hessian2序列化
   hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的,
   它是dubboRPC默认启用的序列化方式。前提是被序列化的类得实现Serializable接口。
3)json序列化
   目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,但其实
   现都不是特别成熟,而且json这种文本序列化性能一般不如上面两种二进制序列化。
4)java序列化
   主要是采用JDK自带的Java序列化实现,性能很不理想。

dubbo序列化主要由Serialization(序列化策略)、
DataInput(反序列化,二进制->对象)、
DataOutput(序列化,对象->二进制流)来进行数据的序列化与反序列化。

hessian 是一个比较老的序列化实现了,而且它是跨语言的,所以不是单独针对java进行优化的。
而dubbo RPC实际上完全是一种Java to Java的远程调用,其实没有必要采用跨语言的序列化方式
(当然肯定也不排斥跨语言的序列化)。
现在有一些新的序列化:
专门针对Java语言的:Kryo,FST等等
跨语言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack等等
这些序列化方式的性能多数都显著优于 hessian2 (甚至包括尚未成熟的dubbo序列化)。
所以我们可以为 dubbo 引入 Kryo 和 FST 这两种高效 Java 来优化 dubbo 的序列化。

三、案例分析

电商网站搞大促,整点秒杀活动,吸引了大量用户,瞬间流量暴增,商品详情系统报警,通过监控看,
所有依赖的RPC服务调用RT都很长,监控系统CPU,已经满负荷运转。

诊断步骤:
1、通过监控页面查看基本的指标,
   CPU 接近100%
   内存正常
   系统RT很长
   系统load很高
   网络正常
   服务方RT正常。
2、登服务器
   top 命令 查看系统实时指标
   tail 命令,查询 error,warn日志
   jstack 命令 dump 线程查看线程堆栈信息

发现大量nio线程调用getDeserializer,至此疑点集中在:hessian反序列化后抛异常。根据以上异常栈,查看hessian-lite源码 (hessian-lite 3.2.4以下版本),代码解析如下:

可看出:

    如果远端与本地jar版本不一致的情况,会造成反射加载类异常(ClassNotFound),同时会打印日志信息、
    异常toString;而这几个动作(反射、抛异常、异常日志等)都是cpu高消耗行为;
    又因为类加载失败,下次接口调用时,这些逻辑都会重新执行一遍,所以在整点活动的高并发场景下,
    将会造成严重cpu性能损耗;符合以上各阶段验证结果

3、问题原因

  就是服务端在对象上增加新的类型,客户端没有升级对应的版本,导致客户端使用dubbo调用服务端,hessian
  序列化时,出现ClassNotFound的异常,不断的出发上述源码里的try{}catch{}里的 反射加载类,并抛
  异常,这些都是cpu消费的大户。

4、问题解决

   1、更换序列化方式
      可更换成如kryo, fastjson等其他序列化方式,以上几种不存在此类性能缺陷,且总体性能较优
      风险:可能会带来其他兼容性问题,更换、验证成本并不低
   2、升级hessian-lite
      升级dubbo 至 2.6.6,因为这版本已将hessian-lite升级为3.2.5
      这个问题在 hessian-lite 3.2.5 已经fixed:
      https://github.com/apache/dubbo/issues/351
      hessian-lite 3.2.5 解决方式是将每个class的异常都缓存住,相关代码如下(红框内):