使用 gRPC 和 .NET7 构建高性能服务

本文的目的是提供有关已添加到 gRPC .NET 7 框架的性能增强的信息,包括 gRPC JSON
首页 新闻资讯 行业资讯 使用 gRPC 和 .NET7 构建高性能服务

097fd4a3550cced887d97526c7fade8d2e5ed1.png

.NET 7 正式推出标准期限支持,支持期限为 18 个月。 其中包括许多令人兴奋的新功能,包括 Web API、gRPC、ASP.NET 和 C#11 的性能升级。

本文涵盖以下主题:

  1. .NET 7 中的性能改进。

  2. gRPC JSON 转码。

  3. 在 .NET 7 中创建 gRPC 服务。

  4. 在 Postman 使用 gRPC 服务。

  5. 使用服务器反射和 Postman

  6. 添加 Swagger 规范。

除了讨论 .NET 7 中 gRPC 的新特性,我们还将实现一个能够在一分钟内流式传输 500 万条记录的真实微服务。

这是一个快速回顾:

  • gRPC 是由 CNCF 开发的流行的开源 RPC 框架。

  • 作为契约优先、独立于语言的框架,客户端和服务器必须就消息的内容和传递方式达成一致,契约在 .proto 文件中定义,然后使用 .NET7 的工具生成代码。

  • 在单个 tcp 连接上,HTTP/2 支持多路复用,您可以同时发送多个请求。

  • 此外,gRPC 支持数据流,其中服务器可以同时向客户端发送多个响应,反之亦然。

.NET 7 中有哪些新功能?

1、性能改进

为了让 gRPC 支持多路复用,HTTP/2 是必需的。 但是,Kestrel 的 HTTP/2 实现存在一个已知问题,该问题会在连接繁忙时通过 HTTP/2 写入响应时出现瓶颈。 当您在同一个 TCP 连接上同时运行多个请求,但一次只有一个线程能够写入该连接时,就会发生这种情况。 这是通过 .NET 6 中的线程锁完成的,这会导致锁争用。

NET 7 使用一种巧妙的方法来解决此瓶颈,即实现一个队列,该队列会在写入完成时通知所有其他线程,让它们等待写入完成。 因此,性能大大提升,CPU资源得到更好的利用——不再需要争锁。

.NET gRPC 团队的基准测试表明服务器流式处理提高了 800%。

  • .NET 6–0.5M RPS

  • .NET 7–4.5M RPS

HTTP/2 上传速度

通过增加缓冲区大小可将延迟减少 600%。 与 .NET 6 相比,.NET 7 将上传 100MB 文件的时间从 26.9 秒减少到 4.3 秒。

.NET 7 gRPC 的性能现在超过了 Rust、Go 和 C++ 等流行框架。

784ae328036a62895d2003560350a2347516f3.jpg

2、gRPC JSON转码

.NET7 为 ASP.NET Core gRPC 提供了扩展,以使 gRPC 服务能够作为 RESTful Web 服务公开。 您现在可以通过 HTTP 调用 gRPC 方法而无需任何重复。

gRPC JSON 转码支持:

  • HTTP 动词

  • URL参数绑定

  • JSON请求/响应

在此扩展中,HTTP 动词通过使用 protobuf 注释的概念映射到 gRPC 服务,扩展在 ASP.NET Core 应用程序中运行,然后将 JSON 反序列化为 protobuf 消息并直接调用 gRPC 服务,而不必编写自己的 gRPC 客户端应用程序。

我们将在下一节中研究如何实现它。

3、开放API规范

现在有一个 Open API 规范,用于 .NET 7 中的 gRPC JSON 转码,使用以下 Nuget 包:

https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.Swagger

4、 Azure 应用服务支持

最后但同样重要的是,Azure 应用服务现在完全支持 gRPC。 这是在 .NET 中使用 gRPC 构建和部署高性能服务的一大进步。

现在我们已经完成了讨论,让我们实现 gRPC 并看看新功能是什么样的。

f757c6148cee412d2e369625bd47d054bc813b.webp

先决条件:

  • 下载并安装 .NET 7 SDK

  • Visual Studio 2022 17.4+

我们需要做的第一件事是启动 Visual Studio 并创建一个新项目。 选择“ASP.NET Core gRPC 服务”,这将创建一个示例 hello world gRPC 服务。

b4a1d6808380c46a35c402093c7354bb54e5e9.webp

确保选择了 .NET7。

d5a365d39a0ff4d2eb56100dff39a3bd116413.webp

435fe4b21848fbc89d98404549d75cf5e0945d.webp

这将分别在 protos 和服务文件夹中的 GreeterService 中创建一个随时可用的 gRPC 应用程序。

这是一个用作契约的 greeting.proto 文件,定义了客户端将接收的消息和服务。

复制

syntax = "proto3";option csharp_namespace = "gRPCUsingNET7Demo";package greet;// The greeting service definition.service Greeter {  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}// The request message containing the user's name.message HelloRequest {  string name = 1;
}// The response message containing the greetings.message HelloReply {  string message = 1;
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

契约可以被认为是接口,这些接口的实现将由服务定义,在我们的例子中是 GreeterService.cs——这个文件将描述契约的实现。

GreeterService 类是一个标准的 C# 类,它向响应返回 hello。 protobuf 的实际实现是通过代码生成实现的,并使用 GreeterBase 抽象出来。 如果您想确切地知道引擎下发生了什么,您可以转到 GreeterBase,您会在那里找到所有底层细节。

复制

public class GreeterService : Greeter.GreeterBase{private readonly ILogger<GreeterService> _logger;public GreeterService(ILogger<GreeterService> logger){_logger = logger;
        }public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {return Task.FromResult(new HelloReply{Message = "Hello " + request.Name});
        }
    }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

代码生成是 .NET 7 的一项不错的功能,它允许您生成服务器端和客户端 gRPC 代码。 通过设置代码生成设置,可以更改 .CS 项目文件中代码生成过程的行为(例如从服务器到客户端)。

复制

  <ItemGroup><Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>
  • 1.

  • 2.

  • 3.

让我们启动 Kestral 并在打开应用程序后在浏览器中浏览 gRPC 端点。

63d5310602fc3bd012e804afd78bd4e9c7562b.webp

我们无法通过网络访问我们的 gRPC 服务,因为它需要使用 gRPC 客户端。 但是,我们不需要使用 gRPC 客户端,而是使用流行的测试工具 Postman 对其进行测试。 它最近在其功能中添加了对 gRPC 请求的支持。

第一步是打开 Postman 并创建一个新的 gRPC 请求。

418a6c332f220f791099258b2b921d5c34f303.webp

请在下面的框中输入服务器地址(您的应用程序运行的地址)。 例如,https://localhost:7211。

18fa2ce54f08b6de8d65334787351b4f9cded3.webp

Postman目前不了解我们的服务如何运作,因此我们有几个选择。 一种是导入 .proto 文件或使用称为“服务器反射”的东西。 它可以被认为是 gRPC 调用的 OpenAPI 规范。

在 gRPC 服务中启用服务器反射。

按照以下步骤启用服务器反射非常简单。

下载并安装以下 nuget 包:

复制

Install-Package Grpc.AspNetCore.Server.Reflection -Version 2.49.0
  • 1.

2、在Program.cs文件中,需要注册如下服务,并将该服务映射到我们的http管道中,如下:

复制

builder.Services.AddGrpcReflection();app.MapGrpcReflectionService();
  • 1.

  • 2.

  • 3.

现在我们已经完成了所有这些前置需求,让我们回到 Postman,再次运行应用程序。

033b0c39618001bcec84618e61504a891a8180.webp

我们可以看到我们的 greet.greeter 服务和它的 SayHello 方法。

可以通过单击带有 JSON 正文(将由 Postman 转换为 protobuf)的 Invoke 按钮来调用此端点。

380822742669090b72f137eb963262a32df486.webp

在 49 毫秒内得到了服务器响应。

将您的 gRPC 服务转变为 REST

本节将实现 gRPC JSON 转码以通过 HTTP 访问 gRPC。

  1. 将以下 nuget 包添加到您的项目中:

复制

Install-Package Microsoft.AspNetCore.Grpc.JsonTranscoding -Version 7.0.0
  • 1.

2. 导航到 Program.cs 并添加 JSONTranscoding 服务:

复制

builder.Services.AddGrpc().AddJsonTranscoding();
  • 1.

下一步,我们将向您的项目添加两个配置文件。

c24881702cc22e381d1366d59682c4d1a7123d.jpg

添加这些文件后,我们需要修改 greet.proto 并添加 import “google/api/annotations.proto” 以便我们可以注解服务方法。

复制

  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply)
  {   option (google.api.http) =
   {get: "/v1/greeter/{name}"
   }

  };
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

基本上,我们向我们的 RPC 方法添加了一个路由,以便它可以作为 REST 方法被调用。 让我们再次运行应用程序并使用浏览器执行端点。

f4161b673348769c3e6365b4cde1ac4ec1027c.jpg

就是这样! 该 API 现在作为基于 REST 的 API 工作,但它仍然可以作为 gRPC 接口使用。 来自 Postman 的 gRPC 响应如下所示。

82ab41f1291de0c9ad9028c307d04d1de3cec4.webp

添加开放 API 规范

本节的目的是解释我们如何使用 gRPC.Swagger 向我们的应用程序添加开放 API 规范。

  1. 安装以下 nuget 包:

复制

Install-Package Microsoft.AspNetCore.Grpc.Swagger -Version 0.3.0
  • 1.

2.注册Swagger服务和中间件,如下

复制

   builder.Services.AddGrpcSwagger();builder.Services.AddSwaggerGen( c=>{      c.SwaggerDoc("v1",new Microsoft.OpenApi.Models.OpenApiInfo { Title = "gRPC using .NET 7 Demo", Version = "v1" } );
            });
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

最后,您的 program.cs 应该如下所示:

复制

using gRPCUsingNET7Demo.Services;namespace gRPCUsingNET7Demo{public class Program{public static void Main(string[] args)
        {var builder = WebApplication.CreateBuilder(args);// Additional configuration is required to successfully run gRPC on macOS.// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682// Add services to the container.builder.Services.AddGrpc().AddJsonTranscoding();builder.Services.AddGrpcReflection();builder.Services.AddGrpcSwagger();builder.Services.AddSwaggerGen( c=>{c.SwaggerDoc("v1",new Microsoft.OpenApi.Models.OpenApiInfo { Title = "gRPC using .NET 7 Demo", Version = "v1" }

            });var app = builder.Build();app.UseSwagger();app.UseSwaggerUI(c{c.SwaggerEndpoint("/swagger/v1/swagger.json", "gRPC using .NET7 Demo");
            }
            );// Configure the HTTP request pipeline.app.MapGrpcService<GreeterService>();app.MapGrpcReflectionService();app.MapGet("/", () "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");app.Run();
        }
    }
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

启动应用程序后调用 Swagger 端点:
https://localhost:7211/swagger/index.html。

e5ddccb86c1ccf7e72001801ae4fa1a0d54961.webp

您可以像调用任何 Restful API 一样尝试调用端点。

7697933597554beae54565190ba2ef8440cf98.webp

在本节之后,我们将演示如何使用 gRPC 服务器流将 5M 记录(大约 600MB 数据)使用流式传输到客户端。

gRPC 服务器流

在服务器流中,gRPC 客户端发送请求并获取响应流。 客户端读取这些响应,直到所有消息都已传递。 gRPC 确保消息排序。

使用此示例 CSV 文件作为示例。

该 CSV 文件包含大约 500 万条销售记录,因此不可能在一个调用中将它们全部发送出去。

此外,传统的基于 REST 的分页涉及多个客户端请求,并且需要在客户端和服务器之间来回通信。

gRPC Server streaming 是解决这个问题的绝佳方案。

  • 客户端将简单地调用服务方法。

  • CSV 文件将逐行读取,转换为原型模型,然后使用 StreamReader 发送回客户端。

  • 响应流将被发送到客户端。

我们将从定义一个原型文件开始:

148674a779c7da62aa3842b3df33a925b37f20.webp

Protos-> sales.proto

复制

syntax = "proto3";import "google/protobuf/timestamp.proto"; csharp_namespace = "gRPCUsingNET7Demo";package sales;service SalesService {rpc GetSalesData(Request) returns (stream SalesDataModel) {}
}message Request{string filters=1;

}message SalesDataModel {  int32 OrderID = 1;  string Region = 2;  
  string Country = 3;  string ItemType=4;  google.protobuf.Timestamp OrderDate=5;  google.protobuf.Timestamp ShipDate=6;  int32 UnitsSold=7;  float UnitCost=8;  float UnitPrice=9;  int32 TotalRevenue=10;  int32 TotalCost=11;  int32 TotalProfit=12;
  }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

使用 stream 关键字,我们可以指定 SalesDataModel 将作为流传递。

我们的下一步是通过以下方式添加一个新服务——SalesDataService.cs:

复制

using Grpc.Core;using gRPCUsingNET7Demo;namespace gRPCUsingNET7Demo.Services{public class SalesDataService : SalesService.SalesServiceBase{public override async TaskGetSalesData(Request request,IServerStreamWriter<SalesDataModel> responseStream, ServerCallContext context){using (var reader = new StreamReader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data", "sales_records.csv")))
            {string line; bool isFirstLine = true;while ((line = reader.ReadLine()) != null)
                {var pieces = line.Split(',');var _model = new SalesDataModel();try{if (isFirstLine)
                        {isFirstLine = false;continue;
                        }_model.Region = pieces[0];_model.Country = pieces[1];_model.OrderID = int.TryParse(pieces[6], out int _orderID) ? _orderID : 0;_model.UnitPrice = float.TryParse(pieces[9], out float _unitPrice) ? _unitPrice : 0;_model.ShipDate = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime ((DateTime.TryParse(pieces[7], out DateTime _dateShip) ? _dateShip : DateTime.MinValue).ToUniversalTime());_model.UnitsSold = int.TryParse(pieces[8], out int _unitsSold) ? _unitsSold : 0;_model.UnitCost = float.TryParse(pieces[10], out float _unitCost) ? _unitCost : 0;_model.TotalRevenue = int.TryParse(pieces[11], out int _totalRevenue) ? _totalRevenue : 0;_model.TotalCost = int.TryParse(pieces[13], out int _totalCost) ? _totalCost : 0;await responseStream.WriteAsync(_model);

                    }catch (Exception ex)
                    {throw new RpcException(new Status(StatusCode.Internal, ex.ToString()));
                    }

                }
            }

        }

    }
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

  • 48.

  • 49.

  • 50.

  • 51.

  • 52.

  • 53.

  • 54.

  • 55.

  • 56.

  • 57.

  • 58.

  • 59.

  • 60.

  • 61.

此服务实现 SalesServiceBase 类,该类由 .NET7 工具使用 proto 文件自动生成。

它只是重写 GetSalesData 以逐行从文件中读取数据并将其作为流返回。

复制

await responseStream.WriteAsync(_model);
  • 1.

让我们构建项目并运行应用程序。

9174c617632c8fbdd9c550417f541fd384f506.webp

应用程序按预期运行。 要从服务器获取订单流,我们需要创建一个单独的 RPC 客户端,这将在下一节中介绍。

使用 .NET7 创建 gRPC 客户端

让我们在您的解决方案中创建一个新的控制台应用程序,并向其中添加以下包

复制

<PackageReference Include="Google.Protobuf" Version="3.21.9" />
  <PackageReference Include="Grpc.Net.ClientFactory" Version="2.49.0" />
  <PackageReference Include="Grpc.Tools" Version="2.40.0">
   <PrivateAssets>all</PrivateAssets>
   <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  1. 确保添加了 Protos 文件夹并将 sales.proto 文件复制到那里。

  2. 为了为客户端生成 gRPC 类,您需要修改 .csproj 文件。

复制

<ItemGroup>
  <Protobuf Include="Protos\sales.proto" GrpcServices="Client" />
 </ItemGroup>
  • 1.

  • 2.

  • 3.

3. 保存并构建项目(以便生成客户端代码)

4. 第一步是打开 Program.cs 并为您的 gRPC 服务创建一个通道。

复制

var channel = GrpcChannel.ForAddress("https://localhost:7211");
  • 1.

5. 创建一个新的SalesService对象(使用gRPC工具创建)如下:

复制

var client = new SalesService.SalesServiceClient(channel);
  • 1.

6.服务方法应按如下方式调用:

复制

using var call = client.GetSalesData(new Request { Filters = "" });
  • 1.

7. 我们的代码只是调用服务器上的 ReadAllAsync 来检索流,然后在收到流后立即在控制台上打印输出。

复制

await foreach (var each in call.ResponseStream.ReadAllAsync())
{  Console.WriteLine(String.Format("New Order Receieved from {0}-{1},Order ID = {2}, Unit Price ={3}, Ship Date={4}", each.Country, each.Region, each.OrderID, each.UnitPrice,each.ShipDate));  Count++;
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

这就是完整实现:

复制

using Grpc.Core;using Grpc.Net.Client;using gRPCUsingNET7Demo;namespace gRPCClient{internal class Program{static async Task Main(string[] args)
        {var channel = GrpcChannel.ForAddress("https://localhost:7211");int Count = 0;var watch = System.Diagnostics.Stopwatch.StartNew();try{var client = new SalesService.SalesServiceClient(channel);using var call = client.GetSalesData(new Request { Filters = "" }
                  , deadline: DateTime.UtcNow.AddMinutes(10)
                );await foreach (var each in call.ResponseStream.ReadAllAsync())
                {Console.WriteLine(String.Format("New Order Receieved from {0}-{1},Order ID = {2}, Unit Price ={3}, Ship Date={4}", each.Country, each.Region, each.OrderID, each.UnitPrice, each.ShipDate));Count++;

                }
            }catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
            {Console.WriteLine("Service timeout.");
            }watch.Stop();Console.WriteLine($"Stream ended: Total Records:{Count.ToString()} in {watch.Elapsed.TotalMinutes} minutes and {watch.Elapsed.TotalSeconds});Console.Read();

        }
    }
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

正如您在上面的示例中看到的,服务方法调用是在deadline的帮助下完成的。 您可以使用deadline指定通话的持续时间,这样您就可以指定通话应该持续多长时间。

复制

using var call = client.GetSalesData(new Request { Filters = "" }
, deadline: DateTime.UtcNow.AddMinutes(10)
);
  • 1.

  • 2.

  • 3.

客户端现在允许您查看来自 gRPC 服务的传入消息。

e1549a4683c4cf34ae7000f44f499545d0db99.webp

结论:

本文的目的是提供有关已添加到 gRPC .NET 7 框架的性能增强的信息,包括 gRPC JSON 转码功能、OpenAPI 规范和服务器反射功能,以及新的性能改进。 本文还解释了如何使用 gRPC 服务器流式处理来创建能够立即处理和交付数百万条记录的高性能服务。

35    2023-01-11 15:17:01    gRPC .NET 7