<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
有关垃圾回收的详细信息,请参阅工作站和服务器垃圾回收。
ASP.NET Core 应用默认使用服务器 GC。 启用 <ServerGarbageCollection>
仅对非服务器 gRPC 客户端应用有用,例如在 gRPC 客户端控制台应用中。
一些负载均衡器不能与 gRPC 一起高效工作。 通过在终结点之间分布 TCP 连接,L4(传输)负载均衡器在连接级别上运行。 这种方法非常适合使用 HTTP / 1.1 进行的负载均衡 API 调用。 使用 HTTP/1.1 进行的并发调用在不同的连接上发送,实现调用在终结点之间的负载均衡。
由于 L4 负载均衡器是在连接级别运行的,它们不太适用于 gRPC。 GRPC 使用 HTTP/2,在单个 TCP 连接上多路复用多个调用。 通过该连接的所有 gRPC 调用都将前往一个终结点。
有两种方法可以高效地对 gRPC 进行负载均衡:
客户端负载均衡
L7(应用程序)代理负载均衡
只有 gRPC 调用可以在终结点之间进行负载均衡。 一旦建立了流式 gRPC 调用,通过流发送的所有消息都将前往一个终结点。
客户端负载均衡
对于客户端负载均衡,客户端了解终结点。 对于每个 gRPC 调用,客户端会选择一个不同的终结点作为将该调用发送到的目的地。 如果延迟很重要,那么客户端负载均衡是一个很好的选择。 客户端和服务之间没有代理,因此调用直接发送到服务。 客户端负载均衡的缺点是每个客户端必须跟踪它应该使用的可用终结点。
Lookaside 客户端负载均衡是一种将负载均衡状态存储在中心位置的技术。 客户端定期查询中心位置以获取在作出负载均衡决策时要使用的信息。
有关详细信息,请参阅 gRPC 客户端负载均衡。
代理负载均衡
L7(应用程序)代理的工作级别高于 L4(传输)代理。 L7 代理理解 HTTP/2。 代理在一个 HTTP/2 连接上接收多路复用的 gRPC 调用,并将它们分发到多个后端终结点上。 使用代理比客户端负载均衡更简单,但会增加 gRPC 调用的额外延迟。
有很多 L7 代理可用。 一些选项包括:
Envoy - 一种常用的开源代理。
Linkerd - Kubernetes 服务网格。
YARP:另一种反向代理 - 用 .NET 编写的开源代理。
进程内通信
客户端和服务之间的 gRPC 调用通常通过 TCP 套接字发送。 TCP 非常适用于网络中的通信,但当客户端和服务在同一台计算机上时,进程间通信 (IPC) 的效率更高。
考虑在同一台计算机上的进程之间使用 Unix 域套接字或命名管道之类的传输进行 gRPC 调用。 有关详细信息,请参阅使用 gRPC 进行进程内通信。
保持活动 ping
保持活动 ping 可用于在非活动期间使 HTTP/2 连接保持为活动状态。 如果在应用恢复活动时已准备好现有 HTTP/2 连接,则可以快速进行初始 gRPC 调用,而不会因重新建立连接而导致延迟。
在 SocketsHttpHandler 上配置保持活动 ping:
var handler = new SocketsHttpHandler
PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
EnableMultipleHttp2Connections = true
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
HttpHandler = handler
前面的代码配置了一个通道,该通道在非活动期间每 60 秒向服务器发送一次保持活动 ping。 ping 确保服务器和使用中的任何代理不会由于不活动而关闭连接。
HTTP/2 流量控制是一项防止应用被数据阻塞的功能。 使用流量控制时:
每个 HTTP/2 连接和请求都有可用的缓冲区窗口。 缓冲区窗口是应用一次可以接收的数据量。
如果填充缓冲区窗口,流量控制功能将激活。 激活后,发送应用会暂停发送更多数据。
接收应用处理完数据后,缓冲区窗口中的空间将变为可用。 发送应用将恢复发送数据。
流量控制在接收大消息时可能会对性能产生负面影响。 如果缓冲区窗口小于传入消息有效负载或客户端和服务器之间出现延迟,则可以在启动/停止突发中发送数据。
流量控制性能问题可以通过增加缓冲区窗口大小来解决。 在 Kestrel 中,这是在应用启动时使用 InitialConnectionWindowSize 和 InitialStreamWindowSize 配置的:
builder.WebHost.ConfigureKestrel(options =>
var http2 = options.Limits.Http2;
http2.InitialConnectionWindowSize = 1024 * 1024 * 2; // 2 MB
http2.InitialStreamWindowSize = 1024 * 1024; // 1 MB
如果 gRPC 服务通常接收大于 96 KB 的消息,即 Kestrel 的默认流窗口大小,请考虑增加连接和流窗口大小。
连接窗口大小应始终等于或大于流窗口大小。 流是连接的一部分,发送方受到两者的限制。
有关流量控制工作原理的详细信息,请参阅 HTTP/2 流量控制(博客文章)。
增加 Kestrel 的窗口大小允许 Kestrel 代表应用缓冲更多数据,这可能会增加内存使用量。 避免配置不必要的大型窗口大小。
在高性能方案中,可使用 gRPC 双向流式处理取代一元 gRPC 调用。 双向流启动后,来回流式处理消息比使用多个一元 gRPC 调用发送消息更快。 流式处理消息作为现有 HTTP/2 请求上的数据发送,节省了为每个一元调用创建新的 HTTP/2 请求的开销。
示例服务:
public override async Task SayHello(IAsyncStreamReader<HelloRequest> requestStream,
IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
await foreach (var request in requestStream.ReadAllAsync())
var helloReply = new HelloReply { Message = "Hello " + request.Name };
await responseStream.WriteAsync(helloReply);
示例客户端:
var client = new Greet.GreeterClient(channel);
using var call = client.SayHello();
Console.WriteLine("Type a name then press enter.");
while (true)
var text = Console.ReadLine();
// Send and receive messages over the stream
await call.RequestStream.WriteAsync(new HelloRequest { Name = text });
await call.ResponseStream.MoveNext();
Console.WriteLine($"Greeting: {call.ResponseStream.Current.Message}");
将一元调用替换为双向流式处理是一种高级技术,由于性能原因,这在许多情况下并不适用。
有以下情况时,使用流式处理调用是一个不错的选择:
需要高吞吐量或低延迟。
gRPC 和 HTTP/2 被标识为性能瓶颈。
客户端的辅助程序使用 gRPC 服务发送或接收常规消息。
请注意使用流式处理调用而不是一元调用的其他复杂性和限制:
流可能会因服务或连接错误而中断。 需要在出现错误时重启流的逻辑。
对于多线程处理,RequestStream.WriteAsync
并不安全。 一次只能将一条消息写入流中。 通过单个流从多个线程发送消息需要制造者/使用者队列(如 Channel<T>)来整理消息。
gRPC 流式处理方法仅限于接收一种类型的消息并发送一种类型的消息。 例如,rpc StreamingCall(stream RequestMessage) returns (stream ResponseMessage)
接收 RequestMessage
并发送 ResponseMessage
。 Protobuf 对使用 Any
和 oneof
支持未知消息或条件消息,可以解决此限制。
二进制有效负载
Protobuf 支持标量值类型为 bytes
的二进制有效负载。 C# 中生成的属性使用 ByteString
作为属性类型。
syntax = "proto3";
message PayloadResponse {
bytes data = 1;
Protobuf 是一种二进制格式,它以最小开销有效地序列化大型二进制有效负载。 基于文本的格式(如 JSON)需要将字节编码为 base64,并将 33% 添加到消息大小。
使用大型 ByteString
有效负载时,有一些最佳做法可以避免下面所讨论的不必要副本和分配。
发送二进制有效负载
ByteString
实例通常使用 ByteString.CopyFrom(byte[] data)
创建。 此方法会分配新的 ByteString
和新的 byte[]
。 数据会复制到新的字节数组中。
通过使用 UnsafeByteOperations.UnsafeWrap(ReadOnlyMemory<byte> bytes)
创建 ByteString
实例,可以避免其他分配和复制操作。
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = UnsafeByteOperations.UnsafeWrap(data);
字节不会通过 UnsafeByteOperations.UnsafeWrap
进行复制,因此在使用 ByteString
时,不得修改字节。
UnsafeByteOperations.UnsafeWrap
要求使用 Google.Protobuf 版本 3.15.0 或更高版本。
读取二进制有效负载
通过使用 ByteString.Memory
和 ByteString.Span
属性,可以有效地从 ByteString
实例读取数据。
var byteString = UnsafeByteOperations.UnsafeWrap(new byte[] { 0, 1, 2 });
var data = byteString.Span;
for (var i = 0; i < data.Length; i++)
Console.WriteLine(data[i]);
这些属性允许代码直接从 ByteString
读取数据,而无需分配或副本。
大多数 .NET API 具有 ReadOnlyMemory<byte>
和 byte[]
重载,因此建议使用 ByteString.Memory
来使用基础数据。 但是,在某些情况下,应用可能需要将数据作为字节数组获取。 如果需要字节数组,则 MemoryMarshal.TryGetArray 方法可用于从 ByteString
获取数组,而无需分配数据的新副本。
var byteString = GetByteString();
ByteArrayContent content;
if (MemoryMarshal.TryGetArray(byteString.Memory, out var segment))
// Success. Use the ByteString's underlying array.
content = new ByteArrayContent(segment.Array, segment.Offset, segment.Count);
// TryGetArray didn't succeed. Fall back to creating a copy of the data with ToByteArray.
content = new ByteArrayContent(byteString.ToByteArray());
var httpRequest = new HttpRequestMessage();
httpRequest.Content = content;
前面的代码:
尝试使用 MemoryMarshal.TryGetArray 从 ByteString.Memory
获取数组。
如果成功检索,则使用 ArraySegment<byte>
。 段具有对数组、偏移和计数的引用。
否则,将回退到使用 ByteString.ToByteArray()
分配新数组。
gRPC 服务和大型二进制有效负载
gRPC 和 Protobuf 可以发送和接收大型二进制有效负载。 尽管二进制 Protobuf 在序列化二进制有效负载时比基于文本的 JSON 更有效,但在处理大型二进制有效负载时仍然需要牢记重要的性能特征。
gRPC 是一个基于消息的 RPC 框架,这意味着:
在 gRPC 可以发送整个消息之前,将整个消息加载到内存中。
收到消息后,整个消息将反序列化为内存。
二进制有效负载被分配为字节数组。 例如,10 MB 二进制有效负载分配了一个 10 MB 的字节数组。 具有大型二进制有效负载的消息可以在大型对象堆上分配字节数组。 大型分配会影响服务器性能和可伸缩性。
有关创建具有大型二进制有效负载的高性能应用程序的建议:
在 gRPC 消息中避免大型二进制有效负载。 大于 85,000 字节的字节数组被视为大型对象。 保持低于该大小可以避免在大型对象堆上分配。
考虑使用 gRPC 流式传输拆分大型二进制有效负载。 二进制数据通过多条消息进行分块和流式传输。 有关如何流式处理文件的更多信息,请参阅 grpc-dotnet 存储库中的示例: