【美好,一直在身边】惊呆了,RPC超时设置竟然引发了线上事故!( 二 )


下面是更加直观的示意图:
【美好,一直在身边】惊呆了,RPC超时设置竟然引发了线上事故!
本文插图

解决方案
解决方案如下:

  • 将业务网关调用推荐服务的超时时间改成了 800ms(推荐服务的 TP99 大约为 540ms) , 超时重试次数改成了 2 次 。
  • 将业务网关调用商品排序服务的超时时间改成了 600ms(商品排序服务的 TP99 大约为 400ms) , 超时重试次数也改成了 2 次 。
关于超时时间和重试次数的设置 , 需要考虑整个调用链中所有依赖服务的耗时、各个服务是否是核心服务等很多因素 。 这里先不作展开 , 后文会详细介绍具体方法 。
超时的实现原理是什么?
只有了解了 RPC 框架的超时实现原理 , 才能更好地去设置它 。 不论是 Dubbo、Spring Cloud 或者大厂自研的微服务框架(比如京东的 JSF) , 超时的实现原理基本类似 。
下面以 Dubbo 2.8.4 版本的源码为例来看下具体实现 。 熟悉 Dubbo 的同学都知道 , 可在两个地方配置超时时间:分别是 Provider(服务端 , 服务提供方)和 Consumer(消费端 , 服务调用方) 。
服务端的超时配置是消费端的缺省配置 , 也就是说只要服务端设置了超时时间 , 则所有消费端都无需设置 , 可通过注册中心传递给消费端 。
这样:一方面简化了配置 , 另一方面因为服务端更清楚自己的接口性能 , 所以交给服务端进行设置也算合理 。
Dubbo 支持非常细粒度的超时设置 , 包括:方法级别、接口级别和全局 。 如果各个级别同时配置了 , 优先级为:消费端方法级>服务端方法级>消费端接口级>服务端接口级>消费端全局>服务端全局 。
通过源码 , 我们先看下服务端的超时处理逻辑:
【美好,一直在身边】惊呆了,RPC超时设置竟然引发了线上事故!
本文插图

可以看到 , 服务端即使超时 , 也只是打印了一个 warn 日志 。 因此 , 服务端的超时设置并不会影响实际的调用过程 , 就算超时也会执行完整个处理逻辑 。
再来看下消费端的超时处理逻辑:
【美好,一直在身边】惊呆了,RPC超时设置竟然引发了线上事故!
本文插图

FailoverCluster 是集群容错的缺省模式 , 当调用失败后会切换成调用其他服务器 。
再看下 doInvoke 方法 , 当调用失败时 , 会先判断是否是业务异常 , 如果是则终止重试 , 否则会一直重试直到达到重试次数 。
继续跟踪 Invoker 的 Invoke 方法 , 可以看到在请求发出后通过 Future 的 get 方法获取结果 。
源码如下:
【美好,一直在身边】惊呆了,RPC超时设置竟然引发了线上事故!
本文插图

进入方法后开始计时 , 如果在设定的超时时间内没有获得返回结果 , 则抛出 TimeoutException 。
因此 , 消费端的超时逻辑同时受到超时时间和超时次数两个参数的控制 , 像网络异常、响应超时等都会一直重试 , 直到达到重试次数 。
设置超时时间是为了解决什么问题?
RPC 框架的超时重试机制到底是为了解决什么问题呢?从微服务架构这个宏观角度来说 , 它是为了确保服务链路的稳定性 , 提供了一种框架级的容错能力 。
微观上如何理解呢?可以从下面几个具体 case 来看:
①Consumer 调用 Provider , 如果不设置超时时间 , 则 Consumer 的响应时间肯定会大于 Provider 的响应时间 。
当 Provider 性能变差时 , Consumer 的性能也会受到影响 , 因为它必须无限期地等待 Provider 的返回 。
假如整个调用链路经过了 A、B、C、D 多个服务 , 只要 D 的性能变差 , 就会自下而上影响到 A、B、C , 最终造成整个链路超时甚至瘫痪 , 因此设置超时时间是非常有必要的 。


推荐阅读