RSS

gRPC 和截止日期

TL;DR: 始终设置截止日期。本文解释了为何我们推荐明确地设置截止日期,并提供了有用的代码片段来展示如何操作。

使用 gRPC 时,gRPC 库负责处理通信、编组(marshalling)、解组(unmarshalling)以及截止日期强制执行。截止日期允许 gRPC 客户端指定愿意等待 RPC 完成的时长,超过该时长后,RPC 将以 DEADLINE_EXCEEDED 错误终止。默认情况下,此截止日期是一个非常大的值,具体取决于语言实现。截止日期的指定方式也因语言而异。一些语言 API 使用截止日期(deadline),即 RPC 应完成的固定时间点。其他语言 API 使用超时(timeout),即 RPC 超时前的时间段。

通常,如果不设置截止日期,资源将被所有进行中的请求占用,并且所有请求都可能达到最大超时。这使得服务面临资源耗尽的风险,例如内存不足,从而增加服务延迟,或者在最坏情况下导致整个进程崩溃。

为避免这种情况,服务应指定它们技术上支持的最长默认截止日期,而客户端应等待到响应对其不再有用为止。对于服务而言,这可以像在 .proto 文件中提供注释一样简单。对于客户端而言,这涉及设置有用的截止日期。

对于“什么是好的截止日期/超时值?”这个问题,没有单一的答案。你的服务可能像我们快速入门指南中的 Greeter 一样简单,这种情况下 100 毫秒就足够了。你的服务也可能像一个全球分布且强一致的数据库那样复杂。客户端查询的截止日期会与它们应等待你删除表格的时间不同。

那么,你需要考虑哪些因素来做出明智的截止日期选择呢?需要考虑的因素包括整个系统的端到端延迟,哪些 RPC 是串行的,哪些可以并行执行。你应该能够量化这些,即使是一个粗略的计算。工程师需要理解服务,然后为客户端和服务器之间的 RPC 设置明确的截止日期。

在 gRPC 中,客户端和服务器都独立地、在本地判断远程过程调用 (RPC) 是否成功。这意味着它们的结论可能不一致!在服务器端成功完成的 RPC 在客户端可能失败。例如,服务器可以发送响应,但响应在客户端的截止日期过后才到达。客户端此时已因状态错误 DEADLINE_EXCEEDED 而终止。这应该在应用层进行检查和管理。

设置截止日期

作为客户端,你应该始终设置一个截止日期,表明你愿意等待服务器回复的时长。以下是使用来自快速入门页面中的 Greeting 服务的示例:

C++

ClientContext context;
time_point deadline = std::chrono::system_clock::now() +
    std::chrono::milliseconds(100);
context.set_deadline(deadline);

Go

clientDeadline := time.Now().Add(time.Duration(*deadlineMs) * time.Millisecond)
ctx, cancel := context.WithDeadline(ctx, clientDeadline)

Java

response = blockingStub.withDeadlineAfter(deadlineMs, TimeUnit.MILLISECONDS).sayHello(request);

这将截止日期设置为 100 毫秒,从客户端 RPC 发起之时算起,直到客户端接收到响应为止。

检查截止日期

在服务器端,服务器可以查询以查看是否不再需要某个特定的 RPC。在服务器开始处理响应之前,检查是否有客户端仍在等待响应非常重要。这在开始执行昂贵的处理之前尤其重要。

C++

if (context->IsCancelled()) {
  return Status(StatusCode::CANCELLED, "Deadline exceeded or Client cancelled, abandoning.");
}

Go

if ctx.Err() == context.Canceled {
	return status.New(codes.Canceled, "Client cancelled, abandoning.")
}

Java

if (Context.current().isCancelled()) {
  responseObserver.onError(Status.CANCELLED.withDescription("Cancelled by client").asRuntimeException());
  return;
}

当你得知客户端已达到其截止日期时,服务器继续处理请求是否有用?这取决于具体情况。如果响应可以在服务器中缓存,那么处理并缓存它可能是值得的;特别是如果处理消耗大量资源,并且每次请求都会产生费用。这将使未来的请求更快,因为结果已经可用。

调整截止日期

如果你设置了截止日期,但新版本或服务器版本导致了严重的回归问题怎么办?截止日期可能太小,导致所有请求都因 DEADLINE_EXCEEDED 而超时,或者太大,导致用户尾部延迟现在变得巨大。你可以使用一个标志来设置和调整截止日期。

C++

#include <gflags/gflags.h>
DEFINE_int32(deadline_ms, 20*1000, "Deadline in milliseconds.");

ClientContext context;
time_point deadline = std::chrono::system_clock::now() +
    std::chrono::milliseconds(FLAGS_deadline_ms);
context.set_deadline(deadline);

Go

var deadlineMs = flag.Int("deadline_ms", 20*1000, "Default deadline in milliseconds.")

ctx, cancel := context.WithTimeout(ctx, time.Duration(*deadlineMs) * time.Millisecond)

Java

@Option(name="--deadline_ms", usage="Deadline in milliseconds.")
private int deadlineMs = 20*1000;

response = blockingStub.withDeadlineAfter(deadlineMs, TimeUnit.MILLISECONDS).sayHello(request);

现在,可以调整截止日期以延长等待时间来避免失败,而无需通过 cherry-pick 一个硬编码了不同截止日期的版本。这使你可以在调试和解决回归问题之前,为用户缓解该问题。