Java 序列化界新貴 kryo 和熟悉的“老大哥”,就是 PowerJob 的序列化方案


本文適合有 Java 基礎知識的人群

作者:HelloGitHub-Salieri

HelloGitHub 推出的《講解開源項目》系列。

項目地址:

https://github.com/KFCFans/PowerJob

序列化與反序列化一直是分布式編程中無法繞開的話題。PowerJob 作為一個完全意義上的分布式系統,自然少不了節點通訊時不可避免的序列化問題。由於 PowerJob 定位是中間件,出於對性能的追求,在序列化上自然也是花費了不少時間去雕琢。以下是整個過程中的一些經驗與分享,希望對大家有所幫助。

一、序列化界新貴:kryo

kryo 作為目前最快的序列化框架,自然受到了我的青睞。在 PowerJob 中,kryo 是內置默認的序列化框架。下面為大家介紹 kryo 的用法。

1.1 基礎用法

對於序列化框架來說,API 其實都差不多,畢竟入參和出參都定義好了(一個是需要序列化的對象,一個是序列化后的結果,比如字節數組)。下面簡單介紹下 kryo 的基礎用法,由於序列化和反序列化類似,以下使用序列化來作為演示。

Kryo kryo = new Kryo();
try (Output opt = new Output(1024, -1)) {
    kryo.writeClassAndObject(opt, obj);
    opt.flush();
    return opt.getBuffer();
}

代碼很簡單,首先需要創建兩個對象:Kryo 和 Output。其中,Kryo 是序列化主角,負責完成實際的序列化/反序列化工作。而 Output 則是 kryo 框架封裝的流對象,用於存儲序列化后的二進制數據。當兩個對象都准備完畢后,調用 kryo.writeClassAndObject(opt, obj) 方法即可完成對象的序列化,最后調用 Output 流對象的 getBuffer() 方法獲取序列化結果,也就是二進制數組。

1.2 線程不安全

相信大家都用過 fastjson,初次接觸 fastjson 肯定會被它簡單的 API 所吸引,常用的序列化/反序列化統統一行代碼搞定,比如 JSON.toJSONString()。通常來說,這種通過靜態方法暴露的 API,其背后的設計與實現都是線程安全的,也就是在多線程環境中,你可以安心的使用 fastjson 的靜態方法進行序列化和反序列化,那么 kryo 可以嗎?

從上述代碼不難看出,不可以~否則,人家為什么要多次一舉讓你創建對象提高使用成本呢?

王進喜同志說過,沒有條件就創造條件。既然 kryo 官方不提供靜態方法讓我們簡單使用,那就自己封裝一個吧~

拋開性能因素,封裝一個工具類非常簡單,畢竟我們的目標是解決 kryo 的並發安全問題,而當沒有任何共享資源時,是不存在任何並發安全問題的。那么我們只需要在剛剛的實例代碼上,套上一個靜態方法,就完成了最簡單的kryo 工具類封裝,代碼示例如下:

public static byte[] serialize(Object obj) {
    Kryo kryo = new Kryo();
    try (Output opt = new Output(1024, -1)) {
        kryo.writeClassAndObject(opt, obj);
        opt.flush();
        return opt.getBuffer();
    }
}

安全問題是解決了,但...事情往往不會那么簡單。這種模式下,每一次調用都會重復創建 2 個新對象(Kryo 和 Output),這在高並發下會產生一筆不小的開銷。為了獲取性能的提升,自然要考慮到對象的復用問題。對象的復用常用解決方案有兩個,分別是對象池和 ThreadLocal,下面分別進行介紹。

1.3 對象池

在編程中,“池”這個名詞相信大家一定不陌生。線程池、連接池已經是並發編程中不可避免的一部分。“池”重復利用了復用的思想,將創建完后的對象通過某個容器保存起來反復使用,從而達到提升性能的作用。Kryo 對象池原理上便是如此。Kryo 框架自帶了對象池的實現,因此使用非常簡單,不外乎創建池、從池中獲取對象、歸還對象三步,以下為代碼實例。

首先,創建 Kryo 對象池,通過重寫 Pool 接口的 create 方法,便可創建出自定義配置的對象池。

private static final Pool<Kryo> kryoPool = new Pool<Kryo>(true, false, 512) {
    @Override
    protected Kryo create() {
        Kryo kryo = new Kryo();
        // 關閉序列化注冊,會導致性能些許下降,但在分布式環境中,注冊類生成ID不一致會導致錯誤
        kryo.setRegistrationRequired(false);
        // 支持循環引用,也會導致性能些許下降 T_T
        kryo.setReferences(true);
        return kryo;
    }
};

當需要使用 kryo 時,調用 kryoPool.obtain() 方法即可,使用完畢后再調用 kryoPool.free(kryo) 歸還對象,就完成了一次完整的租賃使用。

public static byte[] serialize(Object obj) {
    Kryo kryo = kryoPool.obtain();
    // 使用 Output 對象池會導致序列化重復的錯誤(getBuffer返回了Output對象的buffer引用)
    try (Output opt = new Output(1024, -1)) {
        kryo.writeClassAndObject(opt, obj);
        opt.flush();
        return opt.getBuffer();
    }finally {
        kryoPool.free(kryo);
    }
}

對象池技術是所有並發安全方案中性能最好的,只要對象池大小評估得當,就能在占用極小內存空間的情況下完美解決並發安全問題。這也是 PowerJob 誕生初期使用的方案,直到...PowerJob 正式推出容器功能后,才不得不放棄該完美方案。

在容器模式下,使用 kryo 對象池計算會有什么問題呢?這里簡單給大家提一下,至於看不看得懂,就要看各位造化了~

PowerJob 容器功能指的是動態加載外部代碼進行執行,為了進行隔離,PowerJob 會使用單獨的類加載器完成容器中類的加載。因此,每一個 powerjob-worker 中存在着多個類加載器,分別是系統類加載器(負責項目的加載)和每個容器自己的類加載器(加載容器類)。序列化工具類自然是 powerjob-worker 的一部分,隨 powerjob-worker 的啟動而被創建。當 kryo 對象池被創建時,其使用的類加載器是系統類加載器。因此,當需要序列化/反序列化容器中的類時,kryo 並不能從自己的類加載器中獲取相關的類信息,妥妥的拋出 ClassNotFoundError!

因此,PowerJob 在引入容器技術后,只能退而求其次,采取了第二種並發安全方法:ThreadLocal。

1.4 ThreadLocal

ThreadLocal 是一種典型的犧牲空間來換取並發安全的方式,它會為每個線程都單獨創建本線程專用的 kryo 對象。對於每條線程的每個 kryo 對象來說,都是順序執行的,因此天然避免了並發安全問題。創建方法如下:

private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
    Kryo kryo = new Kryo();
    // 支持對象循環引用(否則會棧溢出),會導致性能些許下降 T_T
    kryo.setReferences(true); //默認值就是 true,添加此行的目的是為了提醒維護者,不要改變這個配置
    // 關閉序列化注冊,會導致性能些許下降,但在分布式環境中,注冊類生成ID不一致會導致錯誤
    kryo.setRegistrationRequired(false);
    // 設置類加載器為線程上下文類加載器(如果Processor來源於容器,必須使用容器的類加載器,否則妥妥的CNF)
    kryo.setClassLoader(Thread.currentThread().getContextClassLoader());
    return kryo;
});

之后,僅需要通過 *kryoLocal*.get() 方法從線程上下文中取出對象即可使用,也算是一種簡單好用的方案。(雖然理論性能比對象池差不少)

二、老牌框架:Jackson

大名鼎鼎的 Jackson 相信大家都聽說過,也是很多項目的御用 JSON 序列化/反序列化框架。在 PowerJob 中,本着不重復造輪子的原則,在 akka 通訊層,使用了 jackson-cbor 作為默認的序列化框架。

“什么,你問我為什么不用性能更好且已經在項目中集成了的 kryo?”

“那當然是因為 akka 官方沒有提供 kryo 的官方實現,於是......”

如果使用 kryo,則需要自己實現一大堆編解碼器,儼然有點寫 netty 的味道...而 jackson-cbor 呢?只需要一點小小的配置就能搞定~

actor {
    provider = remote
    allow-java-serialization = off
    serialization-bindings {
        "com.github.kfcfans.powerjob.common.OmsSerializable" = jackson-cbor
    }
  }

雖然絕對性能可能不及 kryo,但對比於自帶的 Java 序列化方式,性能已經提升 10 倍以上,在絕大部分場景都不會是性能瓶頸。所以~又有什么理由拒絕它呢~

三、最后

好了,這就是本文的全部內容了。下篇文章將會為大家帶來 PowerJob 的獨一無二分布式計算功能背后的原理分析,如此重磅的文章作為本專欄的壓軸好戲也是再恰當不過了~

那么,我們下期再見嘍~

『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。歡迎開源項目作者聯系我(微信:xueweihan,備注:講解)加入我們,讓更多人愛上、貢獻開源~


關注 HelloGitHub 公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM