一次失败的protobuf基准测试.
之前看到pemelo资料的时候,作者曾提到对pemolo的一次优化,json的完全序列化成为服务器消息转发的一个瓶颈. 目前我的游戏服务器中使用了protocbuf作为协议, 它的表现很稳定.让客户端和我对于协议的交流很流畅,极大的提高了两边的开发效率. 看起来很完美.但我总隐隐的觉得不安. 我有时候会怀疑:
目前的protobuf的方式会不会有问题.会不会让服务器的对协议对象的序列化和反序列化成为服务器的性能瓶颈.
由于我们游戏的消息类型比较多,大概有100种请求,为了方便对消息的序列/反序列化的操作.一般会使用一个大消息 包裹 子消息的方式来组织:
根据protobuf的官方文档描述了一种union type的组织形式来实现(https://developers.google.com/protocol-buffers/docs/techniques):
message OneMessage { enum Type { FOO = 1; BAR = 2; BAZ = 3; } required Type type = 1; optional Foo foo = 2; optional Bar bar = 3; optional Baz baz = 4; }
而在google group对此需求的讨论中,有人提出了内嵌消息字节流的方式:
message OneMessage { enum Type { FOO = 1; BAR = 2; BAZ = 3; } required Type type = 1; required bytes innerMessage = 2; }
我最终采用了后一个方法.因为我觉得前者会有三个缺点:
1. 大消息体过于冗长,对于一个子消息类型可能有几百种的项目来说,很难维护
2. 消息中的可选消息字段过多.但都为null,每个被序列化后的消息会占用很多无用的引用内存.而消息对象的数量又恰恰是数量最为庞大.可能会有隐患.
3. 内嵌对象不是每次都需要序列化. 服务器保存了对话的状态,每种状态只能发送类型的报文.如果客户端改善的报文类型和服务器状态不对应.则可以直接丢弃报文.无必要序列化内部消息.
就这样,项目稳定的运行了一段时间,但偶然的一次debug, 发现protbuf的内部代码对内部消息的序列存在一个性能隐患:
像protobuf中对字节的定义: required bytes innerMessage = 2;
Probobuf在序列化的时候,都会转化为不变的ByteString对象,但在生成ByteString对象时, 会对原始的消息字节数组进行一次拷贝,再传递给序列化后的对象(参见CodedInputStream.readBytes()).
虽然protbuf对字节数组的拷贝使用是System.arraycopy(native的 memcopy).但从功能上来说,这样的拷贝对于我来说,完全没有必要(google groups有人同样对此产生了怀疑 https://groups.google.com/forum/?fromgroups#!topic/protobuf/ZaDigptdcHM).
相对于一次性序列化的union type方式,使用内嵌消息字节流的方式,会多做一次字节数组的拷贝.我很担心这样会产生性能问题.
于是为了比较这两种方式.我做了一个简单的基本测试.
message PBTestNestPacket { required PBClientRequestType clientRequestType = 1; //请求类型 optional bytes requestData = 2; //请求数据 } message PBTestUnionPacket { required PBClientRequestType clientRequestType = 1; //请求类型 optional PBDummyRequest dummyRequest = 2; ......... optional PBAutoBattleRequest autoBattleRequest = 100; }
生成了两种内容同样的消息,并分别生成消息序列化后的字节数组, 然后对两个不同的字节数组深度序列化.
各自执行不同的几组测试,结果发现两种方式的响应时间并无太大差异,在多数时候,嵌套字节数组的方式.响应时间更快.
报文大小 测试轮数 内嵌方式响应时间 union方式响应时间. report: size:20, testTurn:5000 : nest: 167 union: 216 report: size:100, testTurn:5000 nest 246 union 256 report: size:100, testTurn:50000 nest 1163 union 1236 report: size:500, testTurn:5000 nest 750 union 789 report: size:500, testTurn:50000 nest 10122 union 10129 report: size:1000, testTurn:5000 nest 1407 union 1421 report: size:5000, testTurn:5000 nest 9471 union 9690
虽然产生了不科学的结果,但这次测试至少让我自己放了心.目前的使用协议方式和另一种方式相比性能上没有劣势.维护性上提高了不少.