設想我們在一家很大的網際網路公司做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();
結果如截圖所示:
可以看到,如果只是普通的類,在.net使用二進位制序列化後,反而比json序列化大了不少,增加的長度在二到四倍左右不等,這很反常識,是因為.net二進位制序列化需要儲存更多的後設資料嗎?
大家對我的文章有什麼問題和建議,都希望能夠參與討論,謝謝大家!