protobuf為什么那么快

在分布式應用或者微服務架構中,各個服務之間通常使用json或者xml結構數據進行通信,通常情況下,是沒什么問題的,但是在高性能和大數據通信的系統當中,如果有辦法可以壓縮數據量,提高傳輸效率,顯然會給用戶帶來更快更流暢的體驗。

google公司就通過使用一種新的數據交換格式辦到了這點,新的數據交換的格式叫做protobuf

protobuf有多屌呢,可以看一下下面的官方測試報告:

解包耗時
數據包壓縮后大小

可以看到,一條消息數據,用protobuf序列化后的大小是json的10分之一,是xml格式的20分之一,但是性能卻是它們的5~100倍,我覺得用戶一定會尖叫的:oh my god!

把數據變小一點

下面我們以json數據為基礎出發,通過一步一步的對它進行優化,來理解protobuf的實現原理。

對于一條信息,json的表示方式為:

{ "age": 30, "name": "zhangsan",  "height": 175.33, "weight": 140 }

顯然,中間有很多冗余的字符,比如{"等,為了把數據變小一點,我們可以暴力一點,直接表示為:

30 zhangsan 175.33 140

通過直接將value拼在了一起,舍去了不必要的冗余字符,我們大幅度的壓縮了空間,但是會有一些問題,就是當我們將這段數據發送給接收端,接收端怎么知道每個value對應哪個key呢?比如zhangsan這個值,對應的是age還是name呢?

比較好的方式是事先跟接收端約定好有哪些字段,順序是啥樣子的,然后接收端按照順序對應起來:

字段1:age 字段2:name 字段3: height 字段4:weight
30 zhangsan 175.33 140

很完美,這樣我們確實壓縮了不少數據,棒棒的。

能不能更小一點

假設height這個字段為null,我們其實是不必要傳遞這個字段的,這個時候我們需要傳遞的數據就為:

30 zhangsan 140

但是在接收端,解析數據并按照順序進行字段匹配的時候就會出問題:

字段1:age 字段2:name 字段3: height 字段4:weight
30 zhangsan 140

顯然已經亂套了,為了保證能夠正確的配對,我們可以使用tag技術:

tag|30 tag|zhangsan tag|175.33 tag|140

也就是說,每個字段我們都用tag|value的方式來存儲的,在tag當中記錄兩種信息,一個是value對應的字段的編號,另一個是value的數據類型(比如是整形還是字符串等),因為tag中有字段編號信息,所以即使沒有傳遞height字段的value值,根據編號也能正確的配對。

Tag的開銷

有的同學會問,使用tag的話,會增加額外的空間,這跟jsonkey/value有什么區別嗎?

這個問題問的好,json中的key是字符串,每個字符就會占據一個字節,所以像name這個key就會占據4個字節,但在protobuf中,tag使用二進制進行存儲,一般只會占據一個字節,它的代碼為:

static int makeTag(final int fieldNumber, final int wireType) {
  return (fieldNumber << 3) | wireType;
}

fieldNumber表示后面的value所對應的字段的編號是多少,比如fieldNumber為1,就表示age,如果為2,就表示name等;wireType表示value的數據類型,以此來計算value占用字節的大小。

protobuf當中,wireType可以支持的字段類型如下:

因為tag一般占用一個字節,開銷還算是比較小的,所以protobuf整體的存儲空間占用還是相對小了很多的。

能不能更小點

在實際的傳輸過程中,會傳遞整數,我們知道整數在計算機當中占據4個字節,但是絕大部分的整數,比如價格,庫存等,都是比較小的整數,實際用不了4個字節,像127這種數,在計算機中的二進制是:
00000000 00000000 00000000 01111111(4字節32位)
完全可以用最后1個字節來進行存儲,protobuf當中定義了Varint這種數據類型,可以以不同的長度來存儲整數,將數據進一步的進行了壓縮。

但是這里面也有一個問題,在計算機當中的負數是用補碼表示的,對于-1,它的二進制表示方式為:
11111111 11111111 11111111 11111111(4字節32位)
顯然無法用1個字節來表示了,但-1確實是一個比較簡單的數,這個時候就可以使用zigzag算法來對負數進行進一步的壓縮,最終我們可以使用2個字節來表示-1。

要快

雖然數據現在很小了,但是解析速度還是有很大的提升空間的,因為每個字段都是用tag|value來表示的,在tag中含有value的數據類型的信息,而不同的數據類型有不同的大小,比如如果valuebool型,我們就知道肯定占了一個字節,程序從tag后面直接讀一個字節就可以解析出value,非常快,而json則需要進行字符串解析才可以辦到。

能不能更快一點

如果value是字符串類型的,具體value有多長,我們無法從tag當中了解到,但是如果不知道value的長度,我們就不得不做字符串匹配操作,要知道字符串匹配是非常耗時的。
為了能夠快速解析字符串類型的數據,protobuf在存儲的時候,做了特殊的處理,分成了三部分:tag|leg|value,其中的leg記錄了字符串的長度,同樣使用了varint來存儲,一般一個字節就能搞定,然后程序從leg后截取leg個字節的數據作為value,解析速度非常快。

protobuf能幫助我們干什么?

了解額protobuf的牛逼之處,對我們來說有什么好處呢?

首先,對于我們系統當中的一些大數據傳輸,顯然用protobuf是可以獲得很大的改善的,如果你這么干了,領導一定會想給你漲工資的。

第二,給我們優化數據傳輸提供了一種思路,通過提供更多的數據元數據(數據類型,長度等),我們可以大幅度提高解析數據,比如在nodejs當中就有一個框架叫fastify,通過給json設計了schema來提供更快的解析速度,具體的實現原理大家可以點擊 這里 看。

第三:未知中...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。