共用方式為


Versioning gRPC 服務

作者:James Newton-King

新增至應用程式的新功能可能需要提供給用戶端的 gRPC 服務來進行變更,有時是使用非預期和中斷的方式來進行變更。 gRPC 服務變更時:

  • 應該考慮變更對用戶端的影響。
  • 應該實作支援變更的版本控制策略。

回溯相容性

gRPC 通訊協定的設計目的是支援會隨著時間變更的服務。 一般而言,gRPC 服務和方法的新增為非中斷性。 非中斷性變更可讓現有用戶端在沒有變更的情況下繼續運作。 變更或刪除 gRPC 服務是中斷性變更。 gRPC 服務有中斷性變更時,必須更新和重新部署使用該服務的用戶端。

對服務進行非中斷性變更有一些優點:

  • 現有用戶端會繼續執行。
  • 請避免涉及與通知用戶端有關中斷性變更的工作,並進行更新。
  • 只需要記載和維護服務的一個版本。

非中斷性變更

這些變更在 gRPC 通訊協定層級和 .NET 二進位層級為非中斷性。

  • 新增服務
  • 將新方法新增至服務
  • 將欄位新增至要求訊息 - 未設定預設值時,會使用伺服器上的預設值來還原序列化新增至要求訊息的欄位。 若要成為非中斷性變更,舊版用戶端未設定新欄位時,服務必須成功。
  • 將欄位新增至回應訊息 - 新增至回應訊息的欄位會還原序列化至用戶端上訊息的未知欄位集合。
  • 將值新增至列舉 - 列舉會序列化為數值。 新的列舉值會在用戶端上還原序列化為沒有列舉名稱的列舉值。 若要成為非中斷性變更,舊版用戶端必須在收到新的列舉值時正確執行。

二進位中斷性變更

下列變更在 gRPC 通訊協定層級為非中斷性,但如果用戶端升級至最新的 .proto 合約或用戶端 .NET 組件,則需要更新用戶端。 如果您打算將 gRPC 程式庫發佈至 NuGet,則二進位相容性十分重要。

  • 移除欄位 - 已移除欄位中的值會還原序列化為訊息的未知欄位。 這不是 gRPC 通訊協定中斷性變更,但如果用戶端升級至最新合約,則需要更新用戶端。 請務必不要在未來意外重複使用已移除的欄位編號。 若要確保這不會發生,請使用 Protobuf 的保留關鍵字,以在訊息上指定已刪除的欄位編號和名稱。
  • 重新命名訊息 - 通常不會在網路上傳送訊息名稱,因此這不是 gRPC 通訊協定中斷性變更。 如果用戶端升級至最新合約,則需要更新用戶端。 使用訊息名稱來識別訊息類型時,在網路上「傳送」訊息名稱的其中一種情況是使用任何欄位。
  • 巢狀處理或取消巢狀處理訊息 - 可以巢狀處理訊息類型。 巢狀處理或取消巢狀處理訊息會變更其訊息名稱。 變更訊息類型巢狀處理方式對相容性的影響與重新命名相同。
  • 變更 csharp_namespace - 變更 csharp_namespace 將會變更所產生 .NET 類型的命名空間。 這不是 gRPC 通訊協定中斷性變更,但如果用戶端升級至最新合約,則需要更新用戶端。

通訊協定中斷性變更

下列項目是通訊協定和二進位中斷性變更:

  • 重新命名欄位 - 使用 Protobuf 內容時,欄位名稱只會用於所產生的程式碼。 欄位編號用來識別網路上的欄位。 重新命名欄位不是 Protobuf 的通訊協定中斷性變更。 不過,如果伺服器使用 JSON 內容,則重新命名欄位是中斷性變更。
  • 變更欄位資料類型 - 將欄位的資料類型變更為不相容類型將會導致還原序列化訊息時發生錯誤。 即使新的資料類型相容,如果用戶端升級至最新合約,則可能還是需要更新用戶端以支援新的類型。
  • 變更網域號碼 - 使用 Protobuf 承載時,會使用欄位編號來識別網路上的欄位。
  • 重新命名套件、服務或方法 - gRPC 使用套件名稱、服務名稱和方法名稱來建置 URL。 用戶端會從伺服器取得「未實作」狀態。
  • 移除服務或方法 - 呼叫已移除的方法時,用戶端會從伺服器取得「未實作」狀態。

行為中斷性變更

進行非中斷性變更時,您也必須考慮舊版用戶端是否可以繼續使用新的服務行為。 例如,將新欄位新增至要求訊息:

  • 不是通訊協定中斷性變更。
  • 如果未設定新欄位,則傳回伺服器上的錯誤狀態會使其成為舊用戶端的中斷性變更。

行為相容性是由您的應用程式特定程式碼所決定。

版本號碼服務

服務應該努力保持與舊用戶端的回溯相容。 您應用程式的變更最終可能需要中斷性變更。 中斷舊用戶端,並強制對其與您的服務進行更新,並不是好的使用者體驗。 進行中斷性變更時維護回溯相容性的方式,是發佈服務的多個版本。

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 類型。 將商務邏輯移至集中式位置需要將訊息對應至一般類型。

其他資源