gRPC サービスのバージョン管理

作成者: James Newton-King

アプリに追加された新機能では、クライアントに提供されている gRPC サービスを、たまに予期しない方法または破壊的な方法で変更することが必要になる場合があります。 gRPC サービスが変更される場合は、次のようにします。

  • 変更がクライアントに与える影響について考慮する必要があります。
  • 変更をサポートするためのバージョン管理戦略を実装する必要があります。

下位互換性

gRPC プロトコルは、時間の経過と共に変更されるサービスに対応するように設計されています。 通常、gRPC サービスとメソッドへの追加は非破壊的です。 非破壊的変更では、既存のクライアントは変更なしで動作し続けることが可能です。 gRPC サービスの変更または削除は破壊的変更です。 gRPC サービスに破壊的変更が含まれている場合は、そのサービスを使用しているクライアントを更新して再デプロイする必要があります。

非破壊的変更をサービスに加えることには、いくつかの利点があります。

  • 既存のクライアントは引き続き実行されます。
  • クライアントへの破壊的変更の通知とクライアントの更新にかかわる作業が回避されます。
  • 文書化して管理する必要があるサービスのバージョンは 1 つだけです。

非破壊的変更

次の変更は、gRPC プロトコル レベルと .NET バイナリ レベルで非破壊的です。

  • 新しいサービスの追加
  • サービスへの新しいメソッドの追加
  • 要求メッセージへのフィールドの追加 - 要求メッセージに追加されたフィールドは、設定されていない場合、サーバーの既定値を使用して逆シリアル化されます。 非破壊的変更にするには、新しいフィールドが古いクライアントによって設定されていないときにサービスが成功する必要があります。
  • 応答メッセージへのフィールドの追加 - 応答メッセージに追加されたフィールドは、クライアントでメッセージの不明なフィールドのコレクションに逆シリアル化されます。
  • 列挙型への値の追加 - 列挙型は数値としてシリアル化されます。 新しい列挙値は、クライアントで、列挙型名を指定せずに列挙値に逆シリアル化されます。 非破壊的変更にするには、新しい列挙値を受け取るときに古いクライアントが正しく動作する必要があります。

バイナリの破壊的変更

次の変更は、gRPC プロトコル レベルでは非破壊的ですが、最新の .proto コントラクトまたはクライアントの .NET アセンブリにアップグレードする場合は、クライアントを更新する必要があります。 gRPC ライブラリを NuGet に公開する予定の場合、バイナリの互換性は重要です。

  • フィールドの削除 - 削除されたフィールド の値は、メッセージの不明なフィールドに逆シリアル化されます。 これは gRPC プロトコルの破壊的変更ではありませんが、最新のコントラクトにアップグレードする場合は、クライアントを更新する必要があります。 削除されたフィールド番号が今後誤って再利用されないことが重要です。 これが行われないようにするには、Protobuf の予約キーワードを使用して、削除されたフィールドの番号と名前をメッセージ上に指定します。
  • メッセージ名の変更 - 通常、ネットワーク上ではメッセージ名は送信されないので、これは gRPC プロトコルの破壊的変更ではありません。 最新のコントラクトにアップグレードする場合は、クライアントを更新する必要があります。 ネットワーク上でメッセージ名が送信される状況の 1 つは、Any フィールドを使用する (メッセージ型の識別にメッセージ名を使用する) 場合です。
  • メッセージを入れ子または非入れ子にする - メッセージ型は入れ子にすることができます。 メッセージを入れ子または非入れ子にすると、そのメッセージ名が変更されます。 メッセージ型を入れ子にする方法を変更すると、互換性に対して名前変更と同じ影響があります。
  • csharp_namespace の変更 - csharp_namespace を変更すると、生成された .NET 型の名前空間が変更されます。 これは gRPC プロトコルの破壊的変更ではありませんが、最新のコントラクトにアップグレードする場合は、クライアントを更新する必要があります。

プロトコルの破壊的変更

次の項目は、プロトコルとバイナリの破壊的変更です。

  • フィールド名の変更 - Protobuf コンテンツでは、フィールド名は生成されたコードでのみ使用されます。 ネットワーク上でのフィールドの識別には、フィールド番号が使用されます。 フィールド名の変更は、Protobuf ではプロトコルの破壊的変更ではありません。 ただし、サーバーで JSON コンテンツを使用している場合、フィールド名の変更は破壊的変更となります。
  • フィールドのデータ型の変更 - フィールドのデータ型を互換性のない型に変更すると、メッセージを逆シリアル化しているときにエラーが発生します。 新しいデータ型に互換性がある場合でも、最新のコントラクトにアップグレードすると、新しい型をサポートするためにクライアントを更新する必要が生じる可能性があります。
  • フィールド番号の変更 - Protobuf ペイロードでは、フィールド番号がネットワーク上でのフィールドの識別に使用されます。
  • パッケージ名、サービス名、またはメソッド名の変更 - gRPC では、パッケージ名、サービス名、およびメソッド名を使用して URL を構築します。 クライアントでは、サーバーから UNIMPLEMENTED 状態を取得します。
  • サービスまたはメソッドの削除 - クライアントでは、削除されたメソッドを呼び出すと、サーバーから UNIMPLEMENTED 状態を取得します。

動作の破壊的変更

非破壊的変更を行う場合は、古いクライアントが新しいサービスの動作で続行できるかどうかも検討する必要があります。 たとえば、要求メッセージに新しいフィールドを追加すると、次のようになります。

  • プロトコルの破壊的変更ではありません。
  • 新しいフィールドが設定されていない場合にサーバーでエラー状態を返すと、古いクライアントでは破壊的変更になります。

動作の互換性は、アプリ固有のコードによって決まります。

バージョン番号サービス

サービスは、古いクライアントとの下位互換性を維持する必要があります。 最終的に、アプリに変更を加える場合は、破壊的変更が必要になることがあります。 古いクライアントを中断し、サービスと共に強制的に更新することは、優れたユーザー エクスペリエンスではありません。 破壊的変更を加えながら下位互換性を確保する場合、サービスの複数のバージョンを発行する方法があります。

gRPC では、.NET 名前空間と同様に機能する、省略可能な package 指定子がサポートされています。 実際には、.proto ファイルで option csharp_namespace が設定されていない場合に、生成された .NET 型の .NET 名前空間として package が使用されます。 package を使用すると、サービスとそのメッセージのバージョン番号を指定できます。

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>();
});

パッケージ名にバージョン番号を含めると、v1 バージョンを呼び出す古いクライアントを引き続きサポートしながら、破壊的変更を含む v2 バージョンのサービスを発行できるようになります。 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 型はさまざまです。 ビジネス ロジックを集中管理された場所に移行するには、メッセージを共通の型にマッピングする必要があります。

その他の技術情報