.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; 它在很大程度改进了…

Vue 深度监听和初始绑定

vue的监听属性普通方式无法监听对象内部属性的改变&#xff0c;并且初始化时不会监听数据对象。 vue为监听属性提供了一种对象方法 watch: {option.size: {// handler为默认执行的方法handler (newValue, oldValue) {this.size newValue}&#xff0c;// 立即执行handler方法…

markdown流程图画法小结

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

hihocoder 1689 - 推断大小关系(图论+二分)

题目链接 https://vjudge.net/problem/HihoCoder-1689有N个整数A1, A2, ... AN&#xff0c;现在我们知道M条关于这N个整数的信息。每条信息是&#xff1a;Ai < Aj 或者 Ai Aj 小Hi希望你能从第一条信息开始依次逐条处理这些信息。一旦能推断出A1和AN的大小关系就立即停止。…

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

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

SpringBoot+Shiro学习(四):Realm授权

上一节我们讲了自定义Realm中的认证&#xff08;doGetAuthenticationInfo&#xff09;&#xff0c;这节我们继续讲另一个方法doGetAuthorizationInfo授权 授权流程 流程如下&#xff1a; 首先调用Subject.isPermitted/hasRole接口&#xff0c;其会委托给SecurityManager&#x…

Git放弃文件修改

已提交 # 撤销提交&#xff0c;保留修改内容 git reset <commit_id># 撤销提交&#xff0c;不保留修改内容 git reset --hard <commit_id>已暂存文件 # 撤销单个文件暂存 git reset HEAD <filename># 撤销所有文件/文件夹暂存 git reset HEAD .已跟踪未暂存…

[LeetCode][Java] Unique Paths II

题目&#xff1a; Follow up for "Unique Paths": Now consider if some obstacles are added to the grids. How many unique paths would there be? An obstacle and empty space is marked as 1 and 0 respectively in the grid. For example, There is one obst…

lua windows下编译

从Lua5.1开始官方给出的文件只有源代码和makefile文件了&#xff0c;官网给出的bulid方式也是在linux平台&#xff0c;如果只是想找个库使用下可以到这里来下载&#xff1a;http://joedf.ahkscript.org/LuaBuilds/ &#xff0c;如果需要自定修改库配置的话&#xff0c;就需要自…

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&…

Git 合并分支选项 --squash 合并提交历史

git merge --squash <branchname>--squash选项的含义是&#xff1a;本地文件内容与不使用该选项的合并结果相同&#xff0c;但是不提交、不移动HEAD&#xff0c;因此需要一条额外的commit命令。其效果相当于将another分支上的多个commit合并成一个&#xff0c;放在当前分…

Kubernetes共享使用Ceph存储

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

在Activity不可见时暂停WebView的语音播放,可见时继续播放之前的语音

private AudioManager mAudioManager;private AudioManager.OnAudioFocusChangeListener mFocusChangeListener; Override protected void onPause() {   super.onPause();   stopPlayVoice(); } Override protected void onResume() {   super.onResume();   startPla…

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…

php.ini-development和php.ini-production的区别

使用zip版MySQL安装时&#xff0c;需要将php.ini-development或php.ini-production改成php.ini&#xff0c;那么php.ini-development和php.ini-production的区别在哪儿呢&#xff0c;通俗的说法时&#xff0c;development是开发环境&#xff0c;production用于生产环境&#xf…

Server.MapPath()的用法

http://blog.csdn.net/qiuhaifeng_csu/article/details/19416407 Server.MapPath(string path)作用是返回与Web服务器上的指定虚拟路径相对应的物理文件路径。其参数path为Web 服务器的虚拟路径&#xff0c;返回结果是与path相对应的物理文件路径。但有时参数并非为虚拟路径&a…

为什么阿里巴巴禁止把SimpleDateFormat定义为static类型的?

在日常开发中&#xff0c;我们经常会用到时间&#xff0c;我们有很多办法在Java代码中获取时间。但是不同的方法获取到的时间的格式都不尽相同&#xff0c;这时候就需要一种格式化工具&#xff0c;把时间显示成我们需要的格式。 最常用的方法就是使用SimpleDateFormat类。这是一…

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

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

[译]关于NODE_ENV,哪些你应该了解

原文 Node.js开发者经常检测环境变量NODE_ENV&#xff0c;但你是否知道设置这个值同时也具有着某些别的意义&#xff1f;阅读本文你将发现这些。NODE_ENV是一个在Express框架中极其常用的环境变量。用其确定应用的运行环境&#xff08;诸如开发&#xff0c;staging&#xff0c;…