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


②假设 Consumer 是核心的商品服务 , Provider 是非核心的评论服务 , 当评价服务出现性能问题时 , 商品服务可以接受不返回评价信息 , 从而保证能继续对外提供服务 。
这样情况下 , 就必须设置一个超时时间 , 当评价服务超过这个阈值时 , 商品服务不用继续等待 。
③Provider 很有可能是因为某个瞬间的网络抖动或者机器高负载引起的超时 , 如果超时后直接放弃 , 某些场景会造成业务损失(比如库存接口超时会导致下单失败) 。
因此 , 对于这种临时性的服务抖动 , 如果在超时后重试一下是可以挽救的 , 所以有必要通过重试机制来解决 。
但是引入超时重试机制后 , 并非一切就完美了 。 它同样会带来副作用 , 这些是开发 RPC 接口必须要考虑 , 同时也是最容易忽视的问题:
①重复请求:有可能 Provider 执行完了 , 但是因为网络抖动 Consumer 认为超时了 , 这种情况下重试机制就会导致重复请求 , 从而带来脏数据问题 , 因此服务端必须考虑接口的幂等性 。
②降低 Consumer 的负载能力:如果 Provider 并不是临时性的抖动 , 而是确实存在性能问题 , 这样重试多次也是没法成功的 , 反而会使得 Consumer 的平均响应时间变长 。
比如正常情况下 Provider 的平均响应时间是 1s , Consumer 将超时时间设置成 1.5s , 重试次数设置为 2 次 。
这样单次请求将耗时 3s , Consumer 的整体负载就会被拉下来 , 如果 Consumer 是一个高 QPS 的服务 , 还有可能引起连锁反应造成雪崩 。
③爆炸式的重试风暴:假如一条调用链路经过了 4 个服务 , 最底层的服务 D 出现超时 , 这样上游服务都将发起重试 。
假设重试次数都设置的 3 次 , 那么 B 将面临正常情况下 3 倍的负载量 , C 是 9 倍 , D 是 27 倍 , 整个服务集群可能因此雪崩 。
【美好,一直在身边】惊呆了,RPC超时设置竟然引发了线上事故!
本文插图

应该如何合理的设置超时时间?
理解了 RPC 框架的超时实现原理和可能引入的副作用后 , 可以按照下面的方法进行超时设置:

  • 设置调用方的超时时间之前 , 先了解清楚依赖服务的 TP99 响应时间是多少(如果依赖服务性能波动大 , 也可以看 TP95) , 调用方的超时时间可以在此基础上加 50% 。
  • 如果 RPC 框架支持多粒度的超时设置 , 则:全局超时时间应该要略大于接口级别最长的耗时时间 , 每个接口的超时时间应该要略大于方法级别最长的耗时时间 , 每个方法的超时时间应该要略大于实际的方法执行时间 。
  • 区分是可重试服务还是不可重试服务 , 如果接口没实现幂等则不允许设置重试次数 。 注意:读接口是天然幂等的 , 写接口则可以使用业务单据 ID 或者在调用方生成唯一 ID 传递给服务端 , 通过此 ID 进行防重避免引入脏数据 。
  • 如果 RPC 框架支持服务端的超时设置 , 同样基于前面3条规则依次进行设置 , 这样能避免客户端不设置的情况下配置是合理的 , 减少隐患 。
  • 如果从业务角度来看 , 服务可用性要求不用那么高(比如偏内部的应用系统) , 则可以不用设置超时重试次数 , 直接人工重试即可 , 这样能减少接口实现的复杂度 , 反而更利于后期维护 。
  • 重试次数设置越大 , 服务可用性越高 , 业务损失也能进一步降低 , 但是性能隐患也会更大 , 这个需要综合考虑设置成几次(一般是 2 次 , 最多 3 次) 。
  • 如果调用方是高 QPS 服务 , 则必须考虑服务方超时情况下的降级和熔断策略 。 (比如超过 10% 的请求出错 , 则停止重试机制直接熔断 , 改成调用其他服务、异步 MQ 机制、或者使用调用方的缓存数据)
总结
最后 , 再简单总结下:RPC 接口的超时设置看似简单 , 实际上有很大学问 。 不仅涉及到很多技术层面的问题(比如接口幂等、服务降级和熔断、性能评估和优化) , 同时还需要从业务角度评估必要性 。


推荐阅读