RSS

gRPC 与截止时间

要点:始终设置截止时间。本文解释了我们为何建议有意识地设置截止时间,并提供有用的代码片段来展示如何操作。

当您使用 gRPC 时,gRPC 库负责处理通信、编组、解组和截止时间强制执行。截止时间允许 gRPC 客户端指定它们愿意等待 RPC 完成的时间,在此时间之前,RPC 会因错误 DEADLINE_EXCEEDED 而终止。默认情况下,此截止时间是一个非常大的数字,具体取决于语言实现。截止时间的指定方式也取决于语言。一些语言 API 使用截止时间(deadline),即 RPC 应完成的固定时间点。另一些则使用超时(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);

这会将截止时间设置为从客户端 RPC 设置开始到客户端接收到响应为止的 100 毫秒。

检查截止时间

在服务器端,服务器可以查询某个特定的 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);

现在可以调整截止时间以延长等待时间,从而避免失败,而无需挑选带有不同硬编码截止时间的版本。这使您能够在调试和解决退化问题之前为用户缓解该问题。