惊呆了,RPC超时设置竟然引发了线上事故!
mhr18 2024-11-02 11:53 21 浏览 0 评论
这篇文章将通过一个真实的线上事故,系统性地介绍:在微服务架构下,该如何正确理解并设置 RPC 接口的超时时间,让大家在开发服务端接口时有更全局的视野。
图片来自 Pexels
上面这张监控图,对于服务端的研发同学来说再熟悉不过了。在日常的系统维护中,“服务超时”应该属于监控报警最多的一类问题。
尤其在微服务架构下,一次请求可能要经过一条很长的链路,跨多个服务调用后才能返回结果。
当服务超时发生时,研发同学往往要抽丝剥茧般去分析自身系统的性能以及依赖服务的性能,这也是为什么服务超时相对于服务出错和服务调用量异常更难调查的原因。
本篇文章将分成以下四个部分:
- 从一次 RPC 接口超时引发的线上事故说起
- 超时的实现原理是什么?
- 设置超时时间到底是为了解决什么问题?
- 应该如何合理的设置超时时间?
从一次 RPC 接口超时引发的线上事故说起
事故发生在电商 APP 的首页推荐模块,某天中午突然收到用户反馈:APP 首页除了 banner 图和导航区域,下方的推荐模块变成空白页了(推荐模块占到首页 2/3 的空间,是根据用户兴趣由算法实时推荐的商品 list)。
上面的业务场景可以借助上图的调用链来理解:
- APP 端发起一个 HTTP 请求到业务网关。
- 业务网关 RPC 调用推荐服务,获取推荐商品 list。
- 如果第 2 步调用失败,则服务降级,改成 RPC 调用商品排序服务,获取热销商品 list 进行托底。
- 如果第 3 步调用失败,则再次降级,直接获取 Redis 缓存中的热销商品 list。
粗看起来,两个依赖服务的降级策略都考虑进去了,理论上就算推荐服务或者商品排序服务全部挂掉,服务端都应该可以返回数据给 APP 端。
但是 APP 端的推荐模块确实出现空白了,降级策略可能并未生效,下面详细说下定位过程。
问题定位过程
第 1 步:APP 端通过抓包发现:HTTP 请求存在接口超时(超时时间设置的是 5 秒)。
第 2 步:业务网关通过日志发现:调用推荐服务的 RPC 接口出现了大面积超时(超时时间设置的是 3 秒)。
错误信息如下:
第 3 步:推荐服务通过日志发现:Dubbo 的线程池耗尽。
错误信息如下:
通过以上 3 步,基本就定位到了问题出现在推荐服务,后来进一步调查得出:是因为推荐服务依赖的 Redis 集群不可用导致了超时,进而导致线程池耗尽。
详细原因这里不作展开,跟本文要讨论的主题相关性不大。
降级策略未生效的原因分析
下面再接着分析下:当推荐服务调用失败时,为什么业务网关的降级策略没有生效呢?理论上来说,不应该降级去调用商品排序服务进行托底吗?
最终跟踪分析找到了根本原因:APP 端调用业务网关的超时时间是 5 秒,业务网关调用推荐服务的超时时间是 3 秒,同时还设置了 3 次超时重试。
这样当推荐服务调用失败进行第 2 次重试时,HTTP 请求就已经超时了,因此业务网关的所有降级策略都不会生效。
下面是更加直观的示意图:
解决方案
解决方案如下:
- 将业务网关调用推荐服务的超时时间改成了 800ms(推荐服务的 TP99 大约为 540ms),超时重试次数改成了 2 次。
- 将业务网关调用商品排序服务的超时时间改成了 600ms(商品排序服务的 TP99 大约为 400ms),超时重试次数也改成了 2 次。
关于超时时间和重试次数的设置,需要考虑整个调用链中所有依赖服务的耗时、各个服务是否是核心服务等很多因素。这里先不作展开,后文会详细介绍具体方法。
超时的实现原理是什么?
只有了解了 RPC 框架的超时实现原理,才能更好地去设置它。不论是 Dubbo、Spring Cloud 或者大厂自研的微服务框架(比如京东的 JSF),超时的实现原理基本类似。
下面以 Dubbo 2.8.4 版本的源码为例来看下具体实现。熟悉 Dubbo 的同学都知道,可在两个地方配置超时时间:分别是 Provider(服务端,服务提供方)和 Consumer(消费端,服务调用方)。
服务端的超时配置是消费端的缺省配置,也就是说只要服务端设置了超时时间,则所有消费端都无需设置,可通过注册中心传递给消费端。
这样:一方面简化了配置,另一方面因为服务端更清楚自己的接口性能,所以交给服务端进行设置也算合理。
Dubbo 支持非常细粒度的超时设置,包括:方法级别、接口级别和全局。如果各个级别同时配置了,优先级为:消费端方法级>服务端方法级>消费端接口级>服务端接口级>消费端全局>服务端全局。
通过源码,我们先看下服务端的超时处理逻辑:
可以看到,服务端即使超时,也只是打印了一个 warn 日志。因此,服务端的超时设置并不会影响实际的调用过程,就算超时也会执行完整个处理逻辑。
再来看下消费端的超时处理逻辑:
FailoverCluster 是集群容错的缺省模式,当调用失败后会切换成调用其他服务器。
再看下 doInvoke 方法,当调用失败时,会先判断是否是业务异常,如果是则终止重试,否则会一直重试直到达到重试次数。
继续跟踪 Invoker 的 Invoke 方法,可以看到在请求发出后通过 Future 的 get 方法获取结果。
源码如下:
进入方法后开始计时,如果在设定的超时时间内没有获得返回结果,则抛出 TimeoutException。
因此,消费端的超时逻辑同时受到超时时间和超时次数两个参数的控制,像网络异常、响应超时等都会一直重试,直到达到重试次数。
设置超时时间是为了解决什么问题?
RPC 框架的超时重试机制到底是为了解决什么问题呢?从微服务架构这个宏观角度来说,它是为了确保服务链路的稳定性,提供了一种框架级的容错能力。
微观上如何理解呢?可以从下面几个具体 case 来看:
①Consumer 调用 Provider,如果不设置超时时间,则 Consumer 的响应时间肯定会大于 Provider 的响应时间。
当 Provider 性能变差时,Consumer 的性能也会受到影响,因为它必须无限期地等待 Provider 的返回。
假如整个调用链路经过了 A、B、C、D 多个服务,只要 D 的性能变差,就会自下而上影响到 A、B、C,最终造成整个链路超时甚至瘫痪,因此设置超时时间是非常有必要的。
②假设 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 框架的超时实现原理和可能引入的副作用后,可以按照下面的方法进行超时设置:
- 设置调用方的超时时间之前,先了解清楚依赖服务的 TP99 响应时间是多少(如果依赖服务性能波动大,也可以看 TP95),调用方的超时时间可以在此基础上加 50%。
- 如果 RPC 框架支持多粒度的超时设置,则:全局超时时间应该要略大于接口级别最长的耗时时间,每个接口的超时时间应该要略大于方法级别最长的耗时时间,每个方法的超时时间应该要略大于实际的方法执行时间。
- 区分是可重试服务还是不可重试服务,如果接口没实现幂等则不允许设置重试次数。注意:读接口是天然幂等的,写接口则可以使用业务单据 ID 或者在调用方生成唯一 ID 传递给服务端,通过此 ID 进行防重避免引入脏数据。
- 如果 RPC 框架支持服务端的超时设置,同样基于前面3条规则依次进行设置,这样能避免客户端不设置的情况下配置是合理的,减少隐患。
- 如果从业务角度来看,服务可用性要求不用那么高(比如偏内部的应用系统),则可以不用设置超时重试次数,直接人工重试即可,这样能减少接口实现的复杂度,反而更利于后期维护。
- 重试次数设置越大,服务可用性越高,业务损失也能进一步降低,但是性能隐患也会更大,这个需要综合考虑设置成几次(一般是 2 次,最多 3 次)。
- 如果调用方是高 QPS 服务,则必须考虑服务方超时情况下的降级和熔断策略。(比如超过 10% 的请求出错,则停止重试机制直接熔断,改成调用其他服务、异步 MQ 机制、或者使用调用方的缓存数据)
总结
最后,再简单总结下:RPC 接口的超时设置看似简单,实际上有很大学问。不仅涉及到很多技术层面的问题(比如接口幂等、服务降级和熔断、性能评估和优化),同时还需要从业务角度评估必要性。
知其然知其所以然,希望这些知识能让你在开发 RPC 接口时,有更全局的视野。
作者:骆俊武
来源:IT人的职场进阶
相关推荐
- 如何通过 Redis 日志排查连接超时问题
-
Redis是一种高性能的内存数据存储服务,但在高并发或误配置情况下,可能会出现连接超时问题。借助Redis日志,可以快速定位并解决连接超时的根本原因。以下是具体的排查和解决步骤:1.什么是R...
- 给你1亿的Redis key,如何高效统计?
-
前言有些小伙伴在工作中,可能遇到过这样的场景:老板突然要求统计Redis中所有key的数量,你随手执行了KEYS*命令,下一秒监控告警疯狂闪烁——整个Redis集群彻底卡死,线上服务大面积瘫痪。今天...
- Redis分布式锁的安全性分析与实践指南
-
一、Redis分布式锁的核心原理Redis分布式锁通过SETNX(SetifNotExists)和EXPIRE(Expire)指令实现原子性操作,结合UUID生成唯一标识符,确保锁的互斥性和安全...
- 高可用Redis分布式锁:秒杀系统中的锁战
-
引言在分布式系统中,“程序猿的终极武器是并发控制”。当多个服务实例同时访问共享资源时,如何避免数据不一致和重复操作?答案是分布式锁。Redis凭借其高性能和原子性操作,成为实现分布式锁的首选方案。...
- Redis分布式锁(redis分布式锁解决超卖)
-
场景描述简单模拟一个高并发库存扣减场景,商品库存加载到Redis缓存,如:127.0.0.1:6379>setproduct:stock:101200无锁状态操作从缓存中获取对应商品的库存...
- Redis 分布式锁和 ZooKeeper分布式锁
-
Redis分布式锁和ZooKeeper(简称zk)分布式锁都是用来解决在分布式系统中多个节点之间竞争资源的问题。它们各自有不同的特点和适用场景。Redis分布式锁Redis实现分布式锁主要是...
- Redis vs ZooKeeper锁:高并发下的生死对决,谁才是最终赢家?
-
在分布式系统中,锁是控制资源访问的重要机制。Redis和ZooKeeper作为两种主流的分布式锁实现方案,各有优劣。本文将从原理、性能、代码实现三个维度进行硬核对比,助你做出最佳技术选型。一、原理对比...
- 说说Redis的大key(redis key大小限制)
-
一句话总结Redis大key指存储超大值(如字符串过大、集合元素过多)的键。主要成因包括:1.设计不合理,未拆分数据结构;2.业务需求(如缓存整页数据);3.数据持续积累未清理;4.使用不当的集合类型...
- PHP Laravel框架底层机制(php框架的底层原理)
-
当然可以,Laravel是最受欢迎的PHP框架之一,以优雅的语法和丰富的生态而闻名。尽管开发体验非常“高端”,它的底层其实是由一系列结构清晰、职责分明的组件构成的。下面我从整体架构、核心流程、...
- PHP性能全面优化-值得收藏(php优化网站性能)
-
PHP项目卡顿频发,老技巧失灵?隐藏漏洞竟在代码循环里。上周公司服务器突然开始卡顿,测试发现用户请求响应时间翻倍。我们先按以前学的方法做了基准测试,用AB工具压测时发现2000并发就有5%错误,换成S...
- PHP+UniApp:低成本打造外卖系统横扫App+小程序+H5全平台
-
在餐饮行业数字化转型中,外卖系统开发常面临两大痛点:高昂的开发成本(需独立开发App、小程序、H5)和多端维护的复杂性。PHP+UniApp的组合通过技术复用与跨平台能力,为中小商家和开发者提供了“降...
- 从需求到上线:PHP+Uniapp校园圈子系统源码的架构设计与性能优化
-
一、需求分析与架构设计1.核心功能需求用户体系:支持手机号/微信登录、多角色权限(学生、教师、管理员)。圈子管理:支持创建/加入兴趣圈子(如学术、电竞)、标签分类、动态发布与审核。实时互动:点赞、评...
- PHP 8.0性能翻3倍?四年亲测:这些项目升了哭晕!
-
2020年那个感恩节,当PHP8.0带着“性能翻倍”的豪言横空出世时,无数程序员连夜备份代码准备升级。四年过去了,那些宣称“性能提升3倍”的项目,真的跑出火箭速度了吗?还记得当时铺天盖地的宣传吗?“...
- 我把 Mac mini 托管到机房了:一套打败云服务器的终极方案
-
本内容来源于@什么值得买APP,观点仅代表作者本人|作者:薯仔不爱吃薯仔我把我积灰的Macmini托管到机房了,有图有真相。虽然画质又渣又昏暗,但是!这就是实锤。作为开发者,谁不想拥有个自己的服...
- 从phpstudy到Docker:我用一个下午让开发效率翻倍的实战指南
-
一、为什么放弃phpstudy?上周三下午,我花了3小时将本地开发环境从phpstudy迁移到Docker,没想到第二天团队反馈:环境部署时间从2小时压缩到5分钟,跨设备协作bug减少70%。作为一个...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle 空为0 (51)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (83)
- php redis (97)
- redis 存储 (67)
- redis 锁 (74)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)