Bond——大資料時代的資料交換和儲存格式

thanks發表於2024-07-08

設想我們在一家很大的網際網路公司做IT方面的規劃、開發和維護,有以下這樣的應用場景:

  • 公司裡有若干個不同的開發團隊,開發語言有Java、.net、Python、C++....十來種,還有很多外包團隊對專案進行開發,大中小系統已經多的數不過來;並且各個團隊、系統間都需要進行海量資料的交換(比如搜尋引擎實時的資料,GPS物聯網實時資料,電站的實時監控資料等),如何定義一種資料格式,使得各種平臺和語言都能夠相容,交換的成本最低?
  • 如此多的結構化、非結構化資料都需要進行儲存,有幾百個哈杜叢集、數十萬臺、百萬臺伺服器,資料儲存在Hbase、RocksDb或者其他自己開發的資料庫結構中,對查詢的實時性很高(記憶體表個位數毫秒、SSD十幾毫秒等)。如何用一種較為統一的格式存放這些資料?

相信在大公司或者在大公司做過外包的童鞋,都接觸過這樣一種資料物件,那就是Bond格式,目前Bond由M$維護,官方網站:https://github.com/microsoft/bond/,上面提供了各種語言的示例、編譯工具等。
一個基本的Bond檔案如下所示:

namespace School

struct Student
{
    0: string Name;
    1: uint8 Age;
    2: bool IsBoy;
    3: optional vector<string> Interests;
}

這裡定義了一個學校的名稱空間,裡面有個學生類,學生類裡面有四個欄位,依次是姓名、年齡、是否為男孩、興趣愛好的列表(可選)。

很容易看出Bond結構實際是與平臺和語言無關的,它是一個DSL,在不同的平臺上,利用Bond編譯工具gbc,可以把Bond檔案編譯成不同的類,然後就可以賦值、儲存和傳輸了,編譯好的Bond原生支援RPC呼叫。

Bond支援的資料型別有:

  • 基本型別:int8, int16, int32, int64, uint8, uint16, uint32, uint64, float, double, bool, string, blob等,需要注意java平臺沒有uint型別,會編譯成帶有符號的同型別,資料會丟失精度(正數變成負數);
  • 列表 vector、字典 map
  • 列舉 enum
  • 預設值
  • 可選欄位 optional,必須欄位 required
  • 可空欄位 nullable
  • 支援類的繼承
  • 支援欄位的修飾 Attribute,這對前端驗證和資料庫儲存比較有用,能定義欄位長度、範圍、列族等

這些型別能很好的滿足資料交換和儲存的需要;除此以外,Bond是一種非常高效的資料儲存格式,它的二進位制序列化最大程度去除了後設資料的影響,極其緊湊,我們來看一個示例:

ListingItem是一個Bond型別,它的結構定義如下:

struct ListingItem
{
    1: required uint64 xxxxxxxxx; 
    2: required uint8 xxxxxxxxx; 
    3: optional uint16 score;
    4: optional vector<xxxx> xxxxxxxxxxx;
    5: optional map<xxxxxx, uint16> xxxxxxxxx; 
    6: optional xxxxxx xxxxxxxxxxx = Exxxxx;
    7: optional bool IsDeleted;
    8: optional vector<xxxxxxxx> xxxxxxxxList = nothing;
}

由於牽涉到生產環境的真實資料,所以一些欄位和引用使用xxxxx來代替了,這個類的大小中等,有各種欄位,還有對其它類的引用和集合等等。

我們用隨機化的方式生成一百萬個類,類裡面的欄位和引用都不一樣,數值都是隨機生成的,然後用Bond序列化和Java中帶的Gson序列化方式進行序列化後的二進位制長度比較,渣程式碼如下:

    @Test
    public void ListingItemTest() throws IOException {
        int cycleLength = 1000000;
        Random random = new Random();
        // Create 1000000 listing item
        List<ListingItem> items = new ArrayList<>();
        for(int i = 0; i < cycleLength; i ++){
            ListingItem item = new ListingItem();
            // ... 
            //賦值省略,利用random.nextLong() nextInt()等給欄位賦值
            // ...
            items.add(item);
        }

        StopWatch stopWatch = new StopWatch();
        int length = 0;
        stopWatch.start();
        //Serialization Bond Object for 1000000 times
        for(int i = 0; i < cycleLength; i ++){
            byte[] bytes = BondSerializationUtils.serializeBondToBytes(items.get(i), ProtocolType.MARSHALED_PROTOCOL);
            length += bytes.length;
        }
        stopWatch.stop();
        System.out.println(String.format("Bond Serialization %d objects cost %d ms, avg length in bytes is %d", cycleLength, stopWatch.getTime(), length / cycleLength));

        //Serialization as Json Object
        length = 0;
        stopWatch.reset();
        stopWatch.start();
        for(int i = 0; i < cycleLength; i ++){
            String json = gson.toJson(items.get(i));
            length += json.length();
        }
        stopWatch.stop();
        System.out.println(String.format("Json Serialization %d objects cost %d ms, avg length in string is %d", cycleLength, stopWatch.getTime(), length / cycleLength));
    }

在我的破筆記本(10代i5低功耗u)執行結果如下:

Bond Serialization 1000000 objects cost 1392 ms, avg length in bytes is 60
Json Serialization 1000000 objects cost 8837 ms, avg length in string is 310

由於Java字串getBytes()後和原長度一樣,所以我們可以把字串長度看作二進位制陣列長度。

多執行幾遍程式碼,可以看到,Bond序列化的速度比Gson序列化的速度快4到5倍,序列化後的大小也只有json的1/5。(使用不同的序列化協議,比如COMPACT_PROTOCOL可以進一步壓縮結果大小和序列化時間,速度能比Json序列化快10倍以上)

這是個了不起的成績,如果我們生產環境中每天產生上百億條資料,這些資料用於各種轉換、分析與統計,使用Bond結構儲存只有使用字串儲存空間的1/5,能夠省下4/5以EB、PB計的儲存成本;而且由於資料量的減少,傳輸和計算的成本也進一步壓縮,每年在IT基礎設施上的投入能節約上百億上千億美元,這些節省的成本最後都是利潤。

最後,由於Java平臺沒有自帶二進位制序列化框架,我們用.net自帶的序列化框架測試下二進位制序列化和Json序列化,序列化的類如下:

    [Serializable]
    public class TAListings
    {
        public string LxxxxxxxxList { get; set; }
        public string Titles { get; set; }
        public string CxxxxxxxxxxxxxxList { get; set; }
        public string CxxxxxxxxxxxxxxxxList { get; set; }
    }

程式碼如下:

TAListings listings = new TAListings() { CxxxxxxxxxxxxxxxxList= "5033333309:-:73333333333334,34444444442:-:744444444442,54444444449:-:744444444444444448,544444443:-:744444444444444" };

var binSerilization = BinaryHelper.Serialize(listings);
var jsonSerilization = JsonHelper.Serialize(listings);

Console.WriteLine(string.Join(" ", binSerilization.Select(f => f.ToString("x2"))));
Console.WriteLine("Binary Serilization Length: " + binSerilization.Length);
Console.WriteLine();
Console.WriteLine(jsonSerilization);
Console.WriteLine("Json Serilization UTF8 Length: " + Encoding.UTF8.GetByteCount(jsonSerilization));
Console.ReadLine();

結果如截圖所示:
image
可以看到,如果只是普通的類,在.net使用二進位制序列化後,反而比json序列化大了不少,增加的長度在二到四倍左右不等,這很反常識,是因為.net二進位制序列化需要儲存更多的後設資料嗎?

大家對我的文章有什麼問題和建議,都希望能夠參與討論,謝謝大家!

相關文章