对冲请求模式出现在论文The Tail At Scale中,是Google 解决微服务长尾效应的一个办法.也是gRPC中两种重试模式之一。
对冲请求客户端将同一个请求发送到不同的节点,一旦收到第一个结果,客户端就会取消剩余的未处理请求。 这种模式主要作用是为了实现可以预测的延迟。假设我们的服务的一个调用链路是20个节点,每个节点的P99是1s,从概率上讲,一定有 18.2% 的请求时间大于1s。
通过对冲模式,我们每次都是从最快的节点那里得到结果,所以不会存在不可预测的长尾延迟(服务故障不在考虑范围之内) 。 在Golang中,我们可以使用context很方便的实现对冲请求,比如在下面的例子中:对于同一个后端服务,我们发起五次请求,只取最先返回的那次。
func hedgedRequest() string {
ch := make(chanstring) // chan used to abort other requests
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 5; i++ {
gofunc(ctx *context.Context, ch chan string, i int) {
log.Println("in goroutine: ", i)
if request(ctx, "http://localhost:8090", i) {
ch <- fmt.Sprintf("finsh [from %v]", i)
log.Println("completed goroutine: ", i)
}
}(&ctx, ch, i)
}
select {
case s := <-ch:
cancel()
log.Println("cancelled all inflight requests")
return s
case <-time.After(5 * time.Second):
cancel()
return"all requests timeout after 5 secs"
}
}
完整的代码 请访问:https://go.dev/play/p/fY9Lj_M7ZYE 这样做的好处就是,我们可以规避服务的长尾延迟,使服务的之间的延迟控制在可控的范围内。不过直接这么实现会造成额外的多倍负载。需要仔细设计。
出现长尾延迟的原因有很多,比如
STW
会放大长尾延迟有什么办法可以避免对冲请求模式造成的 请求放大嘛?Go High-Performance Programming EP7: Use singleflight To Merge The Same Request 中详细介绍了如何使用 SingleFlight
来合并相同的请求。这个场景下面,使用SingleFlight
能够一定程度的缓解重复请求。
还有一种做法是只发送一个请求, 到P95的时候,如果还没有收到返回,那么就立即向第二个节点发送请求。这样做的好处就是将重复请求缩小到5%。并且大大缩短了长尾请求。
在这篇论文中,还有一些方法可以用来解决,长尾请求
你还有其他处理长尾请求的好方法吗?