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

【RPC 專欄】深入理解RPC之序列化篇 —— Kryo

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

技術文章第一時間送達!

原始碼精品專欄

 

一年前,筆者剛剛接觸RPC框架,從單體式應用向分散式應用的變革無疑是讓人興奮的,同時也對RPC背後到底做了哪些工作產生了興趣,但其底層的設計對新手而言並不是很友好,其涉及的一些常用技術點都有一定的門檻。如傳輸層常常使用的netty,之前完全沒聽過,想要學習它,需要掌握前置知識點nio;協議層,包括了很多自定義的協議,而每個RPC框架的實現都有差異;代理層的動態代理技術,如jdk動態代理,雖然實戰經驗不多,但至少還算會用,而cglib則又有一個盲區;序列化層倒還算是眾多層次中相對簡單的一環,但RPC為了追求可擴充套件性,效能等諸多因素,通常會支援多種序列化方式以供使用者插拔使用,一些常用的序列化方案hessian,kryo,Protobuf又得熟知…

這個系列打算就RPC框架涉及到的一些知識點進行探討,本篇先從序列化層的一種選擇–kryo開始進行介紹。

序列化概述

大白話介紹下RPC中序列化的概念,可以簡單理解為物件–>位元組的過程,同理,反序列化則是相反的過程。為什麼需要序列化?因為網路傳輸只認位元組。所以互信的過程依賴於序列化。有人會問,FastJson轉換成字串算不算序列化?物件持久化到資料庫算不算序列化?沒必要較真,廣義上理解即可。

JDK序列化

可能你沒用過kryo,沒用過hessian,但你一定用過jdk序列化。我最早接觸jdk序列化,是在大二的JAVA大作業中,《XX管理系統》需要把物件儲存到檔案中(那時還沒學資料庫),jdk原生支援的序列化方式用起來也很方便。

  1. class Student implements Serializable{  

  2.   private String name;  

  3. }  

  4. class Main{

  5.   public static void main(String[] args) throws Exception{  

  6.      // create a Student

  7.      Student st = new Student("kirito");  

  8.     // serialize the st to student.db file  

  9.     ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.db"));  

  10.     oos.writeObject(st);  

  11.     oos.close();  

  12.     // deserialize the object from student.db

  13.     ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.db"));  

  14.     Student kirito = (Student) ois.readObject();  

  15.     ois.close();  

  16.    // assert

  17.    assert "kirito".equals(kirito.getName());  

  18.   }  

  19. }  

Student物體類需要實現Serializable介面,以告知其可被序列化。

序列化協議的選擇通常有下列一些常用的指標:

  1. 通用性。是否只能用於java間序列化/反序列化,是否跨語言,跨平臺。

  2. 效能。分為空間開銷和時間開銷。序列化後的資料一般用於儲存或網路傳輸,其大小是很重要的一個引數;解析的時間也影響了序列化協議的選擇,如今的系統都在追求極致的效能。

  3. 可擴充套件性。系統升級不可避免,某一物體的屬性變更,會不會導致反序列化異常,也應該納入序列化協議的考量範圍。

  4. 易用性。API使用是否複雜,會影響開發效率。

容易用的模型通常效能不好,效能好的模型通常用起來都比較麻煩。顯然,JDK序列化屬於前者。我們不過多介紹它,直接引入今天的主角kryo作為它的替代品。

Kryo入門

引入依賴

  1.    

  2.        com.esotericsoftware

  •        kryo

  •        4.0.1

  •    

  • 由於其底層依賴於ASM技術,與Spring等框架可能會發生ASM依賴的版本衝突(檔案中表示這個衝突還挺容易出現)所以提供了另外一個依賴以供解決此問題

    1.    

    2.        com.esotericsoftware

  •        kryo-shaded

  •        4.0.1

  •    

  • 快速入門

    1. class Student implements Serializable{  

    2.   private String name;  

    3. }  

    4. public class Main {

    5.    public static void main(String[] args) throws Exception{

    6.        Kryo kryo = new Kryo();

    7.        Output output = new Output(new FileOutputStream("student.db"));

    8.        Student kirito = new Student("kirito");

    9.        kryo.writeObject(output, kirito);

    10.        output.close();

    11.        Input input = new Input(new FileInputStream("student.db"));

    12.        Student kiritoBak = kryo.readObject(input, Student.class);

    13.        input.close();

    14.        assert "kirito".equals(kiritoBak.getName());

    15.    }

    16. }

    不需要註釋也能理解它的執行流程,和jdk序列化差距並不是很大。

    三種讀寫方式

    Kryo共支援三種讀寫方式

    1. 如果知道class位元組碼,並且物件不為空

    1.    kryo.writeObject(output, someObject);

    2.    // ...

    3.    SomeClass someObject = kryo.readObject(input, SomeClass.class);

    快速入門中的序列化/反序列化的方式便是這一種。而Kryo考慮到someObject可能為null,也會導致傳回的結果為null,所以提供了第二套讀寫方式。

    1. 如果知道class位元組碼,並且物件可能為空

    1.    kryo.writeObjectOrNull(output, someObject);

    2.    // ...

    3.    SomeClass someObject = kryo.readObjectOrNull(input, SomeClass.class);

    但這兩種方法似乎都不能滿足我們的需求,在RPC呼叫中,序列化和反序列化分佈在不同的端點,物件的型別確定,我們不想依賴於手動指定引數,最好是…emmmmm…將位元組碼的資訊直接存放到序列化結果中,在反序列化時自行讀取位元組碼資訊。Kryo考慮到了這一點,於是提供了第三種方式。

    1. 如果實現類的位元組碼未知,並且物件可能為null

    1.    kryo.writeClassAndObject(output, object);

    2.    // ...

    3.    Object object = kryo.readClassAndObject(input);

    4.    if (object instanceof SomeClass) {

    5.       // ...

    6.    }

    我們犧牲了一些空間一些效能去存放位元組碼資訊,但這種方式是我們在RPC中應當使用的方式。

    我們關心的問題

    繼續介紹Kryo特性之前,不妨讓我們先思考一下,一個序列化工具或者一個序列化協議,應當需要考慮哪些問題。比如,支援哪些型別的序列化?迴圈取用會不會出現問題?在某個類增刪欄位之後反序列化會報錯嗎?等等等等….

    帶著我們考慮到的這些疑惑,以及我們暫時沒考慮到的,但Kryo幫我們考慮到的,來看看Kryo到底支援哪些特性。

    支援的序列化型別

    boolean Boolean byte Byte char
    Character short Short int Integer
    long Long float Float double
    Double byte[] String BigInteger BigDecimal
    Collection Date Collections.emptyList Collections.singleton Map
    StringBuilder TreeMap Collections.emptyMap Collections.emptySet KryoSerializable
    StringBuffer Class Collections.singletonList Collections.singletonMap Currency
    Calendar TimeZone Enum EnumSet

    表格中支援的型別一覽無餘,這都是其預設支援的。

    1.    Kryo kryo = new Kryo();

    2.    kryo.addDefaultSerializer(SomeClass.class, SomeSerializer.class);

    這樣的方式,也可以為一個Kryo實體擴充套件序列化器。

    總體而言,Kryo支援以下的型別:

    • 列舉

    • 集合、陣列

    • 子類/多型

    • 迴圈取用

    • 內部類

    • 泛型

    但需要註意的是,Kryo不支援Bean中增刪欄位。如果使用Kryo序列化了一個類,存入了Redis,對類進行了修改,會導致反序列化的異常。

    另外需要註意的一點是使用反射建立的一些類序列化的支援。如使用Arrays.asList();建立的List物件,會引起序列化異常。

    1. Exception in thread "main" com.esotericsoftware.kryo.KryoException: Class cannot be created (missing no-arg constructor): java.util.Arrays$ArrayList

    但new ArrayList()建立的List物件則不會,使用時需要註意,可以使用第三方庫對Kryo進行序列化型別的擴充套件。如https://github.com/magro/kryo-serializers所提供的。

    不支援不包含無參建構式類的反序列化,嘗試反序列化一個不包含無參建構式的類將會得到以下的異常:

    1. Exception in thread "main" com.esotericsoftware.kryo.KryoException: Class cannot be created (missing no-arg constructor): moe.cnkirito.Xxx

    保證每個類具有無參建構式是應當遵守的程式設計規範,但實際開發中一些第三庫的相關類不包含無參構造,的確是有點麻煩。

    執行緒安全

    Kryo是執行緒不安全的,意味著每當需要序列化和反序列化時都需要實體化一次,或者藉助ThreadLocal來維護以保證其執行緒安全。

    1. private static final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {

    2.    protected Kryo initialValue() {

    3.        Kryo kryo = new Kryo();

    4.        // configure kryo instance, customize settings

    5.        return kryo;

    6.    };

    7. };

    8. // Somewhere else, use Kryo

    9. Kryo k = kryos.get();

    10. ...

    Kryo相關配置引數詳解

    每個Kryo實體都可以擁有兩個配置引數,這值得被拉出來單獨聊一聊。

    1. kryo.setRegistrationRequired(false);//關閉註冊行為

    2. kryo.setReferences(true);//支援迴圈取用

    Kryo支援對註冊行為,如 kryo.register(SomeClazz.class);,這會賦予該Class一個從0開始的編號,但Kryo使用註冊行為最大的問題在於,其不保證同一個Class每一次註冊的號碼想用,這與註冊的順序有關,也就意味著在不同的機器、同一個機器重啟前後都有可能擁有不同的編號,這會導致序列化產生問題,所以在分散式專案中,一般關閉註冊行為。

    第二個註意點在於迴圈取用,Kryo為了追求高效能,可以關閉迴圈取用的支援。不過我並不認為關閉它是一件好的選擇,大多數情況下,請保持 kryo.setReferences(true)

    常用Kryo工具類

    1. public class KryoSerializer {

    2.    public byte[] serialize(Object obj) {

    3.        Kryo kryo = kryoLocal.get();

    4.        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

    5.        Output output = new Output(byteArrayOutputStream);//<1>

    6.        kryo.writeClassAndObject(output, obj);//<2>

    7.        output.close();

    8.        return byteArrayOutputStream.toByteArray();

    9.    }

    10.    public <T> T deserialize(byte[] bytes) {

    11.        Kryo kryo = kryoLocal.get();

    12.        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

    13.        Input input = new Input(byteArrayInputStream);// <1>

    14.        input.close();

    15.        return (T) kryo.readClassAndObject(input);//<2>

    16.    }

    17.    private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {//<3>

    18.        @Override

    19.        protected Kryo initialValue() {

    20.            Kryo kryo = new Kryo();

    21.            kryo.setReferences(true);//預設值為true,強調作用

    22.            kryo.setRegistrationRequired(false);//預設值為false,強調作用

    23.            return kryo;

    24.        }

    25.    };

    26. }

    <1> Kryo的Input和Output接收一個InputStream和OutputStream,Kryo通常完成位元組陣列和物件的轉換,所以常用的輸入輸出流實現為ByteArrayInputStream/ByteArrayOutputStream。

    <2> writeClassAndObject和readClassAndObject配對使用在分散式場景下是最常見的,序列化時將位元組碼存入序列化結果中,便可以在反序列化時不必要傳入位元組碼資訊。

    <3> 使用ThreadLocal維護Kryo實體,這樣減少了每次使用都實體化一次Kryo的開銷又可以保證其執行緒安全。

    參考文章

    https://github.com/EsotericSoftware/kryo

    Kryo 使用指南

    序列化與反序列化


    更多的序列化方案,和RPC其他層次中會涉及到的技術,在後續的文章中進行逐步介紹。


    贊(0)

    分享創造快樂

    © 2024 知識星球   網站地圖