记录 gRPC Deadlines 的一次事故

收到交易服务的报警,服务器内存暴增。后经排查发现是由于gRPC客户端调用的时候在上下文(context)中未设置Deadline导致的。
那么为什么未设置Deadline会导致内存耗尽呢?
当您使用gRPC时,gRPC库负责通信,编组,解组和最后期限执行。Deadline允许gRPC客户端指定在RPC以错误DEADLINE_EXCEEDED终止之前,他们愿意等待RPC完成的时间。默认情况下,此截止日期是一个非常大的数字,取决于语言实现。如何指定截止日期也取决于语言。指定截止日期或超时的方式因语言而异 – 例如,并非所有语言都有默认的截止日期,某些语言使用 deadline ,而某些语言使用timeouts。在服务器端,服务器可以查询特定RPC是否已超时,或者剩余多少时间来完成RPC。
通常,当您未设置截止日期时,将为所有正在进行的请求保留资源,并且所有请求都可能达到最大超时。这会使服务面临资源耗尽的风险,例如内存,这会增加服务的延迟,或者在最坏的情况下可能导致整个过程崩溃。
“什么是好的截止日期/超时值?”没有单一的答案。那么您需要考虑什么才能明智地选择截止日期?要考虑的因素包括整个系统的端到端延迟,哪些RPC是串行的,哪些可以并行进行。工程师需要了解服务,然后设置一个刻意的截止日期用于客户端和服务器之间的RPC。
在gRPC中,关于远程过程调用(RPC)是否成功的,客户端和服务器都做了独立的和本地的判断。这意味着他们的结论可能不匹配!一个在服务端成功完成的RPC可能在客户端失败。例如,服务器可以发送响应,但是这个回复可以在截止日期到期后到达客户端,而客户端将会在回复到达前使用错误状态DEADLINE_EXCEEDED终止。

Setting a deadline

Go

作为客户端,您应始终设置一个期限,以确定您愿意等待服务器回复的时间。

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

Checking deadlines

On the server side, the server can query to see if a particular RPC is no longer wanted.
在服务端,服务器可以查看是否一个特定的RPC不再需要。在服务器开始作出响应之前,检查是否还有客户端等待它是非常重要的。在开始昂贵的处理之前,这一点尤其重要。

Go

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

当您知道客户已达到截止日期时,服务器继续处理请求是否有用?这取决于。如果响应可以缓存在服务器中,则值得处理和缓存;特别是如果它的资源很重,并且每个请求都要花钱。这将使未来的请求更快,因为结果已经可用。

Adjusting deadlines

如果您设置截止日期但新版本或服务器版本会导致错误回归,该怎么办?截止日期可能太小,导致您的所有请求超时DEADLINE_EXCEEDED,或者太大,您的用户尾部延迟现在很大。您可以使用标志来设置和调整截止日期。

Go

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

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

参考