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

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

后续步骤