gRPC 与截止时间
长话短说:请务必设置截止时间。本文解释了我们建议谨慎设置截止时间的原因,并提供了相关代码片段演示如何操作。
当你使用 gRPC 时,gRPC 库会处理通信、序列化、反序列化以及截止时间的强制执行。截止时间允许 gRPC 客户端指定他们愿意等待 RPC 完成的最长时间,如果超过此时间,RPC 将以 DEADLINE_EXCEEDED 错误终止。默认情况下,此截止时间是一个很大的数值,具体取决于语言实现。指定截止时间的方式也因语言而异。有些语言的 API 使用“截止时间”(即 RPC 必须完成的特定时间点),而另一些语言则使用“超时时间”(即 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 而超时,或者设置得太大,导致用户长尾延迟变得非常巨大。你可以使用标志(flag)来设置和调整截止时间。
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);
现在,截止时间可以进行调整,以延长等待时间避免失败,而无需重新挑选一个硬编码了不同截止时间的版本。这使你能够在调试并解决回归问题之前,减轻对用户的影响。