发布于2025年12月5日12月5日 本文将讨论基于WASM 的牧云插件系统开发中序列化方法的选择。我们将从性能、安全性、易用性等方面比较不同序列化方法的优缺点,并解释为什么我们最终选择哪种序列化方法。这是《Host Agent插件引擎开发故事》系列文章的第七篇,以后会持续更新。本系列文章将带领您深入了解长汀牧云团队的主机Agent插件引擎的开发过程。内容涵盖了技术选型、插件接口设计、组件通信框架等多个方面,并详细讲解了其背后的原理和实现方法。无论您是网络安全专业人士,还是对技术开发感兴趣的读者,您都可以从中有所收获。我们希望通过分享开发过程中面临的挑战、解决方案和实践经验,提供深入的见解和有价值的技术参考,帮助读者了解如何构建高效可靠的安全产品,共同推动安全技术社区的发展。 ## 简介 在之前的文章[《牧云插件系统技术选型之插件通信框架大PK》](https://stack.chaitin.com/techblog/detail?id=94)中,我们描述了主机Agent插件使用的通信框架,它使用了我们自己发明的“bincode-rpc”。在`bincode-rpc`中,需要考虑的一个主要问题是如何使用公共的`param`和`result`参数来传递数据。 ## 目标场景 据了解,我们的目标场景是:安全(永远是第一要务)、大量小型消息交换、少量结构复杂的大型消息交换、跨平台、高性能、单一语言。 一种简单而通用的方式就是把通用的数据交换格式放到通用的类型标准上,运行时按照约定解析出想要的字段,动态转换结构和数据类型。这种简单方法最经典的例子是“JSON”,类似的格式包括“BSON”和“CBOR”以及“MessagePack”。 ## 常用数据交换格式讨论 通用数据交换格式的主要问题是空间效率和时间效率比较低,提供的数据类型比较少,与编程语言的内置类型不完全对应。复杂数据结构的表示有时会存在歧义,反序列化回来时需要进行一些手动转换。 ### JSON JSON 是使用最广泛的通用数据交换格式。 [serde_json](https://github.com/serde-rs/json) 是世界上最快的JSON 序列化/反序列化库,但即使与类似格式的cbor 相比,**性能仍然落后**。因此它不适合我们的场景。 ### BSON [BSON](https://github.com/mongodb/bson-rust) 是二进制格式的“JSON”格式变体。 `BSON` 类型是`JSON` 类型的(几乎)超集,添加了日期类型、字节数组类型、特定数字类型等,并删除了通用数字类型(`number`)。 “BSON”最初被用作“MongoDB”文档数据库的存储和网络传输格式。它旨在提高存储和扫描效率。它速度更快,尺寸更小。但由于存在一些索引信息,其大小有时会大于`JSON`。 `MongoDB` 官方提供了`BSON` 序列化和反序列化的`Rust` 实现,维护得很好。 ### CBOR [cbor](https://github.com/pyfisch/cbor) 是“RFC 8949”的实现。标准稳定,格式类似于“JSON”。无需声明“schema”。它基于`serde`并且比`serde_json`具有更好的性能。然而,**存储库已停止维护**,并且“Rust”社区中没有其他成熟且活跃的“cbor”实现。就序列化和反序列化速度而言,大约需要`bincode`的3倍时间。 ### 消息包 [MessagePack](https://github.com/msgpack/msgpack-c) 我们之前使用过它。主要原因是“C”语言的实现速度非常快。主要目的是扩展`Go` 和`Lua` 的复杂数据结构通信能力(涉及到`CGO` 和`Lua` 的C API` 处理部分,相关内容和场景详见【上一篇文章】(https://stack.chaitin.com/techblog/detail?id=72)),但在我们的新场景中并没有什么特别的优势。首先,据说作为二进制序列化格式,它甚至可能比`Gzip`压缩的`JSON`格式数据还要大。其次,广泛报道兼容性存在较大问题。值得一提的是,`MessagePack`支持基于变长编码的数字类型压缩。 ## 特殊场景设计的序列化方法 另一种思路是采用针对专门场景设计的序列化方法,数据类型更丰富,通用性相对较少,可以很好地解决传统序列化中通用数据交换格式的一些痛点。 传统序列化的一个主要缺点是,从序列化值读取、解析和重建类型需要花费大量时间。例如,在“JSON”中,通过将内容括在双引号中并转义其中的无效字符来对字符串进行编码: ```json { \'line\': \'\\\'一切顺利,结局良好\\\'\' } ^^ ^ ^ ```` 将数字转换为字符: ```json { \'pi\': 3.1415926 } ^^^^^^^^^ ```` 在大多数情况下,甚至隐式字段名称也会转换为字符串: ```json { \'message_size\': 334 } ^^^^^^^^^^^^^^^^ ```` 所有这些字符不仅占据空间,而且占据时间。每次我们读取和解析`JSON`时,我们都必须处理这些字符以找出实际值是什么并在内存中重建它们。 “f32”类型的变量只有四个字节,但它使用九个字节进行编码。我们必须在反序列化过程中将这九个字符重新处理为正确的“f32”类型。 这种反序列化所消耗的时间很快就会增加,并且在数据密集型应用程序中,这部分处理占据了程序运行时间的主要部分。 **零拷贝反序列化**技术是解决此类问题的主要思想,**重新定义数据的二进制表示方法**是另一个思想。 根据我们的场景,主要考虑的是`rkyv`/`bincode`/`Protocol Buffers`/`FlatBuffers`。 ### rkyv [rkyv](https://github.com/rkyv/rkyv) 是其中**最快的**。序列化/反序列化/结果大小/内存消耗/社区维护等综合性能最优。它采用在代码内声明“schema”的形式。但默认没有格式检查,内部使用unsafe方法来实现。提供了`safe`和`unsafe`两个序列化接口函数。需要单独添加格式检查,检查方法比较麻烦。添加检查后,整体性能仍然是最好的,但带有检查的`bincode`和`rkyv`版本之间的差异很小。在“benchmark”中,与“bincode”相比,耗时减少了约100微秒。 ### 二进制代码 [bincode](https://github.com/bincode-org/bincode) 是一种专门为Rust 设计的紧凑的序列化/反序列化方法。它速度非常快并且消耗很少的内存。它采用在代码内部声明“schema”的形式,**具有完整的格式检查**,使用“safe”代码实现,可选大小限制,并承诺序列化后占用的空间小于或等于在“Rust”中运行时占用的空间,**整体性能优异**。缺点是它只适用于`Rust`,并且在序列化内容大小方面没有特别的优势(尽管也支持数字类型的变长编码)。它非常适合带有大量小消息的Rust 程序的本机通信。同时,它提供了流式序列化和反序列化API,非常好。除此之外,“Google”的[`tarpc`](https://github.com/google/tarpc) 依赖此库这一事实为复杂场景中的未来开发和兼容性问题提供了一些信心。 ### 协议缓冲区 [prost](https://github.com/tokio-rs/prost) 是一个成熟且流行的“Protocol Buffers”实现,需要一个单独的proto 文件来声明模式。它比此处列出的其他序列化方法更**重量级**。 `Protocol Buffers` 的序列化方法通过提供整数的变长编码来提高空间效率,但这种编码本身的编码和解码需要处理,并且仍然存在大量冗余字段。由于格外强调语言中立性和平台中立性,因此整体效率在二进制序列化方法中并不是特别突出,但具有广泛的支持和良好的兼容性。 ### 扁平缓冲区 [flatbuffers](https://github.com/google/flatbuffers) 是一个专注于“内存效率”的序列化库。基本达到了设计目标,但在结构庞大、复杂的测试数据上出现了明显的性能回归。就序列化和反序列化速度而言,大约需要`bincode`的4倍时间。与Protocol Buffers相比,序列化一般较慢(编码需要更多处理),反序列化速度较快(FlatBuffers是二进制格式,不需要解码),因此它的设计适合读多写少的场景。优点看起来主要是读取速度快,空间利用率高,但这对于在本机上频繁通讯来说毫无意义。 ## 特殊序列化方法的讨论 还有一些特殊的序列化方法似乎在某些方面表现得极其出色,我们将一一解释不选择它们的原因。 ### 憎恶 [Abomonation](https://github.com/TimelyDataflow/abomonation):最快。近三年没有维护过。非常不安全(每次序列化和反序列化都必须使用不安全的块)。它完全是使用指针技术完成的。为了方便处理内存对齐问题,有数据放大。当使用大量小串时,这个问题尤其严重。 ### 阿帕奇节俭 [Apache Thrift](https://github.com/apache/thrift):集成了RPC并内置了复杂的传输层和网络模型相关的东西,这是我们不想要的。 ### 博尔什 [Borsh](https://github.com/near/borsh-rs):它被称为安全关键系统设计,但其关键特性,即序列化对象及其提供的二进制表示的双射对于本地通信来说确实没有意义。大多数情况下处理速度不如其他类似库快。观察其应用,似乎主要用于区块链相关项目中的通信协议。 ### 原型船长 [Cap'n Proto](https://github.com/capnproto/capnproto-rust):采用类似C结构的布局方式,因此与平台无关,与语言无关。这也是一种不需要解码的格式。但当用于编码包含多维向量的大型数据结构时,它会显着减慢速度并占用大量空间。 ### 明信片 [postcard](https://github.com/jamesmunns/postcard):专注于提供`no_std` 体验,在性能方面与`bincode` 类似或更好。但`no_std`本身并不是我们关注的重点,因此获得的好处与我们无关,而做出的牺牲可能才是我们需要的。 ## 牧云插件系统序列化方式选择 众所周知,每次新系统涉及到关键技术选型时,团队内部都会充满兴奋。从更大的范围来看,这涉及到未来几年的成本和收益;从更大的层面来说,它体现了个人的审美和技术能力;在较小的范围内,可能只是支持各种技术互相争斗的忠实粉丝。 我们想补充的一项原则是:**安全,没有黑魔法**。虽然过度使用“技巧”可以带来超规格的好处,但您最终也可能会付出超规格的成本。 综上所述,考虑到成熟度/安全性/快速性的平衡,在我们的场景中选择“bincode”作为序列化方法是比较合适的。 ## 注意 \\* 除官方文档外,主要参考[rust_serialization_benchmark](https://github.com/djkoloski/rust_serialization_benchmark)的测试方法和结果。 ## 推荐相关博文 上一篇:**【【牧云插件系统技术选型及插件通信框架PK】】(https://stack.chaitin.com/techblog/detail?id=94)** 系列文章目录:【【预览】主机Agent插件引擎开发故事总结】(https://stack.chaitin.com/techblog/detail?id=82) 1. **[[揭秘牧云插件开发者的创新之路:从无法解决的问题到“充满乐趣”]](https://stack.chaitin.com/techblog/detail?id=72)** 2. **[【牧云插件系统面向未来的设计原则]](https://stack.chaitin.com/techblog/detail?id=73)** 3. **【【牧云插件系统选型之争】】(https://stack.chaitin.com/techblog/detail?id=77)** 4. **【【牧云插件系统技术选型中探针开发用什么? 】](https://stack.chaitin.com/techblog/detail?id=83)** 5. **【【牧云插件系统技术选型及插件开发? 】](https://stack.chaitin.com/techblog/detail?id=85)** 6. **【【牧云插件系统技术选型插件通信框架PK】】(https://stack.chaitin.com/techblog/detail?id=94)** 7. **[【牧云插件系统技术选型:大家都会抱怨的序列化方法选择之争]](https://stack.chaitin.com/techblog/detail?id=97)** 8. **[【牧云插件系统技术选型:应该选择哪种WASM运行时?如果不选择这个,就会出大问题! 】](https://stack.chaitin.com/techblog/detail?id=105)**
创建帐户或登录后发表意见