C# · #csharp#dotnet-core#microservices#grpc

gRPC in .NET Core:高性能服务间通信实践

2019.09.03 C# 4 min 1.5k
// 目录 · contents

.NET Core 3.0 对 gRPC 提供了一级支持(Grpc.AspNetCore 包),微软官方的工具链也终于成熟了。我们团队在评估微服务间通信方案时对比了 REST 和 gRPC,最终在内部服务间选择了 gRPC。这篇文章记录完整的实践过程。

1. gRPC vs REST:选型考量

维度 REST gRPC
协议 HTTP/1.1(通常) HTTP/2
序列化 JSON(文本) Protobuf(二进制)
性能 高(约 5-10x)
类型安全 弱(靠文档和约定) 强(IDL 生成代码)
流式传输 有限(SSE) 原生支持(双向流)
浏览器支持 原生 需要 grpc-web 代理
调试友好性 高(curl 即可) 低(二进制协议)

我们的选择:内部服务间用 gRPC(性能 + 强类型),对外暴露的 API 用 REST(浏览器兼容性)。

2. 定义 Protobuf 接口

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
// protos/order_service.proto
syntax = "proto3";

option csharp_namespace = "MyApp.GrpcServices";

package orders;

service OrderService {
// 普通一元 RPC
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc GetOrder (GetOrderRequest) returns (OrderDto);

// 服务端流式:客户端发一个请求,服务端推送多个响应
rpc WatchOrderStatus (WatchOrderRequest) returns (stream OrderStatusUpdate);

// 双向流式
rpc BatchProcess (stream OrderBatchItem) returns (stream BatchResult);
}

message CreateOrderRequest {
int32 user_id = 1;
repeated OrderItem items = 2;
string shipping_address = 3;
}

message OrderItem {
int32 product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}

message CreateOrderResponse {
int32 order_id = 1;
string order_no = 2;
}

message OrderStatusUpdate {
int32 order_id = 1;
string status = 2;
google.protobuf.Timestamp updated_at = 3;
}

3. 服务端实现

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 在 .csproj 中添加 proto 文件引用
// <Protobuf Include="Protos\order_service.proto" GrpcServices="Server" />

// 实现服务
public class OrderGrpcService : OrderService.OrderServiceBase
{
private readonly IOrderService _orderService;
private readonly ILogger<OrderGrpcService> _logger;

public OrderGrpcService(IOrderService orderService,
ILogger<OrderGrpcService> logger)
{
_orderService = orderService;
_logger = logger;
}

public override async Task<CreateOrderResponse> CreateOrder(
CreateOrderRequest request,
ServerCallContext context)
{
// 从 gRPC metadata 中获取认证信息(类似 HTTP Header)
var userId = context.RequestHeaders.GetValue("x-user-id");
_logger.LogInformation("CreateOrder called by user {UserId}", userId);

var order = await _orderService.CreateAsync(new CreateOrderDto
{
UserId = request.UserId,
Items = request.Items.Select(i => new OrderItemDto
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = (decimal)i.UnitPrice
}).ToList()
}, context.CancellationToken);

return new CreateOrderResponse
{
OrderId = order.Id,
OrderNo = order.OrderNo
};
}

// 服务端流:实时推送订单状态更新
public override async Task WatchOrderStatus(
WatchOrderRequest request,
IServerStreamWriter<OrderStatusUpdate> responseStream,
ServerCallContext context)
{
while (!context.CancellationToken.IsCancellationRequested)
{
var status = await _orderService.GetStatusAsync(request.OrderId);

await responseStream.WriteAsync(new OrderStatusUpdate
{
OrderId = request.OrderId,
Status = status.ToString()
});

await Task.Delay(TimeSpan.FromSeconds(2), context.CancellationToken);
}
}
}

// Startup.cs 注册
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.EnableDetailedErrors = env.IsDevelopment();
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
});
}

public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<OrderGrpcService>();
});
}

4. 客户端调用

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
// 在 .csproj 中添加
// <Protobuf Include="Protos\order_service.proto" GrpcServices="Client" />

// 注册 gRPC 客户端(利用 IHttpClientFactory 管理连接)
services.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
options.Address = new Uri("https://order-service:5001");
})
.ConfigureChannel(options =>
{
options.Credentials = ChannelCredentials.SecureSsl;
})
.AddCallCredentials((context, metadata) =>
{
// 自动注入认证 Token
metadata.Add("Authorization", $"Bearer {GetToken()}");
return Task.CompletedTask;
});

// 使用
public class ApiController : ControllerBase
{
private readonly OrderService.OrderServiceClient _orderClient;

public ApiController(OrderService.OrderServiceClient orderClient)
{
_orderClient = orderClient;
}

[HttpPost("orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderApiRequest request)
{
var grpcResponse = await _orderClient.CreateOrderAsync(
new CreateOrderRequest
{
UserId = request.UserId,
Items = { request.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity
})}
},
deadline: DateTime.UtcNow.AddSeconds(5)); // 超时设置

return Ok(new { orderId = grpcResponse.OrderId });
}
}

5. 错误处理

gRPC 有自己的状态码体系,不用 HTTP 状态码:

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
// 服务端抛出 gRPC 异常
public override async Task<OrderDto> GetOrder(
GetOrderRequest request,
ServerCallContext context)
{
var order = await _orderService.FindAsync(request.OrderId);

if (order == null)
{
throw new RpcException(new Status(
StatusCode.NotFound,
$"订单 {request.OrderId} 不存在"));
}

return MapToDto(order);
}

// 客户端捕获
try
{
var order = await _client.GetOrderAsync(new GetOrderRequest { OrderId = 999 });
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
Console.WriteLine($"订单不存在:{ex.Status.Detail}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine("请求超时");
}

踩坑记录

上线初期遇到一个问题:gRPC 连接在负载均衡器后面表现异常,某些请求始终路由到同一个后端实例。

原因:HTTP/2 使用连接复用,负载均衡器(L4 TCP 层)只在建立连接时做负载均衡,之后所有请求都走同一个连接(同一个后端)。普通 HTTP/1.1 每个请求可能建立新连接,L4 负载均衡是生效的。

解决方案: 1. 使用支持 HTTP/2 感知的 L7 负载均衡器(如 Envoy、NGINX 1.13.10+) 2. 或者在客户端做客户端负载均衡(gRPC 支持 pick_firstround_robin 策略)

这个问题让我对 HTTP/2 连接复用的理解深了很多,也直接促使我们后来评估并引入了 Envoy 做服务网格。

总结

.NET Core 3.0 的 gRPC 支持已经相当成熟:

  • Protobuf IDL 定义接口,自动生成强类型客户端/服务端代码
  • 性能优势明显:二进制序列化 + HTTP/2 多路复用
  • 支持四种通信模式:一元、客户端流、服务端流、双向流
  • 注意 HTTP/2 与 L4 负载均衡的兼容性问题,生产环境需要 L7 感知负载均衡

这是我在 .NET 生态里写的最后一批文章,不久之后团队完成了技术栈迁移。这段 C#/.NET 经历让我在接触 Java Spring 时有了很多”哦这个类似于 ASP.NET Core 的 XXX”的直觉,迁移成本比预期低很多。

作者 · authorzt
发布 · date2019-09-03
篇幅 · length1.5k 字 · 4 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论