知道这10个让你的API接口突然超时的原因吗?

打印 上一主题 下一主题

主题 561|帖子 561|积分 1683

前言

不知道你有没有遇到过如许的场景:我们提供的某个API接口,相应时间本来不停都很快,但在某个不经意的时间点,突然出现了接口超时。
也许你会有点懵,到底是为什么呢?
今天跟各人一起聊聊接口突然超时的10个原因,希望对你会有所帮助。
1.网络非常

接口本来好好的,突然出现超时,最常见的原因,可能是网络出现非常了。比如:偶尔的网络抖动,或者是带宽被占满了。
1.1 网络抖动

常常上网的我们,肯定遇到过如许的场景:大多数情况下我们访问某个网站很快,但偶尔会出现网页不停转圈,加载不出来的情况。
有可能是你的网络出现了抖动,丢包了。
网页哀求API接口,或者接口返回数据给网页,都有可能会出现网络丢包的情况。
网络丢包可能会导致接口超时。
2.1 带宽被占满

有时候,由于页面或者接口设计不公道,用户哀求量突增的时候,可能会导致服务器的网络带宽被占满的情况。
服务器带宽指的是在一定时间内传输数据的大小,比如:1秒传输了10M的数据。
假如用户哀求量突然增多,超出了1秒10M的上限,比如:1秒100M,而服务器带宽本身1秒就只能传输10M,如许会导致在这1秒内,90M数据就会延迟传输的情况,从而导致接口超时的发生。
   以是对于有些高并发哀求场景,需要评估一下是否需要增长服务器带宽。
  2.线程池满了

我们调用的API接口,有时候为了性能思量,可能会使用线程池异步查询数据,末了把查询结果举行汇总,然后返回。
如下图所示:

调用远程接口总耗时 200ms = 200ms(即耗时最长的那次远程接口调用)
在java8之前可以通过实现Callable接口,获取线程返回结果。
java8以后通过CompleteFuture类实现该功能。我们这里以CompleteFuture为例:
  1. public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {
  2.     final UserInfo userInfo = new UserInfo();
  3.     CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {
  4.         getRemoteUserAndFill(id, userInfo);
  5.         return Boolean.TRUE;
  6.     }, executor);
  7.     CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {
  8.         getRemoteBonusAndFill(id, userInfo);
  9.         return Boolean.TRUE;
  10.     }, executor);
  11.     CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> {
  12.         getRemoteGrowthAndFill(id, userInfo);
  13.         return Boolean.TRUE;
  14.     }, executor);
  15.     CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();
  16.     userFuture.get();
  17.     bonusFuture.get();
  18.     growthFuture.get();
  19.     return userInfo;
  20. }
复制代码
这里我用到了executor,体现自定义的线程池,为了防止高并发场景下,出现线程过多的问题。
但假如用户哀求太多,线程池中已有的线程处理不外来,线程池会把多余的哀求,放到队列中排队,等候空闲线程的行止理。
假如队列中排队的使命非常多,某次API哀求不停在等候,没办法得到及时处理,就会出现接口超时问题。
这时候,我们可以思量是否核心线程数设置太小了,或者有多种业务场景共用了同一个线程池。
假如是因为核心线程池设置太小,可以将其调大一些。
假如是因为多种业务场景共用了同一个线程池,可以拆分成多个线程池。
3.数据库死锁

有时候接口超时得有点莫名其妙,特别是遇到数据库出现死锁的时候。
你提供的API接口中通过某个id更新某条数据,此时,恰好线上在手动执行一个批量更新数据的sql语句。
该sql语句在一个事务当中,而且刚好也在更新那条数据,可能会出现死锁的情况。
由于该sql语句执行时间很长,会导致API接口的那次更新数据操作,长时间被数据库锁住,没法纵然返回数据,而出现接口超时问题。
你说坑不坑?
以是发起在执行数据库批量操作前,一定要评估数据的影响范围,不要一次性更新太多的数据,否则可能会导致很多意想不到的问题。
别的,批量更新操作发起在用户访问少的时段执行,比如:破晓。
4.传入参数太多

有时候,偶尔的一次接口超时,是由于参数传入太多导致的。
例如:根据id集合批量查询分类接口,假如传入的id集合数据量不多,传入几十个或上百个id,不会出现性能问题。毕竟id是分类表的主键,可以走主键索引,数据库的查找速度黑白常快的。
但假如接口调用方,一次性传入几千个,甚至几万个id,批量查询分类,也可能会出现接口超时问题。
因为数据库在执行sql语句之前,会评估一下耗时情况,查询条件太多,有可能走全表扫描更快。
以是这种情况下sql语句可能会丢失索引,让执行时间变慢,出现接口超时问题。
因此我们在设计批量接口的时候,发起要限制传入的集合的大小,比如:500。
假如凌驾我们设置最大的集合大小,则接口直接返回失败,并提示给用户:一次性传入参数过多。
   该限制一定要写到接口文档中,避免接口调用方,在生产环境调用接口失败而踩坑。要在接口开辟阶段通知到位。
  别的,假如接口调用方要传入的参数就是很多怎么办?
答:可能是需求不公道,或者系统设计有问题,我们要尽量在系统设计阶段就规避这个问题。
假如我们重新举行系统设计改动比较大的话,有个临时的办理方案:在接口调用方中多线程分批调用该接口,末了将结果举行汇总。
5.超时时间设置过短

通常情况下,发起我们在调用远程API接口时,要设置连接超时时间和读超时时间这两个参数,而且可以动态配置。
如许做的利益是,可以防止调用远程API接口万一出现了性能问题,相应时间很长,把我们本身的服务拖挂的情况发生。
比如:你调用的远程API接口,要100秒才返回数据,而你设置的超时时间是100秒。这时1000个哀求过来,去哀求该API接口,如许会导致tomcat线程池很快被占满,导致整个服务暂时不可用,至少新的哀求过来,是没法纵然相应的。
以是我们需要设置超时时间,而且超时时间还不能设置太长。
并发量不大的业务场景,可以将这两个超时时间设置稍微长一点,比如:连接超时时间为10秒,读超时时间为20秒。
并发量大的业务场景,可以设置成秒级或者毫秒级。
有些小同伴为了开辟方便,在多种业务场景共用这两个超时时间。
某一天,在并发量大的业务场景中,你将该超时时间改短了。
但直接导致并发量不大的业务场景中,出现调用API接口超时的问题。
   因此,不发起多种业务场景共用同一个超时时间,最好根据并发量的不同,单独设置不同的超时时间。
  6.一次性返回数据太多

不知道你有没有遇到过如许的需求:我们有个job,每天定时调用第三方API查询接口,获取昨天更新的数据,然后更新到我们本身的数据库表中。
由于第三方每天更新的数据不多,以是该API接口相应时间照旧比较快的。
但突然有一天,该API接口却出现了接口超时问题。
检察日志发现,该API接口一次性返回的数据太多,而且该数据的更新时间雷同。
这就可以断定,该API接口提供方举行了批量更新操作,修改了大量的数据,导致该问题的发生。
纵然我们在job中加了失败重试机制,但由于该API一次性返回数据着实太多太多,重试也很有可能会接口超时,如许会导致不停获取不到第三方前一天最新的数据。
   以是第三方这种根据日期查询增量数据的接口,发起做身分页查询的,否则背面没准哪一天,遇到批量更新的操作,就可能出现接口超时的问题。
  7. 死循环

死循环也会导致接口超时?
死循环不应该在接口测试阶段就发现了,为什么要到生产环境才发现?
确实,绝大部分死循环问题,在测试阶段可以发现。
但有些无限递归隐藏的比较深,比如下面的情况。
死循环其实有两种:

  • 平凡死循环
  • 无限递归
7.1 平凡死循环

有时候死循环是我们本身写的,例如下面这段代码:
  1. while(true) {
  2.     if(condition) {
  3.         break;
  4.     }
  5.     System.out.println("do samething");
  6. }
复制代码
这里使用了while(true)的循环调用,这种写法在CAS自旋锁中使用比较多。
当满足condition即是true的时候,则主动退出该循环。
假如condition条件非常复杂,一旦出现判断不正确,或者少写了一些逻辑判断,就可能在某些场景下出现死循环的问题。
出现死循环,大概率是开辟人员人为的bug导致的,不外这种情况很轻易被测出来。
另有一种隐藏的比较深的死循环,是由于代码写的不太严谨导致的。假如用正常数据,可能测不出问题,但一旦出现非常数据,就会立即出现死循环。
7.2 无限递归

假如想要打印某个分类的所有父分类,可以用类似如许的递归方法实现:
  1. public void printCategory(Category category) {
  2.   if(category == null 
  3.       || category.getParentId() == null) {
  4.      return;
  5.   } 
  6.   System.out.println("父分类名称:"+ category.getName());
  7.   Category parent = categoryMapper.getCategoryById(category.getParentId());
  8.   printCategory(parent);
  9. }
复制代码
正常情况下,这段代码是没有问题的。
但假如某次有人误操作,把某个分类的parentId指向了它本身,如许就会出现无限递归的情况。导致接口不停不能返回数据,最终会发生堆栈溢出。
   发起写递归方法时,设定一个递归的深度,比如:分类最大等级有4级,则深度可以设置为4。然后在递归方法中做判断,假如深度大于4时,则主动返回,如许就能避免无限递归的情况。
  8.sql语句没走索引

你有没有遇到过如许一种情况:明明是同一条sql,只有入参不同而已。有的时候走的索引a,有的时候却走的索引b?
没错,有时候mysql会选错索引,甚至有时会不走索引。
mysql在执行某条sql语句之前,会通过抽样统计来估算扫描行数,根据影响行数、区分度、基数、数据页等信息,末了综合评估走哪个索引。
有时候传入参数1,sql语句走了索引a,执行时间很快。但有时候传入参数2,sql语句走了索引b,执行时间显着慢了很多。
如许有可能会导致API接口出现超时问题。
   必要时可以使用force index来欺凌查询sql走某个索引。
  9.服务OOM

我之前遇到过如许一种场景:一个根据id查询分类的接口,该id是主键,sql语句可以走主键索引,竟然也出现了接口超时问题。
我当时觉得有点不可思议,因为这个接口平均耗时只有十几毫秒,怎么可能会出现超时呢?
但从当时的日志看,接口相应时间有5秒,简直出现了接口超时问题。
末了从Prometheus的服务内存监控中,查到了OOM问题。
其实该API接口摆设的服务当时由于OOM内存溢出,其实挂了一段时间。
当时所有的接口都出现了哀求超时问题。
但由于K8S集群有监控,它主动会将挂掉的服务节点kill掉,而且在容器中重新摆设了一个新的服务节点,幸好对用户没造成太大的影响。
10.在debug

我们有时候需要在当地开辟工具,比如:idea中,直接连接测试环境的数据库,调试某个API接口的业务逻辑。
因为在开辟环境,某些问题不太好复现。
为了排查某个bug,你在哀求某个当地接口时,开启了debug模式,一行行的跟踪代码,排查问题。
走到某一行代码的时候,停留了很长一段时间,该行代码主要是更新某条数据。
此时,测试同学在相关的业务页面中,操作更新了雷同的数据。
这种也可能会出现数据库死锁的问题。
由于你在idea的debug模式中,不停都没有提交事务,会导致死锁的时间变得很长,从而导致业务页面哀求的API接口出现超时问题。
当然假如你对通例的接口超时问题比较感兴趣,可以看看我的另一篇文章,内里有非常详细的介绍。

 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

吴旭华

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表