网站广告如何做,网站策划书网站需求分析,免费推广预期效果怎么写,锦州网站建设品牌好计算机单机性能一直受到摩尔定律的约束#xff0c;随着移动互联网的兴趣#xff0c;单机性能不足的瓶颈越来越明显#xff0c;制约着整个行业的发展。不过我们虽然不能无止境的纵向扩容系统#xff0c;但是我们可以分布式、横向的扩容系统#xff0c;这听起来非常的美好随着移动互联网的兴趣单机性能不足的瓶颈越来越明显制约着整个行业的发展。不过我们虽然不能无止境的纵向扩容系统但是我们可以分布式、横向的扩容系统这听起来非常的美好不过也带来了今天要说明的问题分布式的节点越多通信产生的成本就越大。网络传输带宽变得越来越紧缺我们服务器的标配上了 10Gbps 的网卡HTTPx.x 时代 TCP/IP 协议通讯低效我们即将用上 QUIC HTTP 3.0同机器走 Socket 协议栈太慢我们用起了 eBPF....现在我们的应用程序花在网络通讯上的时间太多了其中花在序列化上的时间也非常的多。我们和大家一样在内部微服务通讯序列化协议中绝大的部分都是用 JSON。JSON 的好处很多首先就是它对人非常友好我们能直接读懂它的含义但是它也有着致命的缺点那就是它序列化太慢、序列化以后的字符串太大了。之前笔者做一个项目时就遇到了一个选型的问题我们有数亿行数据需要缓存到 Redis 中每行数据有数百个字段如果用 Json 序列化存储的话它的内存消耗是数 TB级别的部署个集群再做个主从、多中心 需要成倍的内存、太贵了用不起。于是我们就在找有没有除了 JSON 其它更好的序列化方式看看都有哪些目前市面上序列化协议有很多比如 XML、JSON、Thrift、Kryo 等等我们选取了在.NET 平台上比较常用的序列化协议来做比较JSONJSON 是一种轻量级的数据交换格式。采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。ProtobufProtocol 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.DeserializeT(json)Google Protobuf.NET 上最常用的一个 Protobuf 序列化框架它其实是一个工具包通过工具包*.proto文件可以生成 GRPC Service 或者对应实体的序列化代码不过它使用起来有点麻烦。使用它我们需要两个 Nuget 包如下所示!--Google.Protobuf 序列化和反序列化帮助类--
PackageReference IncludeGoogle.Protobuf Version3.21.9 /!--Grpc.Tools 用于生成protobuf的序列化反序列化类 和 GRPC服务--
PackageReference IncludeGrpc.Tools Version2.50.0PrivateAssetsall/PrivateAssetsIncludeAssetsruntime; build; native; contentfiles; analyzers; buildtransitive/IncludeAssets
/PackageReference由于它不能直接使用 C#对象所以我们还需要创建一个*.proto文件布局和上面的 C#类一致加入了一个DemoClassArrayProto方便后面测试syntaxproto3;
option csharp_namespaceDemoClassProto;
package DemoClassProto;message DemoClassArrayProto
{repeated DemoClassProto DemoClass 1;
}message DemoClassProto
{int32 P11;bool P22;string P33;double P44;int64 P55;repeated DemoSubClassProto Subs6;
}message DemoSubClassProto
{int32 P11;bool P22;string P33;double P44;int64 P55;
}做完这一些后还需要在项目文件中加入如下的配置让Grpc.Tools在编译时生成对应的 C#类ItemGroupProtobuf Include*.proto GrpcServicesServer /
/ItemGroup然后 Build 当前项目的话就会在obj目录生成 C#类最后我们可以用下面的方法来实现序列化和反序列化泛型类型T是需要继承IMessageT从*.proto生成的实体(用起来还是挺麻烦的)using Google.Protobuf;// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] GoogleProtobufSerializeT(T origin) where T : IMessageT
{return origin.ToByteArray();
}// Deserialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DemoClassArrayProto GoogleProtobufDeserialize(byte[] bytes)
{return DemoClassArrayProto.Parser.ParseFrom(bytes);
}Protobuf.Net那么在.NET 平台 protobuf 有没有更简单的使用方式呢答案当然是有的我们只需要依赖下面的 Nuget 包PackageReference Includeprotobuf-net Version3.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[]的方法不得不使用一个MemoryStremusing ProtoBuf;// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProtoBufDotNetT(T origin, Stream stream)
{Serializer.Serialize(stream, origin);
}// Deserialize
public T ProtobufDotNet(byte[] bytes)
{using var stream new MemoryStream(bytes);return Serializer.DeserializeT(stream);
}MessagePack这里我们使用的是 Yoshifumi Kawai 实现的MessagePack-CSharp同样也是引入一个 Nuget 包PackageReference IncludeMessagePack Version2.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[] MessagePackT(T origin)
{return global::MessagePack.MessagePackSerializer.Serialize(origin);
}// Deserialize
public T MessagePackT(byte[] bytes)
{return global::MessagePack.MessagePackSerializer.DeserializeT(bytes);
}另外它提供了 Lz4 算法的压缩程序我们只需要配置 Option即可使用 Lz4 压缩压缩有两种方式Lz4Block和Lz4BlockArray我们试试public static readonly MessagePackSerializerOptions MpLz4BOptions MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block);// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] MessagePackLz4BlockT(T origin)
{return global::MessagePack.MessagePackSerializer.Serialize(origin, MpLz4BOptions);
}// Deserialize
public T MessagePackLz4BlockT(byte[] bytes)
{return global::MessagePack.MessagePackSerializer.DeserializeT(bytes, MpLz4BOptions);
}MemoryPack这里也是 Yoshifumi Kawai 大佬实现的MemoryPack同样也是引入一个 Nuget 包不过需要注意的是目前需要安装 VS 2022 17.3 以上版本和.NET7 SDK因为MemoryPack代码生成依赖了它PackageReference IncludeMemoryPack Version1.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[] MemoryPackT(T origin)
{return global::MemoryPack.MemoryPackSerializer.Serialize(origin);
}// Deserialize
public T MemoryPackT(byte[] bytes)
{return global::MemoryPack.MemoryPackSerializer.DeserializeT(bytes)!;
}它原生支持 Brotli 压缩算法使用如下所示// Serialize
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] MemoryPackBrotliT(T origin)
{using var compressor new BrotliCompressor();global::MemoryPack.MemoryPackSerializer.Serialize(compressor, origin);return compressor.ToArray();
}// Deserialize
public T MemoryPackBrotliT(byte[] bytes)
{using var decompressor new BrotliDecompressor();var decompressedBuffer decompressor.Decompress(bytes);return MemoryPackSerializer.DeserializeT(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 的结果如下所示从序列化速度来看MemoryPack遥遥领先比 JSON 要快 88%甚至比 Protobuf 快 15%。从序列化占用的内存来看MemoryPackBrotli是王者它比 JSON 占用少 98%甚至比Protobuf占用少 25%。其中ProtoBufDotNet内存占用大主要还是吃了没有byte[]返回方法的亏只能先创建一个MemoryStream。序列化结果大小这里我们可以看到MemoryPackBrotli赢麻了比不压缩的MemoryPack和Protobuf有着 10 多倍的差异。反序列化反序列化的 Benchmark 结果如下所示反序列化整体开销是比序列化大的毕竟需要创建大量的对象从反序列化的速度来看不出意外MemoryPack还是遥遥领先比 JSON 快 80%比Protobuf快 14%。从内存占用来看ProtobufDotNet是最小的这个结果听让人意外的其余的都表现的差不多总结总的相关数据如下表所示原始数据可以在文末的 Github 项目地址获取从图表来看如果要兼顾序列化后大小和性能的话我们应该要选择MemoryPackBrotli它序列化以后的结果最小而且兼顾了性能不过由于MemoryPack目前需要.NET7 版本所以现阶段最稳妥的选择还是使用MessagePackLz4压缩算法它有着不俗的性能表现和突出的序列化大小。回到文首的技术选型问题笔者那个项目最终选用的是Google Protobuf这个序列化协议和框架因为当时考虑到需要和其它语言交互然后也需要有较小空间占用目前看已经占用了111GB的 Redis 空间占用。如果后续进一步增大可以换成MessagePackLz4方式应该还能节省95GB的左右空间。那可都是白花花的银子。当然其它协议也是可以进一步通过Gzip、Lz4、Brotli算法进行压缩不过鉴于时间和篇幅关系没有进一步做测试有兴趣的同学可以试试。附录代码链接 https://github.com/InCerryGit/WhoIsFastest-Serialization