常用序列化方案比較

當你感到悲哀痛苦時,最好是去學些什麼東西。學習會使你永遠立於不敗之地。

使用場景

在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資源。

測試數據

//github.com/Devskiller/jfairy.git 是用來生成測試數據的,可以支援多國語言,由於其本身不是用來測試序列化的數據集,其生成的對象也不是完全可序列化的,欄位也多,也包含了一下二級欄位,故簡化之。

簡化之後的數據結構如下:

生成測試數據程式碼如下:

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;
    }
}

測試環境

cpu memory disk
1顆cpu,8核 32g 可用 4.9g

測試方案

儘可能地重用流對象,避免新創建對象對結果的影響
儘可能地避免gc對序列化和反序列化的影響,每次序列化反序列化之後都手動gc,並且測試數據集不宜過大,目前設定最大為1kw,儘可能避免gc對結果的影響
在測試操作過程中,避免列印以及磁碟讀取存放等io操作,序列化後的數據直接放在記憶體,供反序列化使用。

測試結果

下面開始對比業內的比較認可的幾種序列化方案。

序列化方案對比結果如下:

不同數據集下各個序列化方案對比

對比結果如下:

times type serialize time(ms) de-serialize time(ms) avg size cpu佔用率(峰值) remark
10000 jdk 73 211 531 2.1
10000 kryo 71 48 144 1.6
10000 msgpack 39 66 119 1.1
10000 fst 53 49 151 2.4
10000 hession 53 113 349 2.7
10000 protoStuff 24 21 131 0.6
100000 jdk 367 1387 531 5.9
100000 kryo 116 88 144 1
100000 msgpack 129 350 119 2
100000 fst 101 104 151 0.9
100000 hession 211 374 349 2.4
100000 protoStuff 63 70 131 0.5
500000 jdk 1746 7412 531 27.2
500000 kryo 437 423 144 2.4
msgpack 414 1510 119 6.6
500000 fst 412 538 151 1.7
500000 hession 890 1768 349 6.4
500000 protoStuff 263 333 131 1
1000000 jdk 3479 14130 531 37.2
1000000 kryo 878 844 144 1.3
1000000 msgpack 864 3036 119 13.6
1000000 fst 827 993 151 3.6
1000000 hession 1688 3522 349 12.8
1000000 protoStuff 513 666 131 2.5
2500000 jdk 15558 35460 531 70.2
2500000 kryo 2151 2281 144 11.7
2500000 msgpack 2185 8170 119 21.7
2500000 fst 2014 2607 151 12.6
2500000 hession 4169 9047 349 15.1
2500000 protoStuff 1238 1777 131 8.1
5000000 jdk 41637 332540 531 100 均值大概在80%
5000000 kryo 4255 4774 144 19.8
5000000 msgpack 4603 16362 119 41.8
5000000 fst 3985 5399 151 18.2
5000000 hession 8716 18436 349 27
5000000 protoStuff 2563 3770 131 20.1

結果分析

時間角度分析

  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 數據量時最大堆記憶體對序列化和反序列化的影響。

Xmx type serialize time(ms) de-serialize time(ms) avg size cpu佔用率(峰值)
20g jdk 36283 130879 531 28.1
20g kryo 8759 9288 144 20.2
20g msgpack 9151 29653 119 38.2
20g fst 8274 10719 151 19.5
20g hession 17853 38109 349 21.2
20g protoStuff 5218 7767 131 22.5
30g jdk 37496 128481 531 22.5
30g kryo 8994 9200 144 14.3
30g msgpack 9035 29973 119 22.5
30g fst 8544 10088 151 15.5
30g hession 17366 36742 349 22.7
30g protoStuff 5217 7289 131 20.1

結果分析

當數據量在 1kw時,增大最大堆記憶體,對不能使用流復用技術的 jdk、hession影響比較大,影響為幾秒,但整體時間影響並不大,對於其他序列化方案影響在毫秒級,幾乎不影響。

分析總結

綜合考慮序列化和反序列化時間以及序列化後的大小來看,優先使用 protoStuff 、 kryo 以及 fst 。