protobuf協議介紹及效能實測
protobuf是谷歌開源的一款高效能序列化框架,特點是效能優異,資料結構設計優秀並具有良好的可擴充套件性,並且配合官方的java、python、go、c++的sdk,可以輕鬆做到跨語言。本文給出protobuf協議的簡單介紹以及與其他框架對比的效能測試結果。
協議簡介
Protocol Buffers 是一種輕便高效的結構化資料儲存格式,可以用於結構化資料序列化,或者說序列化。它很適合做資料儲存或資料交換格式。可用於通訊協議、資料儲存等領域的語言無關、平臺無關、可擴充套件的序列化結構資料格式。你可以理解為另外一種形式的xml,當然protobuf為了追求效能,可讀性沒有xml或者json那麼好,換來的是編碼後的報文容量大大縮小以及序列化速度的提高。
要使用使用protobuf,首先需要定義一個.proto格式的檔案,格式類似下面這樣
syntax = "proto3"; message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; }
目前有v2和v3兩種版本,api會略有不同。
修飾符:
- required : 不可以增加或刪除的欄位,必須初始化;
- optional :可選欄位,可刪除,可以不初始化;
- repeated : 可重複欄位, 對應到java檔案裡,生成的是List。
更多介紹可參考官網。
執行
protoc ./message.proto --java_out=./
可在當前目錄下生成對應對應語言的物件描述程式碼,這邊對應的是java的class檔案,再把檔案拷貝到專案工程目錄內,就可以使用了。(需要下載對應語言的protoc二進位制程式)
大家如果之前用過web service的話,應該會有似曾相識的感覺。這個proto檔案,我理解為類似於SOAP的wsdl描述檔案,即是一份用來描述資料結構的標準文件說明,各語言的sdk可根據該語言的proto檔案生成標準的類檔案(是不是想起來了wsdl2java),從而達到跨語言的遠端呼叫(RPC呼叫)。然而實際使用中,基於這種模式使用還是比較麻煩,如果物件多了要寫一堆proto定義檔案,另外生成出來的java物件可讀性也比較差,和平時用的pojo有很大不同。本文介紹一種更加方便的使用protobuf的方法,就是protostuff。利用這個框架,可以跳過編寫proto檔案的步驟,直接生成protobuf格式的報文,接收端也可以直接使用該框架將二進位制反序列化為Object物件,用到的就是我們平時使用的普通的java物件。下面就來實操一下
首先引入maven依賴
<dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.6.0</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.6.0</version> </dependency>
編寫java物件,這邊為了測試寫了一個比較複雜的屬性帶一個迴圈列表的物件
package com.bocsh.proto; import java.util.List; public class User { private String id; private String name; private Integer age; private String desc; private List<Role> roleList; //setter getter略.. @Override public String toString() { return "name=" + name + ",id=" + id + ",age=" + age + ",role1=" + roleList.get(0).getId() + ",role2=" + roleList.get(1).getId(); } }
package com.bocsh.proto; public class Role { private String id; private String name; private String desc; //setter getter略.. }
編寫測試類進行測試
public class ProtoBufUtilTest { public static void main(String[] args) throws Exception { User user = new User(); user.setAge(300); user.setDesc("備註"); user.setName("張三"); user.setId("HO123"); List<Role> list = new ArrayList<Role>(); for(int i=1;i<=2;i++) { Role role = new Role(); role.setId("R" + Integer.toString(i)); role.setName("經辦"); list.add(role); } user.setRoleList(list); //protobuf序列化 long proto1 = (new Date()).getTime(); LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); Schema<User> schema = RuntimeSchema.getSchema(User.class); ProtobufIOUtil.toByteArray(user, schema, buffer); byte[] serializerResult = ProtoBufUtil.serializer(user); System.out.println("protobuf序列化二進位制:" + bytes2hex(serializerResult)); System.out.println("protobuf序列化ascii:" + new String(serializerResult)); System.out.println("protobuf序列化位元組長度:" + serializerResult.length); User protouser = new User(); ProtobufIOUtil.mergeFrom(serializerResult, protouser, schema); long proto2 = (new Date()).getTime(); System.out.println("protobuf反序列化結果:" + protouser); System.out.println("protobuf序列化耗時:" + (proto2 - proto1)); } }
執行後結果如下
protobuf序列化二進位制:0A 05 48 4F 31 32 33 12 06 E5 BC A0 E4 B8 89 18 AC 02 22 06 E5 A4 87 E6 B3 A8 2A 0C 0A 02 52 31 12 06 E7 BB 8F E5 8A 9E 2A 0C 0A 02 52 32 12 06 E7 BB 8F E5 8A 9E protobuf序列化ascii: �HO123��張三���"�備註* �R1��經辦* �R2��經辦 protobuf序列化位元組長度:54 protobuf反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 protobuf序列化耗時:185
protobuf報文在實際傳輸中是以二進位制方式傳輸的,這裡為了方便分析,我把它專成了ascii字元。可以看到,protobuf的壓縮效率非常高,除了基本的欄位內容之外,其他的標籤之類的全部都壓縮了,以二進位制方式儲存,所以它的報文格式會比xml小非常多,當然代價就是可讀性沒有那麼友好,這也是為什麼谷歌要定義proto檔案的原因。在沒有這個檔案的情況下,你幾乎不可能看懂這個報文所表示的資料結構。
效能測試
說了那麼多,現在來看看實際protobuf能為我們帶來多少傳輸報文的效能提升。這裡我選取了目前日常使用最普遍的XML以及json格式,來看看他們的效能差距到底有多少。
一些基本情況說明:
測試環境:macbook pro筆記本(8G記憶體)
java版本:jdk1.8
protobuf序列化框架:protostuff
xml序列化框架:jdk原生jaxb
json序列化框架:fastjson
測試方法:將List<Role>迴圈多次,模擬不同的報文size大小,以此測試序列化框架的效能。
- size=10
protobuf序列化位元組長度:167 protobuf反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 protobuf序列化耗時:185 xml序列化位元組長度:645 xml反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 xml序列化耗時:135 json序列化位元組長度:350 json反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 json序列化耗時:209
可以看出在資料量很小的情況,幾個框架的效能差別不大,甚至xml的效能還要略高一些。但是有一個地方需要注意,就是序列化後的位元組長度,json是xml的大概1/2多一點,而protobuf只有xml的1/4,這在儲存敏感的場景下是至關重要的。之後的所有測試儲存所佔空間的比例基本都是一樣。
- size=1000
protobuf序列化位元組長度:15919 protobuf反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 protobuf序列化耗時:225 xml序列化位元組長度:53027 xml反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 xml序列化耗時:266 json序列化位元組長度:29962 json反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 json序列化耗時:252
當資料量來到1000這個量級,可以看到protobuf已經反超xml了,這時候json的效能也已經超過xml,畢竟資料量擺在那裡,不快不科學。
- size=100000
protobuf序列化位元組長度:1788921 protobuf反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 protobuf序列化耗時:384 xml序列化位元組長度:5489029 xml反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 xml序列化耗時:1054 json序列化位元組長度:3188964 json反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 json序列化耗時:645
來到10萬這個量級,可以看到protobuf的優勢非常明顯了,處理速度是xml的2.5倍,序列化後的位元組大小為1.7M左右,xml的1/4
- size=1000000
protobuf序列化位元組長度:18888922 protobuf反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 protobuf序列化耗時:2317 xml序列化位元組長度:55889030 xml反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 xml序列化耗時:4256 json序列化位元組長度:32888965 json反序列化結果:name=張三,id=HO123,age=300,role1=R1,role2=R2 json序列化耗時:3033
資料量為100萬條,此時protobuf的報文資料大小為18M,xml已經達到了55M,json也有33M,protobuf的速度仍然是xml的差不多兩倍。
結論
在報文資料量很小的情況,幾種格式差別不是很大,建議都可以選擇。如果請求量很大,佔用大量頻寬,或者單個報文的體積很大,以及有持久化的需求,建議使用protobuf,會有比較明顯的提升。