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

如需查找该库的具体名称和示例,请参阅每个库的文档。

后续步骤