具有期限和取消功能的可靠 gRPC 服務

作者:James Newton-King

期限和取消是 gRPC 用戶端用於中止進行中呼叫的功能。 本文討論期限和取消功能的重要性,以及如何在 .NET gRPC 應用程式中使用這些功能。

期限

期限可讓 gRPC 用戶端指定等待呼叫完成的時間長度。 超過期限時,會取消呼叫。 期限會提供呼叫的執行時間上限,因此設定期限很重要。 它可停止運作異常的服務,避免其無止盡地執行並耗盡伺服器資源。 期限是建置可靠應用程式的實用工具,因此應該設定期限。

期限組態:

  • 期限是進行呼叫時使用 CallOptions.Deadline 來設定。
  • 沒有預設的期限值。 除非指定期限,否則 gRPC 呼叫沒有時間限制。
  • 期限是指超過期限的 UTC 時間。 例如,DateTime.UtcNow.AddSeconds(5) 是從現在起過 5 秒截止的期限。
  • 如果使用過去或目前的時間,呼叫會立即超過期限。
  • 期限會以 gRPC 呼叫傳送至服務,並由用戶端和服務各自獨立追蹤。 gRPC 呼叫有可能在一部電腦上完成,但回應傳回給用戶端時已超過期限。

如果超過期限,用戶端和服務的行為將有所不同:

  • 用戶端會立即中止基礎 HTTP 要求並擲回 DeadlineExceeded 錯誤。 用戶端應用程式可以選擇攔截錯誤,並且向使用者顯示逾時訊息。
  • 在伺服器上,執行中的 HTTP 要求中止並引發 ServerCallCoNtext.CancellationToken。 雖然 HTTP 要求已中止,但 gRPC 呼叫仍會繼續在伺服器上執行,直到方法完成為止。 請務必將取消權杖傳遞至非同步方法,使其隨呼叫一起取消。 例如,將取消權杖傳遞至非同步資料庫查詢和 HTTP 要求。 傳遞取消權杖可讓取消的呼叫在伺服器上快速完成,並釋放資源給其他呼叫使用。

設定 CallOptions.Deadline 以設定 gRPC 呼叫的期限:

var client = new Greet.GreeterClient(channel);

try
{
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = "World" },
        deadline: DateTime.UtcNow.AddSeconds(5));
    
    // Greeting: Hello World
    Console.WriteLine("Greeting: " + response.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
    Console.WriteLine("Greeting timeout.");
}

在 gRPC 服務中使用 ServerCallContext.CancellationToken

public override async Task<HelloReply> SayHello(HelloRequest request,
    ServerCallContext context)
{
    var user = await _databaseContext.GetUserAsync(request.Name,
        context.CancellationToken);

    return new HelloReply { Message = "Hello " + user.DisplayName };
}

期限和重試

若 gRPC 呼叫設定了重試錯誤處理和期限,該期限會追蹤 gRPC 呼叫的所有重試時間。 如果超過期限,gRPC 呼叫會立刻中止基礎 HTTP 要求、略過任何剩餘的重試,並擲回 DeadlineExceeded 錯誤。

散佈期限

從執行中的 gRPC 服務進行 gRPC 呼叫時,應該散佈期限。 例如:

  1. 用戶端應用程式呼叫 FrontendService.GetUser 並指定期限。
  2. FrontendService 會呼叫 UserService.GetUser。 用戶端指定的期限應該在新的 gRPC 呼叫中指定。
  3. UserService.GetUser 接收期限。 如果超過用戶端應用程式的期限,就會如預期地逾時。

呼叫內容會提供期限:ServerCallContext.Deadline

public override async Task<UserResponse> GetUser(UserRequest request,
    ServerCallContext context)
{
    var client = new User.UserServiceClient(_channel);
    var response = await client.GetUserAsync(
        new UserRequest { Id = request.Id },
        deadline: context.Deadline);

    return response;
}

手動散佈期限可能很麻煩。 必須將期限傳遞給每個呼叫,一不小心就很容易遺漏。 gRPC 用戶端處理站提供自動化解決方案。 指定 EnableCallContextPropagation

  • 自動將期限和取消權杖散佈至子呼叫。
  • 如果子呼叫指定較短的期限,就不會散佈期限。 例如,如果子呼叫使用 CallOptions.Deadline 指定 5 秒的新期限,則不會使用散佈的 10 秒期限。 有多個可用期限時,會使用最短的期限。
  • 這是確保複雜的巢狀 gRPC 案例絕對會散佈期限和取消權杖的絕佳方式。
services
    .AddGrpcClient<User.UserServiceClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation();

如需詳細資訊,請參閱 .NET 中的 gRPC 用戶端處理站整合

取消

「取消」可讓 gRPC 用戶端取消不再需要的長時間執行呼叫。 例如,當使用者瀏覽網站上的頁面,串流即時更新的 gRPC 呼叫便會啟動。 一旦使用者離開頁面時,串流應該就會取消。

使用 CallOptions.CancellationToken 傳遞取消權杖或在呼叫中呼叫 Dispose,即可取消用戶端中的 gRPC 呼叫。

private AsyncServerStreamingCall<HelloReply> _call;

public void StartStream()
{
    _call = client.SayHellos(new HelloRequest { Name = "World" });

    // Read response in background task.
    _ = Task.Run(async () =>
    {
        await foreach (var response in _call.ResponseStream.ReadAllAsync())
        {
            Console.WriteLine("Greeting: " + response.Message);
        }
    });
}

public void StopStream()
{
    _call.Dispose();
}

可以取消的 gRPC 服務應該:

  • ServerCallContext.CancellationToken 傳遞到非同步方法。 取消非同步方法可讓伺服器上的呼叫快速完成。
  • 將取消權杖散佈至子呼叫。 散佈取消權杖可確保子呼叫會與父代一起取消。 gRPC 用戶端處理站EnableCallContextPropagation() 會自動散佈取消權杖。

其他資源