最近经过反复测试,遇到了一个比较有意思的问题。测试环境整体运行还算稳定,但使用一段时间后,部分接口开始超时,日志中出现大量“java.net.SocketTimeoutException:Readtimedout”。试过几次重启大法,每次都只能坚持一段时间,然后又出现了SocketTimeoutException。注意:在测试环境遇到问题重启服务不是一个好的做法,因为重启可能会破坏不容易出现的问题站点。如果问题在测试环境无法复现,发布后出现在生产环境,不仅会造成生产运维事件,解决问题的压力也很大。初步分析按照测试报告的问题场景,跟踪调用链上相关服务的日志,发现微服务之间存在循环依赖调用。一般情况可以抽象如下(图中所有调用都是http协议):Client调用serviceFoo.hello()Foo.hello()逻辑中会调用serviceBoo.boo()Boo.boo()并回调到服务Foo的另一个方法another(),当然真实场景比这个复杂,调用链也更长,但最终形成循环依赖调用。至于为什么这个循环依赖会导致超时,当时想了很多可能,比如数据库查询慢、数据库锁、分布式锁等等。但是整个调用链都是查询请求,查询相关的数据量很小,所以不会有锁。问题发生时也没有与查询数据相关的数据库写入请求。鉴于这个循环依赖调用确实是本次迭代版本中引入的变化,虽然因果关系的原理还没有弄清楚,但是这个循环依赖调用还是非常可疑的,是一个不必要的循环调用。抱着去掉循环依赖调用试一试的态度,进行了修复。修复后,SocketTimeoutException不再发生。问题已经解决了。寻找原因问题不再出现,但侥幸解决的问题往往并没有真正解决。只有搞清楚背后的原理,才能真正确认问题是否是这个原因造成的,这样的修复是否真的解决了问题。通过假设振铃呼叫是呼叫超时的直接原因。让我们看看是否可以推断出因果关系。通过更详细地绘制Foo服务容器,如下图所示:通过这张图我们可以发现,如果容器中接收请求的线程池正在等待服务Boo.boo()的响应,而Boo需要回调服务Foo.another()。这时候如果所有的线程都处于这个状态,我们会发现服务Foo容器中已经没有线程来处理Boo的请求了。关注公众号:码猿技术专栏,回复关键词:1111获取阿里巴巴内部Java性能调优手册!在某种程度上,这是一个僵局。至此,我们可以确定,这个循环依赖调用就是导致调用超时的罪魁祸首。当客户端发起的请求速度大于这个循环调用链的处理速度时,就会慢慢导致所有为Foo服务的线程进入这个死锁状态。确认此处只列出了键码。具体代码可以参考gitee项目:https://gitee.com/donghbcn/CircularDependencyEurekaServer搭建一个简单的项目来启动Eureka服务器。服务Foo创建一个SpringBoot项目来实现Foo服务。Foo通过FeignClient调用Boo服务。设置默认容器Tomcat的最大线程数为16,而Tomcat默认配置最大线程数为200,对于验证场景来说有点太大了,要等很久才能看到效果.application.propertiesspring.application.name=demo-fooserver.port=8000eureka.client.serviceUrl.defaultZnotallow=http://localhost:8080/eurekaserver.tomcat.threads.max=16packagecom.cd.demofoo;导入org.springframework。beans.factory.annotation.Autowired;导入org.springframework.web.bind.annotation.RequestMapping;导入org.springframework.web.bind.annotation.RestController;@RestControllerpublicclassFooController{@AutowiredBooFeignClientbooFeignClient;@RequestMapping("/hello")publicStringhello(){longstart=System.currentTimeMillis();System.out.println(["+Thread.currentThread()+"]foo:hellocalled,callboo:boonow");booFeignClient.boo();System.out.println(["+Thread.currentThread()+"]foo:hellocalled,callboo:boo,totalcost:"+(System.currentTimeMillis()-start));返回“你好世界”;}@RequestMapping("/另一个")PUblicStringanother(){longstart=System.currentTimeMillis();try{//通过sleepp模拟一次耗时调用Thread.sleep(100);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("foo:另一个被调用的,总成本:"+(System.currentTimeMillis()-start));返回“另一个”;}}ServiceBoo创建一个SpringBoot项目实现Boo服务Boo通过FeignClient调用Foo服务。包com.cd.demoboo;导入org.springframework.beans.factory.annotation.Autowired;导入org.springframework.web.bind.annotation.RequestMapping;导入org.springframework.web.bind.annotation.RestController;@RestControllerpublicclassBooController{@AutowiredFooFeignClientfooFeignClient;@RequestMapping("/boo")publicStringboo(){longstart=System.currentTimeMillis();fooFeignClient.另一个();System.out.println("boo:boo调用,调用foo:另一个,总成本:"+(System.currentTimeMillis()-start));返回“嘘”;}}Jmeter使用Jmeter模拟并发Client调用。配置30个线程,无限循环。很快服务Foo日志就卡住了。过了一段时间,Boo的log中开始出现SocketTimeoutException,如下图:jstack通过jstack可以看到Foo进程的所有线程都卡在了hello()调用上。综上所述,微服务之间的循环依赖类似于类之间的循环依赖。当依赖关系形成循环时,会造成严重的问题:微服务不能直接形成循环调用,否则很容易造成死锁状态。微服务之间的耦合性很强,严重违背了微服务的初衷;这种情况往往是由于服务之间的调用缺乏约束造成的。为了方便数据的检索或更新,可以随意调用服务。一个用“微服务”设计的系统,会逐渐演变成一个大的分布式单元。
