上面这张监控图,对于服务端的研发同学来说再熟悉不过了。在日常的系统维护中,『服务超时』应该属于监控报警最多的一类问题。

尤其在微服务架构下,一次请求可能要经过一条很长的链路,跨多个服务调用后才能返回结果。当服务超时发生时,研发同学往往要抽丝剥茧般去分析自身系统的性能以及依赖服务的性能,这也是为什么服务超时相对于服务出错和服务调用量异常更难调查的原因。

这篇文章将通过一个真实的线上事故,系统性地介绍下:在微服务架构下,该如何正确理解并设置RPC接口的超时时间,让大家在开发服务端接口时有更全局的视野。内容将分成以下4个部分:

  • 从一次RPC接口超时引发的线上事故说起
  • 超时的实现原理是什么?
  • 设置超时时间到底是为了解决什么问题?
  • 应该如何合理的设置超时时间?

01 从一次线上事故说起

事故发生在电商APP的首页推荐模块,某天中午突然收到用户反馈:APP首页除了banner图和导航区域,下方的推荐模块变成空白页了(推荐模块占到首页2/3的空间,是根据用户兴趣由算法实时推荐的商品list)。

上面的业务场景可以借助下面的调用链来理解


  • APP端发起一个HTTP请求到业务网关
  • 业务网关RPC调用推荐服务,获取推荐商品list
  • 如果第2步调用失败,则服务降级,改成RPC调用商品排序服务,获取热销商品list进行托底
  • 如果第3步调用失败,则再次降级,直接获取Redis缓存中的热销商品list

粗看起来,两个依赖服务的降级策略都考虑进去了,理论上就算推荐服务或者商品排序服务全部挂掉,服务端都应该可以返回数据给APP端。但是APP端的推荐模块确实出现空白了,降级策略可能并未生效,下面详细说下定位过程。

1、问题定位过程

第1步:APP端通过抓包发现:HTTP请求存在接口超时(超时时间设置的是5秒)。第2步:业务网关通过日志发现:调用推荐服务的RPC接口出现了大面积超时(超时时间设置的是3秒),错误信息如下:


第3步:推荐服务通过日志发现:dubbo的线程池耗尽,错误信息如下:


通过以上3步,基本就定位到了问题出现在推荐服务,后来进一步调查得出:是因为推荐服务依赖的redis集群不可用导致了超时,进而导致线程池耗尽。详细原因这里不作展开,跟本文要讨论的主题相关性不大。

2、降级策略未生效的原因分析

下面再接着分析下:当推荐服务调用失败时,为什么业务网关的降级策略没有生效呢?理论上来说,不应该降级去调用商品排序服务进行托底吗?

最终跟踪分析找到了根本原因:APP端调用业务网关的超时时间是5秒,业务网关调用推荐服务的超时时间是3秒,同时还设置了3次超时重试,这样当推荐服务调用失败进行第2次重试时,HTTP请求就已经超时了,因此业务网关的所有降级策略都不会生效。下面是更加直观的示意图:


3、解决方案

  • 将业务网关调用推荐服务的超时时间改成了800ms(推荐服务的TP99大约为540ms),超时重试次数改成了2次
  • 将业务网关调用商品排序服务的超时时间改成了600ms(商品排序服务的TP99大约为400ms),超时重试次数也改成了2次

关于超时时间和重试次数的设置,需要考虑整个调用链中所有依赖服务的耗时、各个服务是否是核心服务等很多因素。这里先不作展开,后文会详细介绍具体方法。

02 超时的实现原理是什么?

只有了解了RPC框架的超时实现原理,才能更好地去设置它。不论是dubbo、SpringCloud或者大厂自研的微服务框架(比如京东的JSF),超时的实现原理基本类似。下面以dubbo 2.8.4版本的源码为例来看下具体实现。

熟悉dubbo的同学都知道,可在两个地方配置超时时间:分别是provider(服务端,服务提供方)和consumer(消费端,服务调用方)。服务端的超时配置是消费端的缺省配置,也就是说只要服务端设置了超时时间,则所有消费端都无需设置,可通过注册中心传递给消费端,这样:一方面简化了配置,另一方面因为服务端更清楚自己的接口性能,所以交给服务端进行设置也算合理。

dubbo支持非常细粒度的超时设置,包括:方法级别、接口级别和全局。如果各个级别同时配置了,优先级为:消费端方法级 > 服务端方法级 > 消费端接口级 > 服务端接口级 > 消费端全局 > 服务端全局。

通过源码,我们先看下服务端的超时处理逻辑

  1 public class TimeoutFilter implements Filter { 2 3    public TimeoutFilter() { 4    } 5 6    public Result invoke(...) throws RpcException { 7        // 执行真正的逻辑调用,并统计耗时 8        long start = System.currentTimeMillis(); 9        Result result = invoker.invoke(invocation);10        long elapsed = System.currentTimeMillis() - start;1112        // 判断是否超时13        if (invoker.getUrl() != null && elapsed > timeout) {14            // 打印warn日志15            logger.warn("invoke time out...");16        }1718        return result;19    }20}

可以看到,服务端即使超时,也只是打印了一个warn日志。因此,服务端的超时设置并不会影响实际的调用过程,就算超时也会执行完整个处理逻辑。再来看下消费端的超时处理逻辑

 1 public class FailoverClusterInvoker { 2 3    public Result doInvoke(...)  { 4        ... 5        // 循环调用设定的重试次数 6        for (int i = 0; i  7            ... 8            try { 9                Result result = invoker.invoke(invocation);10                return result;11            } catch (RpcException e) {12                // 如果是业务异常,终止重试13                if (e.isBiz()) {14                    throw e;15                }1617                le = e;18            } catch (Throwable e) {19                le = new RpcException(...);20            } finally {21                ...22            }23        }2425        throw new RpcException("...");26    }27}

FailoverCluster是集群容错的缺省模式,当调用失败后会切换成调用其他服务器。再看下doInvoke方法,当调用失败时,会先判断是否是业务异常,如果是则终止重试,否则会一直重试直到达到重试次数。

继续跟踪invoker的invoke方法,可以看到在请求发出后通过Future的get方法获取结果,源码如下:

 1 public Object get(int timeout) { 2        if (timeout <= 0) { 3            timeout = 1000; 4        } 5 6        if (!isDone()) { 7            long start = System.currentTimeMillis(); 8            this.lock.lock(); 910            try {11                // 循环判断12                while(!isDone()) {13                    // 放弃锁,进入等待状态14                    done.await((long)timeout, TimeUnit.MILLISECONDS);1516                    // 判断是否已经返回结果或者已经超时17                    long elapsed = System.currentTimeMillis() - start;18                    if (isDone() || elapsed > (long)timeout) {19                        break;20                    }21                }22            } catch (InterruptedException var8) {23                throw new RuntimeException(var8);24            } finally {25                this.lock.unlock();26            }2728            if (!isDone()) {29                // 如果未返回结果,则抛出超时异常30                throw new TimeoutException(...);31            }32        }3334        return returnFromResponse();35    }

进入方法后开始计时,如果在设定的超时时间内没有获得返回结果,则抛出TimeoutException。因此,消费端的超时逻辑同时受到超时时间和超时次数两个参数的控制,像网络异常、响应超时等都会一直重试,直到达到重试次数。

03 设置超时时间是为了解决什么问题?

RPC框架的超时重试机制到底是为了解决什么问题呢?从微服务架构这个宏观角度来说,它是为了确保服务链路的稳定性,提供了一种框架级的容错能力。微观上如何理解呢?可以从下面几个具体case来看:1、consumer调用provider,如果不设置超时时间,则consumer的响应时间肯定会大于provider的响应时间。当provider性能变差时,consumer的性能也会受到影响,因为它必须无限期地等待provider的返回。假如整个调用链路经过了A、B、C、D多个服务,只要D的性能变差,就会自下而上影响到A、B、C,最终造成整个链路超时甚至瘫痪,因此设置超时时间是非常有必要的。

2、假设consumer是核心的商品服务,provider是非核心的评论服务,当评价服务出现性能问题时,商品服务可以接受不返回评价信息,从而保证能继续对外提供服务。这样情况下,就必须设置一个超时时间,当评价服务超过这个阈值时,商品服务不用继续等待。

3、provider很有可能是因为某个瞬间的网络抖动或者机器高负载引起的超时,如果超时后直接放弃,某些场景会造成业务损失(比如库存接口超时会导致下单失败)。因此,对于这种临时性的服务抖动,如果在超时后重试一下是可以挽救的,所以有必要通过重试机制来解决。

但是引入超时重试机制后,并非一切完美了。它同样会带来副作用,这些是开发RPC接口必须要考虑,同时也是最容易忽视的问题:

1、重复请求:有可能provider执行完了,但是因为网络抖动consumer认为超时了,这种情况下重试机制就会导致重复请求,从而带来脏数据问题,因此服务端必须考虑接口的幂等性。

2、降低consumer的负载能力:如果provider并不是临时性的抖动,而是确实存在性能问题,这样重试多次也是没法成功的,反而会使得consumer的平均响应时间变长。比如正常情况下provider的平均响应时间是1s,consumer将超时时间设置成1.5s,重试次数设置为2次,这样单次请求将耗时3s,consumer的整体负载就会被拉下来,如果consumer是一个高QPS的服务,还有可能引起连锁反应造成雪崩。

3、爆炸式的重试风暴:假如一条调用链路经过了4个服务,最底层的服务D出现超时,这样上游服务都将发起重试,假设重试次数都设置的3次,那么B将面临正常情况下3倍的负载量,C是9倍,D是27倍,整个服务集群可能因此雪崩。


04 应该如何合理的设置超时时间?

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

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

最后,再简单总结下:

RPC接口的超时设置看似简单,实际上有很大学问。不仅涉及到很多技术层面的问题(比如接口幂等、服务降级和熔断、性能评估和优化),同时还需要从业务角度评估必要性。知其然知其所以然,希望这些知识能让你在开发RPC接口时,有更全局的视野。

  • 面试官问:MySQL的自增ID用完了,怎么办?

  • ArrayList插入1000w条数据之后,我怀疑了jvm...

  • 蚂蚁二面,面试官问我零拷贝的实现原理,当场懵了…

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

同时设置超时时间_刚入职的小菜鸡,设错了RPC超时,搞了个线上事故相关推荐

  1. 刚入行的小菜鸡,怎样做好功能测试?

    常见的功能测试的流程是:需求分析,用例编写,用例评审,提测验证,Bug 回归验证,上线与线上回归. 但是做起来就是:写不完的用例,跑不完的case,天天都要加班. 我以功能测试的工作流程,结合实际的工 ...

  2. 刚入职新公司做一些什么贡献_如果您有全职工作,如何为Kubernetes做贡献

    刚入职新公司做一些什么贡献 当我在IBM产品安全事件响应团队工作时,我于2018年10月开始为Kubernetes (K8s)做出贡献. 我被分布式系统所吸引,但在日常工作中无法与它们合作,因此我的导 ...

  3. 程序员刚入职很痛苦_在中国,程序员这行能干一辈子吗?

    点击上方蓝色字体"腾讯创业" 选择关注公众号 创投圈大小事,你都能尽在掌握 腾讯创业 | ID:qqchuangye 随着互联网行业发展的不断深化,或许也是时候从文化.机制上去探讨 ...

  4. 程序员刚入职很痛苦_程序员做梦都会笑醒的10件事

    最近两年,996.内卷.青年秃头.中年失业-已经成为程序员以及广大"打工人"普遍关丧心的问题.但就像鲁迅所说(并没有):今年可能是未来十年中最好的一年. 为了更好的面对" ...

  5. 华为刚入职研究生薪水多少钱_薪金谈判:如何不放火烧钱

    华为刚入职研究生薪水多少钱 Pop quiz, hot shot. 流行测验,热门人物. You aced the phone screen. This morning's on-site inter ...

  6. 京东某女程序员求助:刚入职就意外怀孕,纠结还能不能过试用期?网友:职场女性太难!...

    女员工在职场上最大的困境就是结婚生子,许多公司在招聘时都会对女员工的结婚.怀孕时间提出要求,导致不少女员工不敢怀孕,生怕一个不小心就被辞退. 一个京东女员工发帖求助:刚入职就怀孕了,真的不是故意的,纠 ...

  7. 转载:刚入职美团两个月,就想离职了,每天加班到吐

    一名刚入职美团两个月的程序员在互联网社区吐槽自己的遭遇:刚入职美团两个月,就想离职了,每天加班到吐. 看样子,这名美团员工不堪加班的压力,已经萌生了离职的想法,但才入职两个月左右,现在离职又担心简历不 ...

  8. 致刚入职场的你 - 程序员的成长笔记

    前言 我们经常在网上会看到这样的文章,你的同龄人正在如何.这是典型的贩卖焦虑的文章.的确,现阶段,刚毕业几年的年轻人,面临车,房子等,有时候压力挺大的.但你过度焦虑的话,每天生活在恐慌当中,你会发现你 ...

  9. java 实习生刚入职都会做些什么工作呢?

    不知道大家还是学生的时候有没有这个问题:公司做的项目和自己在学校练手的项目有多大的区别.我以前在学校跟着视频做一些项目练手, 总感觉公司做的东西会要难很多,不知道跟公司的真实项目区别在哪.总的来说,我 ...

最新文章

  1. DevExpress ASP.NET 使用经验谈(9)-Dev控件客户端事件 ClientSideEvents
  2. IDEA的UML图介绍(一)
  3. 做JAVA开发的同学一定遇到过的爆表问题,看这里解决
  4. mysql 代理 a_Keepalived+Mysql+Haproxy
  5. 【最强VSCode】之管理MySql数据库
  6. 工作58:element三级列表的问题
  7. 从C# String类理解Unicode(UTF8/UTF16)
  8. mysql 前端proxy_【4.分布式存储】-mysql及proxy
  9. 无人驾驶、免费乘坐,硅谷的出租车行业要变天了
  10. html5编辑漂亮静态页面工具_青岛HTML5与Web前端
  11. android wheel控件滚动,android 滚动选择插件 android-wheel
  12. mysql-conn.php_PHP连接MySQL方式
  13. 软件测试之接口自动化面试题汇总
  14. 老李分享:六度分隔理论 2
  15. Android 代码设置默认输入法
  16. 快递鸟 物流跟踪订阅 即时查询快递 预约取件(在线下单)
  17. 谷歌AdMob广告接入(插屏广告)
  18. win10查看端口号、进程
  19. java连连看解说_java—连连看-实现消除
  20. 如何设置路由器并利用路由器+宽带猫实现单机或是多机共享自动拨号上网

热门文章

  1. SolrJ管理SolrCloud
  2. Bootstrap的datatable控件
  3. python 使用全局变量_如何在Python中的不同模块中使用全局变量
  4. python怎么引用已经输入的信息_如何将用户输入存储在python中,然后调用它?
  5. 一天1个机器学习知识点(五)
  6. chrome 插件精选之 github 篇
  7. 只需 5 分钟看完这篇 HTTPS,去阿里面试和面试官扯皮就没问题了!
  8. 轻松读懂数据结构系列:早操排队图解选择排序
  9. 蓝桥杯java第五届决赛第四题--排列序数
  10. 【Intellij IDEA系列】IDEA的Debug调试技巧