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