本文適合有 Java 基礎知識的人群
作者:HelloGitHub-Salieri
HelloGitHub 推出的《講解開源專案》系列。
專案地址:
序列化與反序列化一直是分散式程式設計中無法繞開的話題。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 公眾號