C++ 用戶端程式庫中的重試政策

本頁說明 C++ 用戶端程式庫使用的重試模型。

用戶端程式庫會代表您發出 RPC (遠端程序呼叫)。這些 RPC 可能會因暫時性錯誤而失敗。伺服器重新啟動、負載平衡器關閉超載或閒置連線,以及速率限制生效等情況,都可能導致暫時性故障。

程式庫可能會將這些錯誤傳回應用程式。不過,程式庫很容易處理許多這類錯誤,因此應用程式程式碼會更簡單。

可重試的錯誤和作業

只有暫時性錯誤可以重試。舉例來說,kUnavailable 表示用戶端無法連線,或是在要求進行期間與服務中斷連線。這幾乎都是暫時性情況,但可能需要很長時間才能復原。這些錯誤一律可以重試 (前提是作業本身可以安全地重試)。在合約中,kPermissionDenied 錯誤 需要額外介入 (通常是人為介入) 才能解決。這類錯誤不屬於「暫時性」錯誤,至少在用戶端程式庫的重試迴圈考量的時間範圍內,不屬於暫時性錯誤。

同樣地,無論錯誤性質為何,部分作業都不適合重試。包括進行漸進式變更的任何作業。舉例來說,如果名為「X」的資源有多個版本,就不適合重試移除「X 的最新版本」的作業。這是因為呼叫端可能打算移除單一版本,而重試這類要求可能會導致所有版本都遭到移除。

設定重試迴圈

用戶端程式庫接受三種不同的設定參數,可控制重試迴圈:

  • *IdempotencyPolicy 可判斷特定要求是否為等冪要求。系統只會重試這類要求。
  • *RetryPolicy 會判斷 (a) 錯誤是否應視為暫時性失敗,以及 (b) 用戶端程式庫重試要求的時間長度 (或次數)。
  • *BackoffPolicy 會決定用戶端程式庫等待多久後,再重新發出要求。

預設冪等性政策

一般來說,如果成功呼叫函式多次後,系統的狀態與成功呼叫函式一次時相同,則該作業為冪等。只有冪等作業可以安全地重試。冪等作業的例子包括但不限於所有唯讀作業,以及只能成功一次的作業。

根據預設,用戶端程式庫只會將透過 GETPUT 動詞實作的 RPC 視為等冪。這可能過於保守,在某些服務中,即使部分 POST 要求是等冪,您隨時可以覆寫預設的等冪政策,以更符合自身需求。

部分作業只有在包含前提條件時,才會是等冪作業。舉例來說,「移除最新版本 (如果最新版本是 Y)」是冪等作業,因為只能成功一次。

用戶端程式庫會不時進行改良,將更多作業視為等冪作業。我們將這些改良視為修正錯誤,因此即使會改變用戶端程式庫的行為,也不會造成重大變更。

請注意,雖然重試作業可能很安全,但這並不代表作業在第二次嘗試時會產生與第一次成功嘗試時相同的結果。舉例來說,建立具有專屬 ID 的資源可能可以安全地重試,因為第二次和後續嘗試都會失敗,且系統會維持在相同狀態。不過,用戶端可能會在重試時收到「已存在」錯誤。

預設重試政策

根據 aip/194 中列出的準則,大多數 C++ 用戶端程式庫只會重試 UNAVAILABLE gRPC 錯誤。這些項目會對應至 StatusCode::kUnavailable。預設政策是重試要求 30 分鐘。

請注意,kUnavailable 錯誤表示伺服器無法接收要求,如果無法傳送要求,或是要求已成功傳送至服務,但用戶端在收到回應前連線中斷,系統就會使用這個錯誤代碼。此外,如果可以判斷要求是否已成功接收,就能解決兩位將軍的問題,這是分散式系統中眾所皆知的不可行結果。

因此,如果所有作業都因 kUnavailable 而失敗,重試並不安全。作業的冪等性也很重要。

預設退避政策

根據預設,大多數程式庫會使用截斷的指數輪詢策略,並加入隨機延遲。初始暫停時間為 1 秒,最長暫停時間為 5 分鐘,每次重試後暫停時間會加倍。

變更預設的重試和停止策略

每個程式庫都會定義 *Option 結構體,用於設定這些政策。您可以在建立 *Client 類別時提供這些選項,甚至在每個要求中提供。

舉例來說,以下程式碼說明如何變更 Cloud Pub/Sub 用戶端的重試和退避政策:

namespace pubsub = ::google::cloud::pubsub;
using ::google::cloud::future;
using ::google::cloud::Options;
using ::google::cloud::StatusOr;
[](std::string project_id, std::string topic_id) {
  auto topic = pubsub::Topic(std::move(project_id), std::move(topic_id));
  // By default a publisher will retry for 60 seconds, with an initial backoff
  // of 100ms, a maximum backoff of 60 seconds, and the backoff will grow by
  // 30% after each attempt. This changes those defaults.
  auto publisher = pubsub::Publisher(pubsub::MakePublisherConnection(
      std::move(topic),
      Options{}
          .set<pubsub::RetryPolicyOption>(
              pubsub::LimitedTimeRetryPolicy(
                  /*maximum_duration=*/std::chrono::minutes(10))
                  .clone())
          .set<pubsub::BackoffPolicyOption>(
              pubsub::ExponentialBackoffPolicy(
                  /*initial_delay=*/std::chrono::milliseconds(200),
                  /*maximum_delay=*/std::chrono::seconds(45),
                  /*scaling=*/2.0)
                  .clone())));

  std::vector<future<bool>> done;
  for (char const* data : {"1", "2", "3", "go!"}) {
    done.push_back(
        publisher.Publish(pubsub::MessageBuilder().SetData(data).Build())
            .then([](future<StatusOr<std::string>> f) {
              return f.get().ok();
            }));
  }
  publisher.Flush();
  int count = 0;
  for (auto& f : done) {
    if (f.get()) ++count;
  }
  std::cout << count << " messages sent successfully\n";
}

請參閱各程式庫的說明文件,找出該程式庫的特定名稱和範例。

後續步驟