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二进制程序)
这边是一个简单的proto报文生成过程


timg.jpg

大家如果之前用过web service的话,应该会有似曾相识的感觉。这个proto文件,我理解为类似于SOAP的wsdl描述文件,即是一份用来描述数据结构的标准文档说明,各语言的sdk可根据该语言的proto文件生成标准的类文件(是不是想起来了wsdl2java),从而达到跨语言的远程调用(RPC调用)。然而实际使用中,基于这种模式使用还是比较麻烦,如果对象多了要写一堆proto定义文件,另外生成出来的java对象可读性也比较差,和平时用的pojo有很大不同。本文介绍一种更加方便的使用protobuf的方法,就是protostuff。利用这个框架,可以跳过编写proto文件的步骤,直接生成protobuf格式的报文,接收端也可以直接使用该框架将二进制反序列化为Object对象,用到的就是我们平时使用的普通的java对象。利用Protostuff-Runtime模块可以不需要静态编译protoc,只要在runtime的时候传入schema就可以了。下面就来实操一下
首先引入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格式,这两个是文本形式的序列化框架,以及hessian2,这个是老牌的二进制序列化框架,也是dubbo里默认的序列化方案。几种格式都可以做到跨语言,现在我们来看看他们的性能差距到底有多少。
一些基本情况说明:
测试环境:macbook pro 笔记本(2012 late,core i5 2.5GHz,8G内存)
java版本:jdk1.8
protobuf序列化框架:protostuff
xml序列化框架:jdk原生jaxb
json序列化框架:fastjson
hessian序列化框架:hessian
测试方法:将List<Role>循环多次,模拟不同的报文size大小,以此测试序列化框架的性能。

  1. size=10
protobuf序列化字节长度:167
protobuf反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
protobuf序列化耗时:196

xml序列化字节长度:645
xml反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
xml序列化耗时:149

json序列化字节长度:350
json反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
json序列化耗时:223

hessian2序列化字节长度:690
hessian2反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
hessian2序列化耗时:63

可以看出在数据量很小的情况,几个框架的性能差别不大,hessian2最好,xml甚至比protobuf还要高一些。但是有一个地方需要注意,就是序列化后的字节长度,json是xml的大概1/2多一点,而protobuf只有xml的1/4,而hessian甚至比xml还要长。这在存储敏感或者带宽敏感的场景下是至关重要的。之后的所有测试存储所占空间的比例基本都是一样。

  1. size=1000
protobuf序列化字节长度:15919
protobuf反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
protobuf序列化耗时:211

xml序列化字节长度:53027
xml反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
xml序列化耗时:256

json序列化字节长度:29962
json反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
json序列化耗时:249

hessian2序列化字节长度:60992
hessian2反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
hessian2序列化耗时:114

当数据量来到1000这个量级,仍然是hessian一马当先,可以看到protobuf已经反超xml了,这时候json的性能也已经超过xml,xml的劣势开始渐渐显现。

  1. size=100000
protobuf序列化字节长度:1788921
protobuf反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
protobuf序列化耗时:333

xml序列化字节长度:5489029
xml反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
xml序列化耗时:1059

json序列化字节长度:3188964
json反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
json序列化耗时:726

hessian2序列化字节长度:6288994
hessian2反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
hessian2序列化耗时:785

来到10万这个量级,protobuf反超!而且优势非常明显,几乎是xml性能的3倍,hessian的2倍,序列化后的字节大小为1.7M左右,xml的1/4

  1. size=1000000
protobuf序列化字节长度:18888922
protobuf反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
protobuf序列化耗时:2405

xml序列化字节长度:55889030
xml反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
xml序列化耗时:4087

json序列化字节长度:32888965
json反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
json序列化耗时:3063

hessian2序列化字节长度:63888995
hessian2反序列化结果:name=张三,id=HO123,age=300,role1=R1,role2=R2
hessian2序列化耗时:4747

数据量为100万条,此时protobuf的报文数据大小为18M,xml已经达到了55M,json也有33M,hessian更是到了64M,protobuf的速度仍然是xml的差不多两倍。这里比较诡异的是json在好几次测试中耗时只有1000多ms,但多测几次又会变回3000多ms,从生成的字节码来看,3000ms是比较合理的结果,不知道是做了什么样子的优化。

结论

在报文数据量很小的情况,几种格式差别不是很大,建议都可以选择。如果对于报文解析性能要求很高,报文体积小的情况下可以选择hessian,体积大选protobuf。如果需要文本格式,选择json。如果对于存储或者带宽敏感,建议选择protobuf,体积比其他几种格式小太多,传输或者存储都很方便。

补充说明

看了protobuf的官方文档,感觉protobuf的性能还有提高的空间,就用原生的protobuf测试了一下,静态编译了user.proto文件,测试结果如下

数据量大小:10
原生protobuf序列化字节长度:167
原生protobuf序列化耗时:54

数据量大小:1000
原生protobuf序列化字节长度:15919
原生protobuf序列化耗时:86

数据量大小:100000
原生protobuf序列化字节长度:1788921
原生protobuf序列化耗时:289

数据量大小:1000000
原生protobuf序列化字节长度:18888922
原生protobuf序列化耗时:2371

可以看出性能有了很大的提高,在各阶段的测试中都是最快的。对比编码后的字节数,和protostuff是完全一致的,说明两种框架最终出来的结果是一样的,但是原生的protobuf要快了很多,看来果然鱼和熊掌不可兼得,运行时的编译对于性能相比静态编译还是有相当的损耗的。在小于1000的数据量级,两者的性能差距差不多有3倍。对于极致追求性能的场景,还是建议使用原生的protobuf。当然本次测试结果还没有达到官方宣称的比xml要快20~100倍这个数量级,这个估计需要C++或者go的环境下才能实现,留给其他同学进行测试了。

测试用到的github代码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容