高性能实体序列化:BSON vs MessagePack(vs JSON)

157

最近我发现了一种替代Google的Protocol BuffersJSON的二进制序列化格式,它叫做MessagePack,并且性能优于两者。

MongoDB用的BSON序列化格式也值得一提,因为它可以用来存储数据。

有人能具体说明下BSON与MessagePack的区别及其优缺点吗?


为了完整列出高性能的二进制序列化格式,还有Gobs。虽然与其他所有提到的格式相比,它们不是语言无关的,并依赖于Go的内置反射机制,但除了Go之外,至少还有一种语言也有Gobs库。


3
这段话大多数只是炒作营销。序列化格式的性能取决于使用的实现方式。虽然某些格式具有固有的开销(例如,JSON因为它是全动态处理的),但格式本身并没有“速度”。该页面接着以非常不客观的方式“挑选和选择”比较自己...这不符合我的喜好。 - user166390
6
更正:Gobs并不是为了取代Protocol Buffers,也很可能永远不会成为替代品。此外,Gobs适用于任何编程语言(可以在任何语言中读写,参见http://code.google.com/p/libgob/),但它们的定义与Go处理数据的方式密切相关,因此最适合在Go中使用。 - Kyle C
6
链接指向 msgpack 性能基准测试的页面无法访问(http://msgpack.org/index/speedtest.png)。 - Aliaksei Ramanau
6个回答

224

// 请注意,我是MessagePack的作者,这个答案可能有偏见。

格式设计

  1. 与JSON兼容性

    BSON的名称虽然与JSON类似,但与MessagePack相比,它与JSON的兼容性不太好。

    BSON具有特殊类型,如“ObjectId”、“Min key”、“UUID”或“MD5”(我认为这些类型是MongoDB所需的)。这些类型与JSON不兼容。这意味着当您将对象从BSON转换为JSON时,某些类型信息可能会丢失,但当这些特殊类型在BSON源中时,当然只有这种情况才会发生。在单个服务中同时使用JSON和BSON可能是一个缺点。

    MessagePack旨在能够透明地从/到JSON进行转换。

  2. MessagePack比BSON更小

    MessagePack的格式比BSON更简洁。因此,MessagePack可以序列化比BSON更小的对象。

    例如,一个简单的映射{"a":1,"b":2}在MessagePack中序列化为7个字节,而BSON使用19个字节。

  3. BSON支持原地更新

    使用BSON,可以修改已存储对象的一部分,而无需重新对整个对象进行序列化。假设在文件中存储了一个映射{"a":1,"b":2},您想要将"a"的值从1更新为2000。

    使用MessagePack,1仅使用1个字节,但2000使用3个字节。因此,“b”必须向后移动2个字节,而“b”未被修改。

    使用BSON,1和2000都使用5个字节。由于这种冗余性,您不必移动“b”。

  4. MessagePack具有RPC

    MessagePack、Protocol Buffers、Thrift和Avro支持RPC。但是,BSON不支持。

这些差异意味着,MessagePack最初设计用于网络通信,而BSON设计用于存储。

实现和API设计

  1. MessagePack具有类型检查API(Java、C++和D)

    MessagePack支持静态类型。

    用于JSON或BSON的动态类型对于像Ruby、Python或JavaScript这样的动态语言非常有用。但是对于静态语言来说很麻烦。您必须编写枯燥的类型检查代码。

    MessagePack提供类型检查API。它将动态类型对象转换为静态类型对象。这里是一个简单的示例(C ++):

    #include <msgpack.hpp>

    class myclass {
    private:
        std::string str;
        std::vector<int> vec;
    public:
        // This macro enables this class to be serialized/deserialized
        MSGPACK_DEFINE(str, vec);
    };

    int main(void) {
        // serialize
        myclass m1 = ...;

        msgpack::sbuffer buffer;
        msgpack::pack(&buffer, m1);

        // deserialize
        msgpack::unpacked result;
        msgpack::unpack(&result, buffer.data(), buffer.size());

        // you get dynamically-typed object
        msgpack::object obj = result.get();

        // convert it to statically-typed object
        myclass m2 = obj.as<myclass>();
    }
  1. MessagePack支持IDL

    与类型检查API相关,MessagePack支持IDL。(规范可从以下链接获取:http://wiki.msgpack.org/display/MSGPACK/Design+of+IDL

    Protocol Buffers和Thrift需要IDL(不支持动态类型),并提供更成熟的IDL实现。

  2. MessagePack具有流API(Ruby、Python、Java、C++等)

    MessagePack支持流式反序列化器。这个功能对于网络通信很有用。以下是一个示例(Ruby):

    require 'msgpack'

    # write objects to stdout
    $stdout.write [1,2,3].to_msgpack
    $stdout.write [1,2,3].to_msgpack

    # read objects from stdin using streaming deserializer
    unpacker = MessagePack::Unpacker.new($stdin)
    # use iterator
    unpacker.each {|obj|
      p obj
    }

35
就数据大小和因此产生的传输性能而言,MessagePack如何与Google Protobufs相比? - Ellis
5
第一点忽略了一个事实,即MessagePack具有不能在JSON中表示的原始字节能力。因此在这方面它与BSON是一样的... - user172783
4
通常情况下,原始字节被认为是一个字符串(通常是utf-8),除非在传输双方都预先约定了其他的格式。msgpack被用作流/序列化格式,比JSON更简洁,但也不如JSON易于阅读。 - Tracker1
4
不完全准确。这对于静态类型语言中的BSON实现也是正确的。 - Brandon Black
2
有没有人能告诉我什么是“RPC”? - gen
显示剩余5条评论

18

我认为重要的是要提到,这取决于您的客户端/服务器环境的样子。

如果您多次传递未经检查的字节,例如使用消息队列系统或将日志条目流式传输到磁盘,则可能更喜欢二进制编码以强调紧凑性。否则,不同环境下会有不同的情况。

某些环境可以非常快速地进行msgpack / protobuf的序列化和反序列化,而其他环境则不然。一般来说,语言/环境越低级,二进制序列化的效果就越好。在高级语言(node.js、.Net、JVM)中,您经常会看到JSON序列化实际上更快。那么问题就变成了您的网络开销是否比内存/CPU受限更少?

关于msgpack vs bson vs protocol buffers... msgpack是三者中所需字节数最小的,protocol buffers大约相同。BSON定义的本机类型比其他两种类型更广泛,可能更符合您的对象模型,但这使其更冗长。Protocol buffers具有流设计,这使其成为二进制传输/存储格式的更自然格式。

个人而言,除非存在更轻量级的数据传输的明显需求,否则我会倾向于直接使用JSON提供的透明度。通过使用压缩的数据传输HTTP,格式间的网络开销差异甚至更小。


7
原文:Native MsgPack is only efficient with ProtocolBuffers size-wise as the length of the keys (which are always-present text) are short such as "a" or "b" - or are otherwise an insignificant part of the entire payload. They are always short in ProtocolBuffers which uses an IDL/compile to map field descriptors to ids. This is also what makes MsgPack "dynamic", which ProtocolBuffers is most certainly not ..翻译:仅在键的长度(始终存在的文本)较短,例如"a"或"b",或在整个负载中是不重要的情况下,原生MsgPack与ProtocolBuffers在大小方面才有效率。在使用IDL /编译将字段描述符映射到ID的ProtocolBuffers中,它们始终很短。这也是使MsgPack“动态”的原因,而ProtocolBuffers则完全不是。 - user2864740
2
终点还是很好的:gzip/deflate 对于在许多记录中出现“较长但重复很多”的键(MsgPack、JSON/BSON 和 XML 等)的冗余处理非常出色,但对于 ProtocolBuffers 毫无帮助。Avro 通过单独传输模式来手动消除键冗余。 - user2864740

9

正如作者所说,MessagePack最初是为网络通信设计的,而BSON则是为数据存储设计的。

MessagePack很紧凑,而BSON则比较冗长。MessagePack旨在提高空间效率,而BSON则是为CURD(时间效率)而设计的。

最重要的是,MessagePack的类型系统(前缀)遵循Huffman编码,这里我画了一个MessagePack的Huffman树(点击链接查看图像):

MessagePack的Huffman树


5

一个尚未被提及的关键区别是,BSON包含整个文档以及进一步嵌套的子文档的字节大小信息。

document    ::=     int32 e_list

这对于有大小和性能限制的环境(例如嵌入式系统)有两个主要好处:
  1. 您可以立即检查您将要解析的数据是否代表完整的文档,或者您是否需要在某个时候请求更多数据(无论是来自某个连接还是存储)。由于这很可能是一个异步操作,因此您可能已经在解析之前发送了新的请求。
  2. 您的数据可能包含完整的子文档以及与您无关的信息。BSON允许您使用子文档的大小信息跳过它,轻松遍历到子文档后面的下一个对象。另一方面,msgpack包含所谓映射中的元素数(类似于BSON的子文档)。虽然这无疑是有用的信息,但它对解析器没有什么帮助。您仍然必须解析映射内的每个对象,而不能只跳过它。根据您的数据结构,这可能会对性能产生巨大影响。

1
快速测试显示,经过压缩的 JSON 反序列化比二进制的 MessagePack 更快。在测试中,Article.json 是 550kb 的经过压缩的 JSON,Article.mpack 是它的 420kb MP 版本。当然,这可能是实现问题。
MessagePack:
//test_mp.js
var msg = require('msgpack');
var fs = require('fs');

var article = fs.readFileSync('Article.mpack');

for (var i = 0; i < 10000; i++) {
    msg.unpack(article);    
}

JSON:

// test_json.js
var msg = require('msgpack');
var fs = require('fs');

var article = fs.readFileSync('Article.json', 'utf-8');

for (var i = 0; i < 10000; i++) {
    JSON.parse(article);
}

有时候是这样的:

Anarki:Downloads oleksii$ time node test_mp.js 

real    2m45.042s
user    2m44.662s
sys     0m2.034s

Anarki:Downloads oleksii$ time node test_json.js 

real    2m15.497s
user    2m15.458s
sys     0m0.824s

那么节省空间了,但速度快吗?不是的。

测试版本:

Anarki:Downloads oleksii$ node --version
v0.8.12
Anarki:Downloads oleksii$ npm list msgpack
/Users/oleksii
└── msgpack@0.1.7  

9
肯定取决于实现方式。我用Python 2.7.3测试了一个489K的test.json(等效于409K的test.msgpack),测试结果显示对于10000次迭代,simplejson 2.6.2需要66.7秒,而msgpack 0.2.2只需要28.8秒。 - Day
2
这个 Article.json 文件从哪里来的? - Ant6n
同事们,测试代码在我上面的评论中,你还期望什么呢?Article.json是我们项目中序列化为JSON的对象。而且现在那些结果可能已经不相关了。 - Oleksiy Khilkevich
23
由于JS在C++中本地实现了JSON,而msgpack在JS中实现,因此这不是一次公平的性能比较。 - Alex Panchenko
4
您希望让MessagePack比古罗马人更擅长说拉丁语。JSON是JavaScript本地支持的(C ++),而MessagePack是用JavaScript编写的,这意味着它需要解释执行。基本上这是在比较两段代码片段,一个是用JavaScript编写的,另一个是用C ++编写的。 - ramazan polat
显示剩余3条评论

1

我进行了一个简单的基准测试,比较了MessagePack和BSON的编码和解码速度。如果你有大型二进制数组,BSON的速度会更快:

BSON writer: 2296 ms (243487 bytes)
BSON reader: 435 ms
MESSAGEPACK writer: 5472 ms (243510 bytes)
MESSAGEPACK reader: 1364 ms

使用C# Newtonsoft.Json和MessagePack by neuecc:
    public class TestData
    {
        public byte[] buffer;
        public bool foobar;
        public int x, y, w, h;
    }

    static void Main(string[] args)
    {
        try
        {
            int loop = 10000;

            var buffer = new TestData();
            TestData data2;
            byte[] data = null;
            int val = 0, val2 = 0, val3 = 0;

            buffer.buffer = new byte[243432];

            var sw = new Stopwatch();

            sw.Start();
            for (int i = 0; i < loop; i++)
            {
                data = SerializeBson(buffer);
                val2 = data.Length;
            }

            var rc1 = sw.ElapsedMilliseconds;

            sw.Restart();
            for (int i = 0; i < loop; i++)
            {
                data2 = DeserializeBson(data);
                val += data2.buffer[0];
            }
            var rc2 = sw.ElapsedMilliseconds;

            sw.Restart();
            for (int i = 0; i < loop; i++)
            {
                data = SerializeMP(buffer);
                val3 = data.Length;
                val += data[0];
            }

            var rc3 = sw.ElapsedMilliseconds;

            sw.Restart();
            for (int i = 0; i < loop; i++)
            {
                data2 = DeserializeMP(data);
                val += data2.buffer[0];
            }
            var rc4 = sw.ElapsedMilliseconds;

            Console.WriteLine("Results:", val);
            Console.WriteLine("BSON writer: {0} ms ({1} bytes)", rc1, val2);
            Console.WriteLine("BSON reader: {0} ms", rc2);
            Console.WriteLine("MESSAGEPACK writer: {0} ms ({1} bytes)", rc3, val3);
            Console.WriteLine("MESSAGEPACK reader: {0} ms", rc4);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }

        Console.ReadLine();
    }

    static private byte[] SerializeBson(TestData data)
    {
        var ms = new MemoryStream();

        using (var writer = new Newtonsoft.Json.Bson.BsonWriter(ms))
        {
            var s = new Newtonsoft.Json.JsonSerializer();
            s.Serialize(writer, data);
            return ms.ToArray();
        }
    }

    static private TestData DeserializeBson(byte[] data)
    {
        var ms = new MemoryStream(data);

        using (var reader = new Newtonsoft.Json.Bson.BsonReader(ms))
        {
            var s = new Newtonsoft.Json.JsonSerializer();
            return s.Deserialize<TestData>(reader);
        }
    }

    static private byte[] SerializeMP(TestData data)
    {
        return MessagePackSerializer.Typeless.Serialize(data);
    }

    static private TestData DeserializeMP(byte[] data)
    {
        return (TestData)MessagePackSerializer.Typeless.Deserialize(data);
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接