你了解微服务的超时传递吗?
为什么需要超时控制?
很多连锁故障的场景下的一个常见问题是服务器正在消耗大量资源处理那些早已经超过客户端截止时间的请求,这样的结果是,服务器消耗大量资源没有做任何有价值的工作,回复已经超时的请求是没有任何意义的。
超时控制可以说是保证服务稳定性的一道重要的防线,它的本质是快速失败(fail fast),良好的超时控制策略可以尽快清空高延迟的请求,尽快释放资源避免请求的堆积。
服务间超时传递
如果一个请求有多个阶段,比如由一系列 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 就应该立刻放弃该请求,因为已经到了截止时间,客户端可能已经报错。我们在设置超时传递的时候一般会将传递出去的截止时间减少一点,比如 100 毫秒,以便将网络传输时间和客户端收到回复之后的处理时间考虑在内。
进程内超时传递
不光服务间需要超时传递进程内同样需要进行超时传递,比如在一个进程内串行的调用了 Mysql、Redis 和服务 B,设置总的请求时间为 3s,请求 Mysql 耗时 1s 后再次请求 Redis 这时的超时时间为 2s,Redis 执行耗时 500ms 再请求服务 B 这时候超时时间为 1.5s,因为我们的每个中间件或者服务都会在配置文件中设置一个固定的超时时间,我们需要取剩余时间和设置时间中的最小值。
context 实现超时传递
context 原理非常简单,但功能却非常强大,go 的标准库也都已实现了对 context 的支持,各种开源的框架也实现了对 context 的支持,context 已然成为了标准,超时传递也依赖 context 来实现。
我们一般在服务的最上层通过设置初始 context 进行超时控制传递,比如设置超时时间为 3s
当进行 context 传递的时候,比如上图中请求 Redis,那么通过如下方式获取剩余时间,然后对比 Redis 设置的超时时间取较小的时间
服务间超时传递主要是指 RPC 调用时候的超时传递,对于 gRPC 来说并不需要要我们做额外的处理,gRPC 本身就支持超时传递,原理和上面差不多,是通过 metadata 进行传递,最终会被转化为 grpc-timeout 的值,如下代码所示 grpc-go/internal/transport/handler_server.go:79
超时传递是保证服务稳定性的一道重要防线,原理和实现都非常简单,你们的框架中实现了超时传递了吗?如果没有的话就赶紧动起手来吧。
go-zero 中的超时传递
go-zero 中可以通过配置文件中的 Timeout
配置 api gateway
和 rpc
服务的超时,并且会在服务间自动传递。
之前的 一文搞懂如何实现 Go 超时控制 里面有讲解超时控制如何使用。
参考
《SRE:Google 运维解密》
项目地址
https://github.com/zeromicro/go-zero
欢迎使用 go-zero
并 star/fork 支持我们!
版权声明: 本文为 InfoQ 作者【万俊峰Kevin】的原创文章。
原文链接:【http://xie.infoq.cn/article/b73440e584597e2aa7d8a6f87】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论