当你感到悲哀痛苦时,最好是去学些什么东西。学习会使你永远立于不败之地。

使用场景

在rdd的每一个分区上,执行迭代操作,在每一次的迭代操作中,需要先访问redis缓存,并获取key对应的value,若value存在则对value进行反序列化操作,否则从db里查询并序列化存放到redis缓存中。

伪代码如下:

rdd.mapPartitions {
  iter.map{
    val value:Option[Array[Byte]] = getValueByKey(key)
    value match {
       case Some(bs) => {
                           deserilialize(bs);
                           other operations...
                         }
       case None => {
                       val newVals = fetchFromDbByKey(key);
                       other operations ....;
                       val newBs = serialize(key);
                       storeRedis(newBs)
                     }
    }
  }
}

从这段位代码可以看出,影响效率的有序列化和反序列化的效率以及序列化后byte数组的字节大小(可以影响网络IO)。

测试指标

主要从四方面来考虑,序列化总时间,反序列化总时间,序列化后平均字节大小,cpu使用率峰值。

其中,使用jconsole监控其cpu使用率峰值。

注意,cpu使用率的峰值只是一个参考,因为在数据量增大时,在序列化和反序列化过程中,伴随着gc,也会消耗cpu资源。

测试数据

https://github.com/Devskiller/jfairy.git 是用来生成测试数据的,可以支持多国语言,由于其本身不是用来测试序列化的数据集,其生成的对象也不是完全可序列化的,字段也多,也包含了一下二级字段,故简化之。

简化之后的数据结构如下:
常用序列化方案比较-LMLPHP
生成测试数据代码如下:

package com.wisers;

import com.devskiller.jfairy.Fairy;
import com.devskiller.jfairy.producer.person.Person;

import java.util.ArrayList;
import java.util.Locale;

public class DataGenerator {
    public static ArrayList<People> generatePeople(int sampleNum) {
        Fairy chineseFairy = Fairy.create(Locale.CHINESE);
        Fairy englishFairy = Fairy.create(Locale.ENGLISH);
        ArrayList<People> people = new ArrayList<People>(sampleNum);
        for (int i = 0; i < sampleNum; i++) {
            Person person = Math.random() >= 0.5 ? chineseFairy.person() : englishFairy.person();
            people.add(People.createBy(person));
        }
        return people;
    }
}

测试环境

测试方案

尽可能地重用流对象,避免新创建对象对结果的影响
尽可能地避免gc对序列化和反序列化的影响,每次序列化反序列化之后都手动gc,并且测试数据集不宜过大,目前设定最大为1kw,尽可能避免gc对结果的影响
在测试操作过程中,避免打印以及磁盘读取存放等io操作,序列化后的数据直接放在内存,供反序列化使用。

测试结果

下面开始对比业内的比较认可的几种序列化方案。

序列化方案对比结果如下:

不同数据集下各个序列化方案对比

对比结果如下:

结果分析

时间角度分析

  1. 由于jdk本身在序列化和反序列化时,ObjectOutputStream、ByteArrayInputStream以及ObjectInputStream不能复用,序列化时间会包含部分对象创建的时间,这会增加gc时间
  2. msgpack在序列化过程中,MessageUnpacker不能复用,并且需要手动创建类,反序列化时间会比序列化时间长
  3. hession在反序列化过程中,流不能复用,反序列化时间会比较长
  4. kryo、fst、protoStuff 在序列化和反序列化的过程中,很好的使用了流复用,序列化效果比较好
  5. 整体来看,同一种序列化方案,反序列化消耗时间会比序列化消耗时间长,多了对象的创建以及字段映射时间
  6. 数量级达到百万级后,使用protoStuff、fst以及kryo效果比较好,整体时间消耗依次为 protoStuff < kryo < fst

序列化后字节大小分析

  1. 整体大小如下:msgpack < protoStuff < kryo < fst < hession < jdk
  2. msgpack需要手动序列化字段,并不包含类信息,故序列化后的结果比较小

最大堆内存对序列化时间的影响

默认最大堆内存约为7.7g,由于序列化后的数据被存放在内存,不能被gc回收,数据量达到1kw 时,出现内存溢出异常,故调大堆内存,对比在1kw 数据量时最大堆内存对序列化和反序列化的影响。

结果分析

当数据量在 1kw时,增大最大堆内存,对不能使用流复用技术的 jdk、hession影响比较大,影响为几秒,但整体时间影响并不大,对于其他序列化方案影响在毫秒级,几乎不影响。

分析总结

综合考虑序列化和反序列化时间以及序列化后的大小来看,优先使用 protoStuff 、 kryo 以及 fst 。

05-07 21:02