版本控制 gRPC 服务

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

作者:James Newton-King

添加到应用的新功能可能要求提供给客户端的 gRPC 服务进行更改(有时要求该服务以意想不到、打破常规的方式进行更改)。 gRPC 服务更改时:

  • 应考虑更改会如何影响客户端。
  • 应实现支持更改的版本控制策略。

向后兼容性

gRPC 协议旨在支持随时间变化的服务。 通常,gRPC 服务和方法会不中断地新增内容。 非中断性变更允许现有客户端继续工作而不做任何变更。 更改或删除 gRPC 服务是中断性变更。 gRPC 服务发生中断性变更时,必须更新和重新部署使用该服务的客户端。

对服务进行非中断性变更有许多好处:

  • 现有客户端可继续运行。
  • 避免向客户端通知中断性变更并进行更新。
  • 只需要记录和维护服务的一个版本。

非重大变化

在 gRPC 协议级别和 .NET 二进制级别,这些变更不会中断。

  • 添加新服务
  • 向服务中添加新方法
  • 将字段添加到请求消息 - 添加到请求消息的字段将在服务器上通过默认值(若未设置)进行反序列化。 若要实现非中断性变更,当新字段不是由旧客户端设置时,服务必须成功。
  • 向响应消息添加字段 - 如果旧客户端尚未更新新字段,则该值将被反序列化到响应消息的未知字段集合中。
  • 向枚举添加值 - 枚举被序列化为数值。 新的枚举值在客户端反序列化为没有枚举名的枚举值。 若要实现非中断性变更,旧客户端在接收新枚举值时必须正确运行。

二进制中断性变更

以下变更在 gRPC 协议级别是非中断性变更,但如果客户端升级到最新的 .proto 协定或客户端 .NET 程序集,则需要对其进行更新。 如果你计划将 gRPC 库发布到 NuGet,二进制兼容性很重要。

  • 删除字段 - 已删除字段中的值被反序列化为消息的未知字段。 这并不是 gRPC 协议中断性变更,但如果客户端升级到最新的协定,则需要对其进行更新。 删除的字段编号不会在将来被意外重用,这点很重要。 若要确保不会发生这种情况,请使用 Protobuf 的保留关键字指定邮件上已删除的字段编号和名称。
  • 重命名消息 - 消息名称通常不会在网络上发送,因此这不是 gRPC 协议中断性变更。 如果客户端升级到最新的协定,则需要对其进行更新。 当消息名称用于标识消息类型时,任何字段都会出现消息名称在网络上发送的情况。
  • 嵌套或取消嵌套消息 - 消息类型可以嵌套。 嵌套或取消嵌套消息将更改其消息名称。 更改消息类型的嵌套方式对兼容性的影响与重命名相同。
  • 更改 csharp_namespace - 更改 将更改所生成的 .NET 类型的命名空间。 这并不是 gRPC 协议中断性变更,但如果客户端升级到最新的协定,则需要对其进行更新。

协议中断性变更

以下各项是协议和二进制的中断性变更:

  • 重命名字段 - 对于 Protobuf 内容,字段名只在生成的代码中使用。 字段编号用于标识网络上的字段。 对 Protobuf 来说,重命名字段不是协议中断性变更。 但是,如果服务器正在使用 JSON 内容,则重命名字段是一个中断性变更。
  • 更改字段数据类型 - 将字段的数据类型更改为不兼容类型将在反序列化消息时导致错误。 即使新的数据类型是兼容的,但如果客户端升级到最新的协定,它也可能需要更新以支持新的类型。
  • 更改字段编号 - 对于 Protobuf 有效负载,字段编号用于标识网络上的字段。
  • 重命名包、服务或方法 - gRPC 使用包名、服务名和方法名来生成 URL。 客户端从服务器获取 UNIMPLEMENTED 状态。
  • 删除服务或方法 - 客户端在调用已删除的方法时从服务器获取 UNIMPLEMENTED 状态。

行为中断性变更

在进行非中断性更改时,还必须考虑旧客户端是否可以继续使用新的服务行为。 例如,向请求消息添加新字段:

  • 它不是协议中断性变更。
  • 如果未设置新字段,则在服务器上返回错误状态对于旧客户端来说是一个中断性变更。

行为兼容性由应用特定的代码决定。

版本号服务

服务应尽量保持与旧客户端的后向兼容。 最终对应用的更改可能需要进行中断性变更。 中断旧客户端并强制其随服务一起更新不是一种好的用户体验。 若要在进行中断性变更的同时保持后向兼容性,一种方法是发布服务的多个版本。

gRPC 支持可选的说明符,它的功能非常类似于 .NET 命名空间。 实际上,如果 .proto 文件中未设置 option csharp_namespace,则将 package 用作生成的 .NET 类型的 .NET 命名空间。 该包可用于指定服务的版本号及其消息:

syntax = "proto3";

package greet.v1;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

包名称与服务名称相结合以标识服务地址。 服务地址允许并行托管服务的多个版本:

  • greet.v1.Greeter
  • greet.v2.Greeter

已进行版本控制的服务的实现在 Startup.cs 中注册:

app.UseEndpoints(endpoints =>
{
    // Implements greet.v1.Greeter
    endpoints.MapGrpcService<GreeterServiceV1>();

    // Implements greet.v2.Greeter
    endpoints.MapGrpcService<GreeterServiceV2>();
});

通过在包名称中包含版本号,你可发布具有中断性变更的服务 v2 版本,同时继续支持调用 v1 版本的旧客户端 。 更新客户端以使用 v2 服务后,你可选择删除旧版本。 计划发布服务的多个版本时:

  • 如果合理,请避免中断性变更。
  • 除非进行中断性更改,否则请勿更新版本号。
  • 进行中断性变更时,请务必更新版本号。

发布多个服务版本会使其重复。 若要减少重复,请考虑将业务逻辑从服务实现移动到可由新旧实现重用的集中位置:

using Greet.V1;
using Grpc.Core;
using System.Threading.Tasks;

namespace Services
{
    public class GreeterServiceV1 : Greeter.GreeterBase
    {
        private readonly IGreeter _greeter;
        public GreeterServiceV1(IGreeter greeter)
        {
            _greeter = greeter;
        }

        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = _greeter.GetHelloMessage(request.Name)
            });
        }
    }
}

使用不同包名称生成的服务和消息属于不同的 .NET 类型。 将业务逻辑移动到集中位置需要将消息映射到常见类型。

其他资源