当前位置: 欣欣网 > 码农

微服务循环依赖引发惨案,有坑!

2024-07-16码农

👉 欢迎 ,你将获得: 专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目: 【从零手撸:仿小红书(微服务架构)】 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., ;

  • 【从零手撸:前后端分离博客项目(全栈开发)】 2期已完结,演示链接: http://116.62.199.48/ ;

  • 截止目前, 累计输出 50w+ 字,讲解图 2200+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,

    最近的迭代转测后遇到了一个比较有意思的问题。在测试环境整体运行还算平稳,但是过一段时间之后,就开始有接口超时了,日志中出现非常多的 「java.net.SocketTimeoutException: Read timed out」。试了几次重启大法,每次都是只能坚持一会之后,再次出现 SocketTimeoutException。

    注意 :在测试环境于遇到问题重启服务,并不是一个好的实践,因为重启可能会让不容易出现的问题现场被破坏。如果问题在测试环境不能再重新,却在发版后出现在生产环境的话,那不仅会造成生产运维事件,还要在巨大的压力下去解决问题。

    初步分析

    顺着测试汇报的出现问题的场景,跟踪调用链上相关服务的日志,发现出现了微服务之间循依赖调用。大致情况可以抽象如下所示(图中所有调用都是 http 协议):

    图片
  • Client 调用服务 Foo.hello()

  • Foo.hello() 逻辑中会调用服务 Boo.boo()

  • Boo.boo() 又调用回服务 Foo 的另外一个方法 another()

  • 当然真实的场景要比较这个复杂,调用链更长,不过最终形成了环形依赖调用。至于这个环形依赖为什么回导致超时,当时想了多种可能,比如数据库慢查询、数据库锁、分布式锁等等。但是整个调用链上都是查询请求,而且查询相关的数据量也非常小,不会有锁存在。发生问题的时候也没有与查询数据相关的数据库写请求。

    鉴于这个环形依赖调用确实是这个迭代版本中引入的变更,以及虽然没有理清其中的因果关系原理,但是这个环性依赖调用还是很可疑的,而且是不必要的环形调用。就抱着将环形依赖调用去掉试试看的态度,做了修复。修复完后,SocketTimeoutException 不再出现了。问题解决了。

    探寻原因

    问题虽然不再出现,但是凭运气解决的问题,通常有可能不是真的的解决。只有弄清楚背后的原理,我们才能真正的确认问题是不是这个原因导致的,这样的修复是不是真的把问题解决了。

    通过假设环形调用就是导致调用超时的直接原因。我们看看能不能推出因果关系。通过把Foo 服务容器画的更详细一点,如下图:

    图片

    通过这个图示,我们可以发现,如果容器中接收请求的线程池如果都在等待服务Boo.boo() 的响应,而 Boo 又需要调用回服务 Foo.another()。这个时候,如果所有的线程都处于这样的状态,我们就会发现服务 Foo 容器中以及没有线程来处理 Boo 的请求了。某种程度上来说就是死锁了。到这里,我们就可以很确定了,这个环形依赖调用就是导致出现调用超时的罪魁祸首。当 client 发起的请求速度大于这个环形调用链的处理速度的时候,慢慢的就会导致服务 Foo 的所有线程都进入这种死锁状态。

    验证

    这里只列出关键的代码,具体的代码可以参考 gitee 工程:https://gitee.com/donghbcn/CircularDependency

    Eureka 服务器

    建个简单工程将Eureka server启动起来。

    服务 Foo

    创建 SpringBoot 工程实现 Foo 服务。Foo 通过 FeignClient 调用 Boo 服务。设置缺省的容器 Tomcat 的最大线程数为 16,Tomcat 默认配置最大线程数 200,对于验证这个场景有点了大了,要看到效果需要等的时间有点长。

    application.properties

    spring.application.name=demo-foo
    server.port=8000
    eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka
    server.tomcat.threads.max=16
    package com.cd.demofoo;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    @RestController
    public class FooController {
    @Autowired
    BooFeignClient booFeignClient;
    @RequestMapping("/hello")
    public String hello(){
    long start = System.currentTimeMillis();
    System.out.println("[" + Thread.currentThread() +
    "] foo:hello called, call boo:boo now");
    booFeignClient.boo();
    System.out.println("[" + Thread.currentThread() +
    "] foo:hello called, call boo:boo, total cost:" +
    (System.currentTimeMillis() - start));
    return"hello world";
    }
    @RequestMapping("/another")
    public String another(){
    long start = System.currentTimeMillis();
    try {
    //通过 slepp 模拟一个耗时调用
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("foo:another called, total cost:" + (System.currentTimeMillis() - start));
    return"another";
    }
    }

    服务 Boo

    创建 SpringBoot 工程实现 Boo 服务。Boo 通过 FeignClient 调用 Foo 服务。

    package com.cd.demoboo;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    @RestController
    public class BooController {
    @Autowired
    FooFeignClient fooFeignClient;
    @RequestMapping("/boo")
    public String boo(){
    long start = System.currentTimeMillis();
    fooFeignClient.another();
    System.out.println("boo:boo called, call foo:another, total cost:" +
    (System.currentTimeMillis() - start));
    return"boo";
    }
    }



    Jmeter

    采用 Jmeter 来模拟并发 Client 调用。配置了30 个 线程,无限循环。

    图片

    很快服务 Foo 日志就卡死了。过一会 Boo 的日志开始出现 SocketTimeoutException,如下图:

    图片

    jstack

    通过 jstack 我们可以看到 Foo 进程的所有线程都卡在 hello() 调用上了。

    图片

    总结

    微服务之间的环形依赖类似于类之间的循环依赖,当依赖关系形成了环,会造成比较严重的问题:

  • 微服务直接不能形成环形调用,否则非常容易出现死锁状态

  • 微服务之间的耦合性非常强,这严重违反了微服务的初衷;这种情况往往是服务之间的调用没有约束导致的,为了方便取到或更新数据,服务之间可以随意的调用,以」微服务「为设计目标的系统会逐渐演变成一个 分布式大单体

  • 👉 欢迎 ,你将获得: 专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目: 【从零手撸:仿小红书(微服务架构)】 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., ;

  • 【从零手撸:前后端分离博客项目(全栈开发)】 2期已完结,演示链接: http://116.62.199.48/ ;

  • 截止目前, 累计输出 50w+ 字,讲解图 2200+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,


    1. 

    2. 

    3. 

    4. 

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

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

    PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下在看,加个星标,这样每次新文章推送才会第一时间出现在你的订阅列表里。

    「在看」支持小哈呀,谢谢啦