C++ 客户端库中的重试政策

本页面介绍了 C++ 客户端库使用的重试模型。

客户端库会代表您发出 RPC(远程过程调用)。这些 RPC 可能会因瞬时错误而失败。服务器重启、负载平衡器关闭过载或空闲连接以及速率限制生效,这些都只是瞬时故障的一些示例。

库可能会将这些错误返回给应用。不过,其中许多错误都可以在库中轻松处理,这使得应用代码更简单。

可重试错误和可重试操作

只有瞬时错误可以重试。例如,kUnavailable 表示客户端无法连接到服务,或者在请求进行期间与服务断开连接。这几乎总是瞬时情况,不过可能需要很长时间才能恢复。这些错误始终可以重试(假设操作本身可以安全地重试)。相比之下,kPermissionDenied 错误需要额外的干预(通常由人工干预)才能解决。此类错误不被视为“瞬时”错误,或者至少在客户端库中的重试循环所考虑的时间范围内不被视为瞬时错误。

同样,无论错误的性质如何,某些操作都无法安全地重试。这包括进行增量更改的任何操作。例如,如果名为“X”的资源可能有多个版本,则重试移除“X 的最新版本”的操作是不安全的。这是因为调用方可能打算移除单个版本,而重试此类请求可能会导致移除所有版本。

配置重试循环

客户端库接受三个不同的配置参数来控制重试循环:

  • *IdempotencyPolicy 确定特定请求是否具有幂等性。只有此类请求才会重试。
  • *RetryPolicy 确定 (a) 是否应将错误视为瞬时故障,以及 (b) 客户端库重试请求的时长(或次数)。
  • *BackoffPolicy 确定客户端库在重新发出请求之前等待的时长。

默认幂等性政策

一般来说,如果多次成功调用某个函数后,系统与成功调用该函数一次后处于相同的状态,则该操作具有幂等性。只有具有幂等性的操作才能安全地重试。具有幂等性的操作的示例包括但不限于所有只读操作,以及只能成功一次的操作。

默认情况下,客户端库仅将通过 GETPUT 谓词实现的 RPC 视为具有幂等性。这可能过于保守,在某些服务中,即使某些 POST 请求也具有幂等性。您可以随时替换默认幂等性政策,以更好地满足您的需求。

某些操作只有在包含前提条件时才具有幂等性。例如,“如果最新版本是 Y,则移除最新版本”具有幂等性,因为它只能成功一次。

客户端库会不时收到改进,以便将更多操作视为具有幂等性。我们将这些改进视为 bug 修复,因此即使它们更改了客户端库的行为,也不会造成重大变更。

请注意,虽然重试操作可能是安全的,但这并不意味着该操作在第二次尝试时与第一次成功尝试时产生相同的结果。例如,创建具有唯一标识符的资源可能可以安全地重试,因为第二次和后续尝试都会失败,并使系统处于相同的状态。不过,客户端可能会在重试尝试时收到“已存在”错误。

默认重试政策

按照 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";
}

请参阅每个库的文档,以查找该库的特定名称和示例。

后续步骤