常用序列化方案比較
- 2020 年 5 月 7 日
- 筆記
當你感到悲哀痛苦時,最好是去學些什麼東西。學習會使你永遠立於不敗之地。
使用場景
在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 |
結果分析
時間角度分析
- 由於jdk本身在序列化和反序列化時,ObjectOutputStream、ByteArrayInputStream以及ObjectInputStream不能復用,序列化時間會包含部分對象創建的時間,這會增加gc時間
- msgpack在序列化過程中,MessageUnpacker不能復用,並且需要手動創建類,反序列化時間會比序列化時間長
- hession在反序列化過程中,流不能復用,反序列化時間會比較長
- kryo、fst、protoStuff 在序列化和反序列化的過程中,很好的使用了流復用,序列化效果比較好
- 整體來看,同一種序列化方案,反序列化消耗時間會比序列化消耗時間長,多了對象的創建以及欄位映射時間
- 數量級達到百萬級後,使用protoStuff、fst以及kryo效果比較好,整體時間消耗依次為 protoStuff < kryo < fst
序列化後位元組大小分析
- 整體大小如下:msgpack < protoStuff < kryo < fst < hession < jdk
- 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 。