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:选型考量
| 协议 |
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
| syntax = "proto3";
option csharp_namespace = "MyApp.GrpcServices";
package orders;
service OrderService { 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
|
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) { 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); } } }
public void ConfigureServices(IServiceCollection services) { services.AddGrpc(options => { options.EnableDetailedErrors = env.IsDevelopment(); options.MaxReceiveMessageSize = 16 * 1024 * 1024; }); }
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
|
services.AddGrpcClient<OrderService.OrderServiceClient>(options => { options.Address = new Uri("https://order-service:5001"); }) .ConfigureChannel(options => { options.Credentials = ChannelCredentials.SecureSsl; }) .AddCallCredentials((context, metadata) => { 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
| 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_first 和 round_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”的直觉,迁移成本比预期低很多。