下个月,Microsoft.NET5将正式发布,大家都在关注这门新语言。不知道大家对.NET5有没有期待,前几天官方发布了一些.net5特性的说明,其中gRPC的性能让人印象深刻。在不同gRPC服务器实现的社区运行基准测试中,.NET的QPS超越了C++和Go,仅次于Rust位居亚军。gRPC是一个现代开源远程过程调用框架。gRPC有许多令人兴奋的特性:实时流、端到端代码生成和强大的跨平台支持。结果基于在.NET5中完成的工作。基准测试表明.NET5服务器性能比.NETCore3.1快60%。.NET5客户端性能比.NETCore3.1快230%。在这篇文章中,让我们一起来了解一下.NET5是用什么黑魔法让性能提升如此之大。减少内存分配去年,微软为CNCF提供了一个新的gRPCfor.NET实现。该框架建立在Kestrel和HttpClient之上,使gRPC成为.NET生态系统的一流成员。gRPC使用HTTP/2作为其底层协议。就性能而言,快速的HTTP/2实现是最重要的因素。.NET的gRPC服务器基于Kestrel,这是一种用C#编写的HTTP服务器,在设计时考虑到了性能,并且是TechEmpower基准测试中表现最好的服务器之一。gRPC自动受益于Kestrel的许多性能改进。然而,.NET5中有许多HTTP/2特定的优化。减少内存分配是优化的第一部分。减少每个HTTP/2请求的内存分配可以减少垃圾收集(GC)时间。这是请求超过100,000个gRPC请求时的性能分析器:活动对象图的锯齿形模式表示正在构建内存,然后进行垃圾回收。每个请求分配大约3.9KB。通过向HTTP/2连接添加连接池,每个请求的内存分配减少了一半。它支持对内部类型(例如Http2Stream和Http2Stream)和可公开访问的类型(例如HttpContext和HttpRequest)的请求重用。合并流后,可以进行一系列的优化:重用输入和输出Pipe实例。重用已知的标头字符串值。与标头重用相关,将HTTP/Masquerade标头添加为已知标头。字符串分配使用倒数第三个字节。一些较小的请求对象被重用。连接池在服务器处于负载状态下很有用,但需要释放不再使用的内存。如果在过去5秒内未被HTTP请求使用,该流将从连接池中删除。还有许多减少内存分配的较小方法:删除Kestrel的HTTP/2流控制中的分配。每当触发流程控制时,可重置的ManualResetValueTaskSourceCore类型将改为分配一个新对象。在验证HTTP请求路径时用stackalloc替换数组分配。消除了一些与日志记录相关的意外分配。如果任务已经完成,请避免分配。最后,字符串分配与特殊的Taskcontent-length0字节一起保存。优化后,.NET5每次请求的内存分配仅为330B,减少了92%。优化后不再出现锯齿状图案。这样,当服务器处理100,000个gRPC调用时,垃圾收集将不再运行。从KestrelHTTP/2连接读取HTTP标头支持通过TCP套接字进行并发请求,这是一种称为多路复用的功能。它允许HTTP/2有效地使用连接,但一次只处理一个连接上的一个请求的标头。HTTP/2的HPack标头压缩是有状态的并且依赖于顺序。处理HTTP/2标头是一个瓶颈,所以要尽可能快。优化了HPackDecoder的性能。解码器是一个状态机,它读取传入的HTTP/2HEADER帧。状态机允许Kestrel在帧到达时对其进行解码,但解码器会在解析每个字节后检查状态。另一个问题是语义值,标题名称和值重复多次。本次PR的优化包括:加强解析循环。例如,如果头名称刚刚被解析,值必须在后面。无需检查状态机即可确定下一个状态。跳过所有语义解析。HPack中的文字具有长度前缀。如果您知道接下来的100个字节是语义的,则无需检查每个字节。标记语义的位置并在它的末尾继续解析。避免复制语义字节。以前,文字字节总是在传递给Kestrel之前复制到中间数组。在大多数情况下,这不是必需的,您可以将原始缓冲区切片并将ReadOnlySpan传递给Kestrel。总之,这些更改显着减少了解析标头所需的时间。标头大小几乎不再是一个因素。解码器标记值的开始和结束,然后对该范围进行切片。[基准]publicvoidSmallDecode()=>_decoder.Decode(_smallHeader,endHeaders:true,handler:_noOpHandler);[基准]publicvoidLargeDecode()=>_decoder.Decode(_largeHeader,endHeaders:true,handler:_noOpHandler);结果:header解码后,Kestrel需要对其进行验证和处理。例如,特殊的HTTP/2标头:path和:method需要在HttpRequest.Path和HttpRequest.Method上设置,而其他标头需要转换为字符串并添加到HttpRequest.Headers集合中。Kestrel有一个已知请求标头的概念。已知标头是一些常见的请求标头,这些标头已针对快速设置和获取进行了优化。添加了将HPack静态标头设置为已知标头的更快路径。HPack静态表给出了61个通用头名称和值的数量,可以代替全名ID发送。具有静态表ID的表头可以使用优化路径绕过一些验证并根据其ID快速设置在集合中。添加了对具有名称和值的静态表ID的额外优化。添加了HPack响应压缩在.NET5之前,Kestrel支持读取请求中的HPack压缩标头,但不支持压缩响应标头。标头压缩的明显优势是减少网络使用,但也有性能优势。为压缩标头写入几位比将标头的全名和值编码并写入字节更快。添加了初始HPack静态压缩。静态压缩很简单:如果header在HPack静态表中,写入标识header的ID,而不是长名。动态HPack头压缩更复杂,但也带来更大的收益。响应标头的名称和值在动态表中进行跟踪,并为每个标头分配一个ID。写入响应的标头后,服务器会检查表中的标头名称和值。如果匹配,请写下ID。如果不是,则完整的标头被写入并添加到下一个响应的表中。动态表具有最大大小,因此向其添加标头可能会以先进先出的顺序逐出其他标头。添加了动态HPack标头压缩。为了快速搜索标题,动态表使用基本哈希表对标题条目进行分组。为了跟踪顺序并清理旧标题,维护了一个链接列表。为了避免分配,删除的条目被合并并重新使用。使用Wireshark捕获数据包,您可以在示例中看到标头压缩对gRPC调用的响应大小的影响。.NETCore3.x写了77B,而.NET5只有12B。Protobuf消息序列化gRPCfor.NET使用Google.Protobuf包作为消息的默认序列化程序。Protobuf是一种高效的二进制序列化格式。Google.Protobuf专为性能而设计,使用代码生成而不是反射来序列化.NET对象。可以向其中添加一些现代.NETAPI和功能,以减少分配并提高效率。Google.Protobuf的最大改进是支持现代.NETIO类型:Span、ReadOnlySequence和IBufferWriter。这些类型允许使用Kestrel公开的缓冲区直接序列化gRPC消息。这使得Google.Protobuf在序列化和反序列化Protobuf内容时不必分配中间数组。对Protobuf缓冲区序列化的支持是Microsoft和Google工程师多年的努力。更改分布在多个存储库中。优化对Google.Protobuf缓冲区序列化的支持。这是迄今为止最大和最复杂的变化。Protobuf读取和写入使用许多面向性能的功能和添加到C#和.NETCore的API:Span和C#ref结构类型提供对内存的快速和安全访问。Span表示任意内存的连续区域。使用span允许我们在不使用指针的情况下序列化到托管.NET数组、堆栈分配数组或非托管内存。Span和.NET防止缓冲区溢出。stackalloc用于创建基于堆栈的数组。stackalloc是一个有用的工具,可以在需要较小的缓冲区时避免分配。添加了MemoryMarshal.GetReference()、Unsafe.ReadUnaligned()和Unsafe.WriteUnaligned()等低级方法,以实现原始类型和字节之间的直接转换。BinaryPrimitives具有帮助方法,可在.NET原语和字节之间高效地进行转换。例如,BinaryPrimitives.ReadUInt64读取小数字节并返回一个无符号的64位数字。LittleEndianBinaryPrimitive提供的方法经过优化并使用矢量化。现代C#和.NET的一大优点是您可以在不牺牲内存安全的情况下编写快速、高效的低级库。在性能方面,它可以极大地压榨你的服务器:privateTestMessage_testMessage=CreateMessage();privateReadOnlySequence
