.NET性能优化-是时候换个序列化协议了

计算机单机性能一直受到摩尔定律的约束,随着移动互联网的兴趣,单机性能不足的瓶颈越来越明显,制约着整个行业的发展。不过我们虽然不能无止境的纵向扩容系统,但是我们可以分布式、横向的扩容系统,这听起来非常的美好,不过也带来了今天要说明的问题,分布式的节点越多,通信产生的成本就越大

  • 网络传输带宽变得越来越紧缺,我们服务器的标配上了 10Gbps 的网卡

  • HTTPx.x 时代 TCP/IP 协议通讯低效,我们即将用上 QUIC HTTP 3.0

  • 同机器走 Socket 协议栈太慢,我们用起了 eBPF

  • ....

现在我们的应用程序花在网络通讯上的时间太多了,其中花在序列化上的时间也非常的多。我们和大家一样,在内部微服务通讯序列化协议中,绝大的部分都是用 JSON。JSON 的好处很多,首先就是它对人非常友好,我们能直接读懂它的含义,但是它也有着致命的缺点,那就是它序列化太慢、序列化以后的字符串太大了。

之前笔者做一个项目时,就遇到了一个选型的问题,我们有数亿行数据需要缓存到 Redis 中,每行数据有数百个字段,如果用 Json 序列化存储的话它的内存消耗是数 TB级别的(部署个集群再做个主从、多中心 需要成倍的内存、太贵了,用不起)。于是我们就在找有没有除了 JSON 其它更好的序列化方式?

看看都有哪些

目前市面上序列化协议有很多比如 XML、JSON、Thrift、Kryo 等等,我们选取了在.NET 平台上比较常用的序列化协议来做比较:

  • JSON:JSON 是一种轻量级的数据交换格式。采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。

  • Protobuf:Protocol Buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等,它类似 XML,但比它更小、更快、更简单。

  • MessagePack:是一种高效的二进制序列化格式。它可以让你像 JSON 一样在多种语言之间交换数据。但它更快、更小。小的整数被编码成一个字节,典型的短字符串除了字符串本身之外,只需要一个额外的字节。

  • MemoryPack:是 Yoshifumi Kawai 大佬专为 C#设计的一个高效的二进制序列化格式,它有着.NET 平台很多新的特性,并且它是 Code First 开箱即用,非常简单;同时它还有着非常好的性能。

我们选择的都是.NET 平台上比较常用的,特别是后面的三种都宣称自己是非常小,非常快的,那么我们就来看看到底是谁最快,谁序列化后的结果最小。

准备工作

我们准备了一个 DemoClass 类,里面简单的设置了几个不同类型的属性,然后依赖了一个子类数组。暂时忽略上面的一些头标记。

[MemoryPackable]
[MessagePackObject]
[ProtoContract]
public partial class DemoClass
{[Key(0)] [ProtoMember(1)] public int P1 { get; set; }[Key(1)] [ProtoMember(2)] public bool P2 { get; set; }[Key(2)] [ProtoMember(3)] public string P3 { get; set; } = null!;[Key(3)] [ProtoMember(4)] public double P4 { get; set; }[Key(4)] [ProtoMember(5)] public long P5 { get; set; }[Key(5)] [ProtoMember(6)] public DemoSubClass[] Subs { get; set; } = null!;
}[MemoryPackable]
[MessagePackObject]
[ProtoContract]
public partial class DemoSubClass
{[Key(0)] [ProtoMember(1)] public int P1 { get; set; }[Key(1)] [ProtoMember(2)] public bool P2 { get; set; }[Key(2)] [ProtoMember(3)] public string P3 { get; set; } = null!;[Key(3)] [ProtoMember(4)] public double P4 { get; set; }[Key(4)] [ProtoMember(5)] public long P5 { get; set; }
}

System.Text.Json

选用它的原因很简单,这应该是.NET 目前最快的 JSON 序列化框架之一了,它的使用非常简单,已经内置在.NET BCL 中,只需要引用System.Text.Json命名空间,访问它的静态方法即可完成序列化和反序列化。

using System.Text.Json;var obj = ....;// Serialize
var json = JsonSerializer.Serialize(obj);// Deserialize
var newObj = JsonSerializer.Deserialize<T>(json)

Google Protobuf

.NET 上最常用的一个 Protobuf 序列化框架,它其实是一个工具包,通过工具包+*.proto文件可以生成 GRPC Service 或者对应实体的序列化代码,不过它使用起来有点麻烦。

使用它我们需要两个 Nuget 包,如下所示:

<!--Google.Protobuf 序列化和反序列化帮助类-->
<PackageReference Include="Google.Protobuf" Version="3.21.9" /><!--Grpc.Tools 用于生成protobuf的序列化反序列化类 和 GRPC服务-->
<PackageReference Include="Grpc.Tools" Version="2.50.0"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

由于它不能直接使用 C#对象,所以我们还需要创建一个*.proto文件,布局和上面的 C#类一致,加入了一个DemoClassArrayProto方便后面测试:

syntax="proto3";
option csharp_namespace="DemoClassProto";
package DemoClassProto;message DemoClassArrayProto
{repeated DemoClassProto DemoClass = 1;
}message DemoClassProto
{int32 P1=1;bool P2=2;string P3=3;double P4=4;int64 P5=5;repeated DemoSubClassProto Subs=6;
}message DemoSubClassProto
{int32 P1=1;bool P2=2;string P3=3;double P4=4;int64 P5=5;
}

做完这一些后,还需要在项目文件中加入如下的配置,让Grpc.Tools在编译时生成对应的 C#类:

<ItemGroup><Protobuf Include="*.proto" GrpcServices="Server" />
</ItemGroup>

然后 Build 当前项目的话就会在obj目录生成 C#类:91543da362e6497ecbbab05cf8d3d8b6.png476a4582d6feabe289d3483cc0398706.png

最后我们可以用下面的方法来实现序列化和反序列化,泛型类型T是需要继承IMessage<T>*.proto生成的实体(用起来还是挺麻烦的):

using Google.Protobuf;// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] GoogleProtobufSerialize<T>(T origin) where T : IMessage<T>
{return origin.ToByteArray();
}// Deserialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DemoClassArrayProto GoogleProtobufDeserialize(byte[] bytes)
{return DemoClassArrayProto.Parser.ParseFrom(bytes);
}

Protobuf.Net

那么在.NET 平台 protobuf 有没有更简单的使用方式呢?答案当然是有的,我们只需要依赖下面的 Nuget 包:

<PackageReference Include="protobuf-net" Version="3.1.22" />

然后给我们需要进行序列化的 C#类打上ProtoContract特性,另外将所需要序列化的属性打上ProtoMember特性,如下所示:

[ProtoContract]
public class DemoClass
{[ProtoMember(1)] public int P1 { get; set; }[ProtoMember(2)] public bool P2 { get; set; }[ProtoMember(3)] public string P3 { get; set; } = null!;[ProtoMember(4)] public double P4 { get; set; }[ProtoMember(5)] public long P5 { get; set; }
}

然后就可以直接使用框架提供的静态类进行序列化和反序列化,遗憾的是它没有提供直接返回byte[]的方法,不得不使用一个MemoryStrem

using ProtoBuf;// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProtoBufDotNet<T>(T origin, Stream stream)
{Serializer.Serialize(stream, origin);
}// Deserialize
public T ProtobufDotNet(byte[] bytes)
{using var stream = new MemoryStream(bytes);return Serializer.Deserialize<T>(stream);
}

MessagePack

这里我们使用的是 Yoshifumi Kawai 实现的MessagePack-CSharp,同样也是引入一个 Nuget 包:

<PackageReference Include="MessagePack" Version="2.4.35" />

然后在类上只需要打一个MessagePackObject的特性,然后在需要序列化的属性打上Key特性:

[MessagePackObject]
public partial class DemoClass
{[Key(0)] public int P1 { get; set; }[Key(1)] public bool P2 { get; set; }[Key(2)] public string P3 { get; set; } = null!;[Key(3)] public double P4 { get; set; }[Key(4)] public long P5 { get; set; }
}

使用起来也非常简单,直接调用MessagePack提供的静态类即可:

using MessagePack;// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] MessagePack<T>(T origin)
{return global::MessagePack.MessagePackSerializer.Serialize(origin);
}// Deserialize
public T MessagePack<T>(byte[] bytes)
{return global::MessagePack.MessagePackSerializer.Deserialize<T>(bytes);
}

另外它提供了 Lz4 算法的压缩程序,我们只需要配置 Option,即可使用 Lz4 压缩,压缩有两种方式,Lz4BlockLz4BlockArray,我们试试:

public static readonly MessagePackSerializerOptions MpLz4BOptions =   MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block);// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] MessagePackLz4Block<T>(T origin)
{return global::MessagePack.MessagePackSerializer.Serialize(origin, MpLz4BOptions);
}// Deserialize
public T MessagePackLz4Block<T>(byte[] bytes)
{return global::MessagePack.MessagePackSerializer.Deserialize<T>(bytes, MpLz4BOptions);
}

MemoryPack

这里也是 Yoshifumi Kawai 大佬实现的MemoryPack,同样也是引入一个 Nuget 包,不过需要注意的是,目前需要安装 VS 2022 17.3 以上版本和.NET7 SDK,因为MemoryPack代码生成依赖了它:

<PackageReference Include="MemoryPack" Version="1.4.4" />

使用起来应该是这几个二进制序列化协议最简单的了,只需要给对应的类加上partial关键字,另外打上MemoryPackable特性即可:

[MemoryPackable]
public partial class DemoClass
{public int P1 { get; set; }public bool P2 { get; set; }public string P3 { get; set; } = null!;public double P4 { get; set; }public long P5 { get; set; }
}

序列化和反序列化也是调用静态方法:

// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] MemoryPack<T>(T origin)
{return global::MemoryPack.MemoryPackSerializer.Serialize(origin);
}// Deserialize
public T MemoryPack<T>(byte[] bytes)
{return global::MemoryPack.MemoryPackSerializer.Deserialize<T>(bytes)!;
}

它原生支持 Brotli 压缩算法,使用如下所示:

// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] MemoryPackBrotli<T>(T origin)
{using var compressor = new BrotliCompressor();global::MemoryPack.MemoryPackSerializer.Serialize(compressor, origin);return compressor.ToArray();
}// Deserialize
public T MemoryPackBrotli<T>(byte[] bytes)
{using var decompressor = new BrotliDecompressor();var decompressedBuffer = decompressor.Decompress(bytes);return MemoryPackSerializer.Deserialize<T>(decompressedBuffer)!;
}

跑个分吧

我使用BenchmarkDotNet构建了一个 10 万个对象序列化和反序列化的测试,源码在末尾的 Github 链接可见,比较了序列化、反序列化的性能,还有序列化以后占用的空间大小。

public static class TestData
{//public static readonly DemoClass[] Origin = Enumerable.Range(0, 10000).Select(i =>{return new DemoClass{P1 = i,P2 = i % 2 == 0,P3 = $"Hello World {i}",P4 = i,P5 = i,Subs = new DemoSubClass[]{new() {P1 = i, P2 = i % 2 == 0, P3 = $"Hello World {i}", P4 = i, P5 = i,},new() {P1 = i, P2 = i % 2 == 0, P3 = $"Hello World {i}", P4 = i, P5 = i,},new() {P1 = i, P2 = i % 2 == 0, P3 = $"Hello World {i}", P4 = i, P5 = i,},new() {P1 = i, P2 = i % 2 == 0, P3 = $"Hello World {i}", P4 = i, P5 = i,},}};}).ToArray();public static readonly DemoClassProto.DemoClassArrayProto OriginProto;static TestData(){OriginProto = new DemoClassArrayProto();for (int i = 0; i < Origin.Length; i++){OriginProto.DemoClass.Add(DemoClassProto.DemoClassProto.Parser.ParseJson(JsonSerializer.Serialize(Origin[i])));}}
}

序列化

序列化的 Bemchmark 的结果如下所示:72797be2164dc6d80dd17e8c798992c6.png

从序列化速度来看MemoryPack遥遥领先,比 JSON 要快 88%,甚至比 Protobuf 快 15%。f1c2b71d76c0cc46059a5c99556cea41.png

从序列化占用的内存来看,MemoryPackBrotli是王者,它比 JSON 占用少 98%,甚至比Protobuf占用少 25%。其中ProtoBufDotNet内存占用大主要还是吃了没有byte[]返回方法的亏,只能先创建一个MemoryStream

19f665c71b2f0b979e37d2413d7d15ee.png

序列化结果大小

这里我们可以看到MemoryPackBrotli赢麻了,比不压缩的MemoryPackProtobuf有着 10 多倍的差异。1e88bb105e0c8a79d4ffff8bc299cc23.png

反序列化

反序列化的 Benchmark 结果如下所示,反序列化整体开销是比序列化大的,毕竟需要创建大量的对象:289168523f073e44f91c5d90a05c923e.png

从反序列化的速度来看,不出意外MemoryPack还是遥遥领先,比 JSON 快 80%,比Protobuf快 14%。79860de5b1b90a20648e51a888735c21.png

从内存占用来看ProtobufDotNet是最小的,这个结果听让人意外的,其余的都表现的差不多:4a4669ab3c60b29e6f84f84a15dd9b8b.png

总结

总的相关数据如下表所示,原始数据可以在文末的 Github 项目地址获取:04c4485e38f4cf6a7d86986de66942be.png

从图表来看,如果要兼顾序列化后大小和性能的话我们应该要选择MemoryPackBrotli,它序列化以后的结果最小,而且兼顾了性能:7881a8b608fc2bb7d061903d278b9b21.png

不过由于MemoryPack目前需要.NET7 版本,所以现阶段最稳妥的选择还是使用MessagePack+Lz4压缩算法,它有着不俗的性能表现和突出的序列化大小。

回到文首的技术选型问题,笔者那个项目最终选用的是Google Protobuf这个序列化协议和框架,因为当时考虑到需要和其它语言交互,然后也需要有较小空间占用,目前看已经占用了111GB的 Redis 空间占用。

9337bdc2d08f2cb7437b32bd5e10490a.png

如果后续进一步增大,可以换成MessagePack+Lz4方式,应该还能节省95GB的左右空间。那可都是白花花的银子。

当然其它协议也是可以进一步通过GzipLz4Brotli算法进行压缩,不过鉴于时间和篇幅关系,没有进一步做测试,有兴趣的同学可以试试。

附录

代码链接: https://github.com/InCerryGit/WhoIsFastest-Serialization

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/281287.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kubernetes-基于Helm安装部署高可用的Redis

1、Redis简介 Redis是一个开放源代码&#xff08;BSD许可证&#xff09;的代理&#xff0c;其在内存中存储数据&#xff0c;可以代理数据库、缓存和消息。它支持字符串、散列、列表、集合和位图等数据结构。Redis 是一个高性能的key-value数据库&#xff0c; 它在很大程度改进了…

markdown流程图画法小结

markdown流程图画法小结markdown画图流程图 最简单的流程图为例mermaid! graph TD A --> B //在没有(),[].{}等括号的情况之下&#xff0c;图标默认名字就是字母 A --> C C --> D B --> D 给图标添加名字&#xff0c;改变只有矩阵图形&#xff0c;在箭头上添加文字…

32岁京东毕业程序员,走投无路当了外企外包,闲得心里发慌,到点下班浑身不自在!...

‍‍当一位京东程序员进入外企当外包会怎么样&#xff1f;顺利躺平&#xff0c;实现wlb&#xff08;工作生活平衡&#xff09;吗&#xff1f;未必&#xff0c;因为人是一种很奇怪的动物。这位网友说&#xff1a;32岁京东毕业程序员&#xff0c;找了几个月工作一直没有合适的&am…

XAML 创建浏览器应用程序

XAML 创建浏览器应用程序XAML 创建浏览器应用程序作者&#xff1a;WPFDevelopersOrg - 驚鏵原文链接&#xff1a;https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/app-development/wpf-xaml-browser-applications-overview?viewnetframeworkdesktop-4.8框架使用.NET40&…

Kubernetes共享使用Ceph存储

目录 简要概述环境测试结果验证简要概述 Kubernetes pod 结合Ceph rbd块设备的使用&#xff0c;让Docker 数据存储在Ceph,重启Docker或k8s RC重新 调 度pod 不会引起数据来回迁移。 工作原理无非就是拿到ceph集群的key作为认证&#xff0c;远程rbdmap映射挂载使用。那么就要启用…

MFC界面库BCGControlBar v25.3新版亮点:Dialogs和Forms

2019独角兽企业重金招聘Python工程师标准>>> 亲爱的BCGSoft用户&#xff0c;我们非常高兴地宣布BCGControlBar Professional for MFC和BCGSuite for MFC v25.3正式发布&#xff01;新版本添加了对Visual Studio 2017的支持、增强对Windows 10的支持等。接下来几篇文…

基于 .NET 7 的 QUIC 实现 Echo 服务

前言随着今年6月份的 HTTP/3 协议的正式发布&#xff0c;它背后的网络传输协议 QUIC&#xff0c;凭借其高效的传输效率和多路并发的能力&#xff0c;也大概率会取代我们熟悉的使用了几十年的 TCP&#xff0c;成为互联网的下一代标准传输协议。在去年 .NET 6 发布的时候&#xf…

关于信息收集和加工的思考

随着互联网的发展&#xff0c;获取信息的手段越来越多&#xff0c;我们对手机的依赖程度超乎想象&#xff0c;每天忙碌着&#xff0c;大脑接收着丰富的信息&#xff0c;感觉每天都学习到了很多的知识。但我们对学习经常会有些误区&#xff1a;1、书买了摆在书架上&#xff0c;看…

8支团队正在努力构建下一代Ethereum

“我们不想在构建 Ethereum 2.0时重新造轮子。” 谈到开发人员为 Ethereum 区块链进行两个独立的升级&#xff08;一个称为 Ethereum 2.0&#xff0c;另一个称为 Ethereum 1x&#xff09;所作出的补充努力&#xff0c;劳尔乔丹坚持认为&#xff0c;在较短的时间内将升级包括在 …

windows环境下TP5.1使用think-worker(Workerman/GatewayWorker)

文章目录首先是解决如何运行gatewayworker调试gatewayworker程序向指定客户端发送消息在TP框架中调用Gateway的API总结说明测试环境 windows10&#xff1b;PHP7.2&#xff1b;TP5.1&#xff1b; 这里只介绍如何使用TP集成的workerman扩展库think-worker&#xff0c;原生workerm…

webpack之DefinePlugin使用

DefinePlugin是webpack注入全局变量的插件&#xff0c;通常使用该插件来判别代码运行的环境变量。在使用该插件需要注意的是&#xff0c;如果在该插件配置了相关的参数&#xff0c;必须要源码中使用&#xff0c;webpack才会注入。例如&#xff1a; new webpack.DefinePlugin({p…

Magicodes.IE 2.7.0发布

2.7.02022.11.07使用SkiaSharp替代SixLabors.ImageSharp移除SixLabors.Fonts感谢linch90的大力支持&#xff08;具体见pr#462&#xff09;部分方法改为虚方法2.7.0-beta2022.10.27使用SixLabors.ImageSharp替代System.Drawing&#xff0c;感谢linch90 &#xff08;见pr#454&…

linux lsof/netstat查看进程和端口号相关命令:

本文为博主原创&#xff0c;未经允许不得转载&#xff1a; 在linux操作时&#xff0c;经常要查看运行的项目的进程和端口号&#xff0c;在这里总结了以下常用到的相关命令&#xff1a; 1.查看系统运行的java项目&#xff0c;并查看进程号 这个用到的命令为&#xff1a; ps -ef|…

C#高级编程9 第17章 使用VS2013-C#特性

C#高级编程9 第17章 使用VS2013 编辑定位到 如果默认勾选了这项&#xff0c;请去掉勾选&#xff0c;因为勾选之后解决方案的目录会根据当前文件选中。 可以设置项目并行生成数 版本控制软件设置 所有文本编辑器行号显示 启用编辑继续 收集调试信息&#xff0c;将影响性能 Code …

还在手画C#依赖关系图吗?快来试试这个工具吧!

还在手画C#依赖关系图吗&#xff1f;快来试试这个工具吧&#xff01;笔者最近见到了一个不错的工具&#xff0c;可以让大家在看代码的时候一键生成C#依赖的类图。非常适合编写文档、查看和学习开源项目设计时使用&#xff0c;比如下方就是笔者通过这个工具生成的Microsoft.Exte…

51 Nod 1027 大数乘法【Java大数乱搞】

1027 大数乘法 基准时间限制&#xff1a;1 秒 空间限制&#xff1a;131072 KB 分值: 0 难度&#xff1a;基础题 给出2个大整数A,B&#xff0c;计算A*B的结果。Input第1行&#xff1a;大数A 第2行&#xff1a;大数B (A,B的长度 < 1000&#xff0c;A,B > 0&#xff09; Out…

关于ASP.NET Core WebSocket实现集群的思考

前言提到WebSocket相信大家都听说过&#xff0c;它的初衷是为了解决客户端浏览器与服务端进行双向通信&#xff0c;是在单个TCP连接上进行全双工通讯的协议。在没有WebSocket之前只能通过浏览器到服务端的请求应答模式比如轮询&#xff0c;来实现服务端的变更响应到客户端&…

更快,更强的.NET 7 发布了

.NET Conf 2022 在昨晚(11⽉8⽇) 11 点 正式开始了&#xff0c;为期三天的会议&#xff08;11⽉8-10⽇&#xff09;&#xff0c; 围绕 .NET 7 展开。相信各位⼩伙伴都已经开始安装 .NET 7 正式版本还有以及相关的开发⼯具。这次 .NET 7 围绕传统的 C# &#xff0c;ASP.NET Core…

jvm(Java virtual machine) JVM架构解释

2019独角兽企业重金招聘Python工程师标准>>> JVM 架构解释 每个Java开发者都知道通过JRE【Java运行环境】执行字节码。 但是很多人都不知道JRE是JVM实现的事实。JVM负责执行字节码的分析 代码的解释和运行。 我们应该了解JVM的架构&#xff0c;这对开发者来说是很重…

WinForm(十五)窗体间通信

在很多WinForm的程序中&#xff0c;会有客户端之间相互通信的需求&#xff0c;或服务端与客户端通信的需求&#xff0c;这时就要用到TCP/IP的功能。在.NET中&#xff0c;主要是通过Socket来完成的&#xff0c;下面的例子是通过一个TcpListerner作为监听&#xff0c;等待TcpClie…