当前位置: 首页 > 科技观察

一篇带你了解接口突然超时的十大罪过的文章

时间:2023-03-19 21:55:51 科技观察

前言不知道大家有没有遇到过这样的场景:我们提供的一个API接口的响应时间一直都很快,但是在一些不小心就在这个时候,界面突然超时了。或许你会有些疑惑,为什么呢?今天就和大家聊聊界面突然超时的10个原因,希望对大家有所帮助。1.网络异常界面本来好好的,突然超时了。最常见的原因可能是网络异常。例如:偶尔的网络抖动,或者带宽被占满。(1)网络抖动经常上网的我们一定遇到过这样的场景:大多数情况下,我们快速访问某个网站,但偶尔网页会一直转来转去,无法加载。可能是你的网络抖动,丢包了。当网页请求API接口,或接口返回数据给网页时,可能会出现网络丢包。网络丢包可能会导致接口超时。(2)带宽被占满有时,由于页面或界面设计不合理,当用户请求突然增加时,服务器的网络带宽可能会被占满。服务器带宽是指一定时间内传输数据的大小,例如:1秒传输10M数据。如果用户请求量突然变大,超过每秒10M的上限,例如:每秒100M,而服务器带宽本身每秒只能传输10M,这将导致这1秒内延迟传输90M数据,导致接口超时。因此,对于一些高并发请求的场景,需要评估是否需要增加服务器带宽。2、线程池里全是我们调用的API接口。有时出于性能考虑,可能会使用线程池来异步查询数据,最后将查询结果汇总并返回。如下图所示:调用远程接口的总耗时为200ms=200ms(即耗时最长的远程接口调用)。在java8之前,可以通过实现Callable接口来获取线程返回的结果。java8之后,这个功能是通过CompleteFuture类来实现的。这里以CompleteFuture为例:publicUserInfogetUserInfo(Longid)throwsInterruptedException,ExecutionException{finalUserInfouserInfo=newUserInfo();CompletableFutureuserFuture=CompletableFuture.supplyAsync(()->{getRemoteUserAndFill(id,Booleinfo)anreturnTRUE;},executor);CompletableFuturebonusFuture=CompletableFuture.supplyAsync(()->{getRemoteBonusAndFill(id,userInfo);returnBoolean.TRUE;},executor);CompletableFuturegrowthFuture=CompletableFuture.supplyAsync(()->{getRemoteGrowthAndFill(id,userInfo);returnBoolean.TRUE;},executor);CompletableFuture.allOf(userFuture,bonusFuture,growthFuture).join();userFuture.get();bonusFuture.get();growthFuture.get();returnuserInfo;}这里我使用了executor,也就是自定义的线程池,目的是防止高并发场景下线程过多的问题。但是,如果用户请求过多,线程池中已有的线程无法处理,线程池会将多余的请求放入队列queue中,等待空闲线程处理。如果队列中有很多任务在排队,某个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.一次返回的数据过多。不知道大家有没有遇到过这样的需求:我们有一个作业,每天定时调用第三方API查询接口,获取昨天更新的数据,然后更新到我们自己的数据库表中。由于第三方每天更新的数据不多,所以这个API接口的响应时间还是比较快的。但是突然有一天,API接口出现了接口超时的问题。查看日志发现API接口一次返回的数据过多,而且数据的更新时间都是一样的。可以推断是API接口提供者进行了批量更新操作,修改了大量数据,导致出现该问题。即使我们在job中加入失败重试机制,由于API一次返回的数据过多,重试很可能会导致接口超时,从而导致无法从第三方获取最新数据前一天。.所以第三方根据日期查询增量数据的接口,建议分页。不然以后哪天遇到批量更新操作,可能会出现接口超时的情况。7、死循环死循环也会导致界面超时?死循环不应该在接口测试阶段发现,为什么要在生产环境中发现呢?事实上,大多数死循环问题都可以在测试阶段发现。但是有些无限递归隐藏的比较深,比如下面这种情况。死循环其实有两种:无限递归的普通死循环(1)常见的死循环,有时死循环是自己写的,比如下面的代码:while(true){if(condition){break;}System.out.println("dosamething");}这里用到了while(true)的循环调用,在CAS自旋锁中经常用到。当条件为真时,循环自动退出。如果条件很复杂,一旦判断不正确,或者缺少一些逻辑判断,在某些场景下可能会出现死循环。死循环的出现极有可能是开发者人为bug造成的,但这种情况很容易被发现。还有一个隐藏很深的死循环,是代码写的不太严谨造成的。如果使用正常数据,可能检测不到问题,但一旦出现异常数据,马上就会出现死循环。(2)无限递归如果要打印一个类别的所有父类别,可以使用这样的递归方法:publicvoidprintCategory(Categorycategory){if(category==null||category.getParentId()==null){返回;}System.out.println("父类名:"+category.getName());父类别=categoryMapper.getCategoryById(category.getParentId());printCategory(parent);}一般情况下,这段代码是没有问题的。但是如果有人弄错了,把一个类别的parentId指向自己,就会出现无限递归。这样一来,接口就不能一直返回数据,最终会出现栈溢出。建议在编写递归方法时设置递归深度。比如分类的最大层级为4,那么深度可以设置为4。然后在递归的方法中进行判断。如果深度大于4,会自动返回,这样可以避免无限递归。8、sql语句没有走索引。你有没有遇到过这样的情况:明明是同一个sql,只是入参不同而已。有时取索引a,有时取索引b?没错,有时候mysql选错索引,有时候连索引都不用。在执行一条SQL语句之前,mysql会通过抽样统计来估计扫描到的行数,最后根据受影响的行数、区分度、基数、数据页等信息综合评估使用哪个索引。有时传入参数1,SQL语句就到索引a,执行时间很快。但是有时候传入参数2,sql语句就走到索引b,执行时间明显慢很多。这可能会导致API接口出现超时问题。如果需要,可以使用forceindex强制查询sql到某个索引。9、服务OOM之前遇到过这样一个场景:一个接口按照id分类查询,id为主键,sql语句可以使用主键索引,但是也出现了接口超时的问题。当时觉得有点不可思议,因为这个接口平均只用了十几毫秒,怎么会有超时呢?但是从当时的日志来看,接口响应时间为5秒,确实存在接口超时问题。最后从Prometheus的服务内存监控,发现了OOM问题。实际上是由于OOM内存溢出导致API接口部署的服务暂停了一段时间。当时所有的接口都有请求超时的问题。但是,由于K8S集群被监控,它会自动杀死挂起的服务节点,并在容器中重新部署一个新的服务节点。好在对用户影响不大。10、在调试中,我们有时需要在本地开发工具,比如:在idea中,直接连接测试环境的数据库,调试某个API接口的业务逻辑。因为在开发环境中,有些问题是不容易重现的。为了排查某个bug,你在请求??某个本地接口的时候开启了debug模式,逐行跟踪代码,排查问题。当我来到某一行代码时,我停留了很长时间。这行代码主要是更新某条数据。此时测试同学在相关业务页面更新相同的数据。这也可能导致数据库死锁问题。由于你从来没有在idea的debug模式下提交过事务,所以会造成很长的死锁时间,从而导致业务页面请求的API接口出现超时问题。