当前位置: 首页 > 科技观察

微服务的超时交付你了解吗?

时间:2023-03-15 19:46:21 科技观察

本文转载自微信公众号《微服务实战》,作者周曙光。转载本文请联系微服务实践公众号。为什么需要超时控制?许多级联故障场景中的一个常见问题是服务器正在消耗大量资源来处理已经超过客户端截止日期的请求。结果,服务器消耗了大量资源,却没有做任何有价值的工作。回复超时的请求是没有意义的。超时控制可以说是保障服务稳定的重要防线。它的本质是快速失败。好的超时控制策略可以尽快清除高延迟请求,尽快释放资源,避免请求堆积。服务之间的超时传递如果一个请求有多个阶段,比如一系列的RPC调用,那么我们的服务应该在每个阶段之前检查截止时间,避免做无用功,即检查是否有足够的剩余时间来处理请求。一个常见的错误是在每个RPC服务上设置一个固定的超时时间。我们应该传递每个服务之间的超时时间。可以在服务调用的顶部设置超时。由初始请求触发的整个RPC树将被设置为相同的绝对期限。例如在服务请求的顶层设置超时时间为3s。服务A请求服务B,服务B执行需要1s。然后服务B请求服务C,此时剩余超时时间为2s,服务C执行需要1s。当服务C再次请求服务D时,服务D需要500ms执行,以此类推。理想情况下,在整个调用链中使用相同的超时传递机制。如果不使用超时下发机制,会出现如下情况:服务A向服务B发送请求,设置的超时时间为3s。服务B处理请求需要2s,继续请求服务C,如果使用超时下发,服务C的超时时间应该是1s,但是这里没有超时发送,所以配置里写的超时时间是3s.服务C继续执行需要2s。其实此时顶层设置的超时时间已经过期,后面的请求都是无效的。表示继续请求服务D,如果服务B采用了超时投递机制,那么服务C应该立即放弃请求,因为deadline已经到了,客户端可能报错了。我们在设置超时投递的时候,一般会把投递的截止时间降低一点,比如100毫秒,这样可以兼顾网络传输时间和客户端收到回复后的处理时间。进程内超时传递不仅需要服务间的超时传递,还需要进程内的超时传递。比如在一个进程中,串行调用Mysql、Redis、服务B,总请求时间设置为3s。1s后再次请求Mysql。此时请求Redis的超时时间为2s。Redis执行耗时500ms,请求服务B的超时时间为1.5s。因为我们每个中间件或者服务都会在配置文件中设置一个固定的超时时间,所以我们需要取剩余时间和设置时间中的最小值。实现上下文随时间传递的原理很简单,但是功能很强大。Go的标准库也实现了对上下文的支持,各种开源框架也实现了对上下文的支持。上下文已经成为标准,超时传递也依赖于上下文来实现。我们一般通过在服务最上层设置初始上下文来进行超时控制传输,比如设置超时时间为3sctx,cancel:=context.WithTimeout(context.Background(),time.Second*3)defercancel()时进行上下文传输比如上图中请求Redis,那么通过下面的方法获取剩余时间,然后是超时时间dl,ok:=ctx.Deadline()timeout:=time.Now()。Add(time.Second*3)ifok:=dl.Before(timeout);ok{timeout=dl}服务间的超时传递主要是指RPC调用时的超时传递。对于gRPC,我们不需要做额外的处理。gRPC本身支持超时传输。原理和上面类似。它通过元数据传输,最终会转化为grpc-timeout的值,如下代码grpc-go/internal/transport/handler_server.go:79ifv:=r所示。Header.Get("grpc-timeout");v!=""{to,err:=decodeTimeout(v)iferr!=nil{returnil,status.Errorf(codes.Internal,"malformedtime-out:%v",err)}st.timeoutSet=true.timeout=to}超时下发是保证服务稳定的重要防线。原理和实现都很简单。你的框架有没有实现超时传递?如果没有,赶快开始吧。go-zero中的超时传递在go-zero中,api网关和rpc服务的超时时间可以通过配置文件中的Timeout进行配置,服务之间会自动传递。上一篇文章UnderstandinghowtoimplementGotimeoutcontrol讲解了如何使用超时控制。参考项目地址https://github.com/zeromicro/go-zero