歡迎光臨
每天分享高質量文章

【RPC 專欄】深入理解RPC之序列化篇–總結篇

點選上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 

上一篇《深入理解RPC之序列化篇–Kryo》,介紹了序列化的基礎概念,並且詳細介紹了Kryo的一系列特性,在這一篇中,簡略的介紹其他常用的序列化器,並對它們進行一些比較。序列化篇僅僅由Kryo篇和總結篇構成可能有點突兀,等待後續有時間會補充詳細的探討。

定義抽象介面

public interface Serialization {
  byte[] serialize(Object obj) throws IOException;
  T deserialize(byte[] bytes, Class clz) throws IOException;
}

RPC框架中的序列化實現自然是種類多樣,但它們必須遵循統一的規範,於是我們使用 Serialization 作為序列化的統一介面,無論何種方案都需要實現該介面。

Kryo實現

Kryo篇已經給出了實現程式碼。

public class KryoSerialization implements Serialization {
   @Override
   public byte[] serialize(Object obj) {
       Kryo kryo = kryoLocal.get();
       ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
       Output output = new Output(byteArrayOutputStream);
       kryo.writeObject(output, obj);
       output.close();
       return byteArrayOutputStream.toByteArray();
   }
   @Override
   public T deserialize(byte[] bytes, Class clz) {
       Kryo kryo = kryoLocal.get();
       ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
       Input input = new Input(byteArrayInputStream);
       input.close();
       return (T) kryo.readObject(input, clz);
   }
   private static final ThreadLocal kryoLocal = new ThreadLocal() {
       @Override
       protected Kryo initialValue() {
           Kryo kryo = new Kryo();
           kryo.setReferences(true);
           kryo.setRegistrationRequired(false);
           return kryo;
       }
   };
}

所需依賴:

<dependency>
   <groupId>com.esotericsoftwaregroupId>


   <artifactId>kryoartifactId>
   <version>4.0.1version>
dependency>

Hessian實現

public class Hessian2Serialization implements Serialization {
   @Override
   public byte[] serialize(Object data) throws IOException {
       ByteArrayOutputStream bos = new ByteArrayOutputStream();
       Hessian2Output out = new Hessian2Output(bos);
       out.writeObject(data);
       out.flush();
       return bos.toByteArray();
   }
   @Override
   public T deserialize(byte[] bytes, Class clz) throws IOException {
       Hessian2Input input = new Hessian2Input(new ByteArrayInputStream(bytes));
       return (T) input.readObject(clz);
   }
}

所需依賴:

<dependency>
   <groupId>com.cauchogroupId>


   <artifactId>hessianartifactId>
   <version>4.0.51version>
dependency>

大名鼎鼎的 Hessian 序列化方案經常被RPC框架用來作為預設的序列化方案,可見其必然具備一定的優勢。其具體的優劣我們放到文末的總結對比中與其他序列化方案一起討論。而在此,著重提一點Hessian使用時的坑點。

BigDecimal的反序列化

使用 Hessian 序列化包含 BigDecimal 欄位的物件時會導致其值一直為0,不註意這個bug會導致很大的問題,在最新的4.0.51版本仍然可以復現。解決方案也很簡單,指定 BigDecimal 的序列化器即可,透過新增兩個檔案解決這個bug:

resources\META-INF\hessian\serializers

java.math.BigDecimal=com.caucho.hessian.io.StringValueSerializer

resources\META-INF\hessian\deserializers

java.math.BigDecimal=com.caucho.hessian.io.BigDecimalDeserializer

Protostuff實現

public class ProtostuffSerialization implements Serialization {
   @Override
   public byte[] serialize(Object obj) throws IOException {
       Class clz = obj.getClass();
       LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
       try {
           Schema schema = RuntimeSchema.createFrom(clz);
           return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
       } catch (Exception e) {
           throw e;
       } finally {
           buffer.clear();
       }
   }
   @Override
   public T deserialize(byte[] bytes, Class clz) throws IOException {
       T message = objenesis.newInstance(clz); // <1>
       Schema schema = RuntimeSchema.createFrom(clz);
       ProtostuffIOUtil.mergeFrom(bytes, message, schema);
       return message;
   }
   private Objenesis objenesis = new ObjenesisStd(); // <2>
}

所需依賴:


<dependency>
   <groupId>com.dyuproject.protostuffgroupId>


   <artifactId>protostuff-coreartifactId>
   <version>1.0.9version>
dependency>
<dependency>
   <groupId>com.dyuproject.protostuffgroupId>
   <artifactId>protostuff-runtimeartifactId>
   <version>1.0.9version>
dependency>

<dependency>
   <groupId>org.objenesisgroupId>
   <artifactId>objenesisartifactId>
   <version>2.5version>
dependency>

Protostuff 可以理解為 google protobuf 序列化的升級版本,protostuff-runtime 無需靜態編譯,這比較適合RPC通訊時的特性,很少見到有人直接拿 protobuf 作為RPC的序列化器,而 protostuff-runtime 仍然佔據一席之地。

<1> 使用 Protostuff 的一個坑點在於其反序列化時需使用者自己實體化序列化後的物件,所以才有了 T message = objenesis.newInstance(clz); 這行程式碼。使用 objenesis 工具實體化一個需要的物件,而後使用 ProtostuffIOUtil 完成賦值操作。

<2> 上述的 objenesis.newInstance(clz) 可以由 clz.newInstance() 代替,後者也可以實體化一個物件,但如果物件缺少無參建構式,則會報錯。藉助於objenesis 可以繞開無參建構式實體化一個物件,且效能優於直接反射建立。所以一般在選擇 Protostuff 作為序列化器時,一般配合 objenesis 使用。

Fastjson實現

public class FastJsonSerialization implements Serialization {
   static final String charsetName = "UTF-8";
   @Override
   public byte[] serialize(Object data) throws IOException {
       SerializeWriter out = new SerializeWriter();
       JSONSerializer serializer = new JSONSerializer(out);
       serializer.config(SerializerFeature.WriteEnumUsingToString, true);//<1>
       serializer.config(SerializerFeature.WriteClassName, true);//<1>
       serializer.write(data);
       return out.toBytes(charsetName);
   }
   @Override
   public T deserialize(byte[] data, Class clz) throws IOException {
       return JSON.parseObject(new String(data), clz);
   }
}

所需依賴:

<dependency>
   <groupId>com.alibabagroupId>


   <artifactId>fastjsonartifactId>
   <version>1.2.28version>
dependency>

<1> JSON序列化註意對列舉型別的特殊處理;額外補充類名可以在反序列化時獲得更豐富的資訊。

序列化對比

在我的PC上對上述序列化方案進行測試:

測試用例:對一個簡單POJO物件序列化/反序列化100W次

serialize/ms deserialize/ms
Fastjson 2832 2242
Kryo 2975 1987
Hessian 4598 3631
Protostuff 2944 2541

測試用例:序列化包含1000個簡單物件的List,迴圈1000次

serialize/ms deserialize/ms
Fastjson 2551 2821
Kryo 1951 1342
Hessian 1828 2213
Protostuff 1409 2813

對於耗時型別的測試需要做到預熱+平均值等條件,測試後效果其實並不如人意,從我不太嚴謹的測試來看,並不能明顯地區分出他們的效能。另外,Kryo關閉Reference可以加速,Protostuff支援靜態編譯加速,Schema快取等特性,每個序列化方案都有自身的特殊性,啟用這些特性會伴隨一些限制。但在RPC實際地序列化使用中不會利用到這些特性,所以在測試時並沒有特別關照它們。

序列化包含1000個簡單物件的List,檢視位元組數

位元組數/byte
Fastjson 120157
Kryo 39134
Hessian 86166
Protostuff 86084

位元組數這個指標還是很直觀的,Kryo擁有絕對的優勢,只有Hessian,Protostuff的一半,而Fastjson作為一個文字型別的序列化方案,自然無法和位元組型別的序列化方案比較。而位元組最終將用於網路傳輸,是RPC框架非常在意的一個效能點。

綜合評價

經過個人測試,以及一些官方的測試結果,我覺得在 RPC 場景下,序列化的速度並不是一個很大考量標準,因為各個序列化方案都在有意最佳化速度,只要不是 jdk 序列化,速度就不會太慢。

Kryo:專為 JAVA 定製的序列化協議,序列化後位元組數少,利於網路傳輸。但不支援跨語言(或支援的代價比較大)。dubbox 擴充套件中支援了 kryo 序列化協議。github 3018 star。

Hessian:支援跨語言,序列化後位元組數適中,API 易用。是國內主流 rpc 框架:dubbo,motan 的預設序列化協議。hessian.caucho.com 未託管在github

Protostuff:提起 Protostuff 不得不說到 Protobuf。Protobuf可能更出名一些,因為其是google的親兒子,grpc框架便是使用protobuf作為序列化協議,雖然protobuf與語言無關平臺無關,但需要使用特定的語法編寫 .prpto 檔案,然後靜態編譯,這帶了一些複雜性。而 protostuff 實際是對 protobuf 的擴充套件,protostuff-runtime 模組繼承了protobuf 效能,且不需要預編譯檔案,但與此同時,也失去了跨語言的特性。所以 protostuff 的定位是一個 JAVA 序列化框架,其效能略優於 Hessian。tip :protostuff 反序列化時需使用者自己初始化序列化後的物件,其只負責將該物件進行賦值。github 719 star。

Fastjson:作為一個 json 工具,被拉到 RPC 的序列化方案中似乎有點不妥,但 motan 該 RPC 框架除了支援 hessian 之外,還支援了 fastjson 的序列化。可以將其作為一個跨語言序列化的簡易實現方案。github 11.8k star。


贊(0)

分享創造快樂