Redian新闻
>
微服务循环依赖调用引发的血案

微服务循环依赖调用引发的血案

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:juejin.cn/post/
7090331001485787149

问题表现

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

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

初步分析

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

  • Client 调用服务 Foo.hello()
  • Foo.hello() 逻辑中会调用服务 Boo.boo()
  • Boo.boo() 又调用回服务 Foo 的另外一个方法 another()

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

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

探寻原因

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

通过假设环形调用就是导致调用超时的直接原因。我们看看能不能推出因果关系。通过把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() 调用上了。

总结

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

  • 微服务直接不能形成环形调用,否则非常容易出现死锁状态
  • 微服务之间的耦合性非常强,这严重违反了微服务的初衷;这种情况往往是服务之间的调用没有约束导致的,为了方便取到或更新数据,服务之间可以随意的调用,以”微服务“为设计目标的系统会逐渐演变成一个分布式大单体


欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

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

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
Go二次开发实战:K8s、Prometheus、Traefk的微服务网关SpringCloud Gateway网关为认证中心和用户微服务构建统一的认证授权入口一行代码引发的“血案”:欧洲航天局价值 5 亿欧元的火箭,发射 40 秒后凌空爆炸让远程成为本地,微服务后端开发的福音您需要模块,而不是微服务微服务虽已老生常谈,但生命力超出不少人想象 | 解读微服务的2022五四运动反对文言文提倡白话文难住了,微服务之间的几种调用方式哪种最佳?是的。对个体来说,还是法治清爽。但中国的“大局观”虽因压制个体而遭到反抗与蔑视,但却一直在整体层面起作用。悖论是传奇程序员用“考古”方式剖析微服务利弊:我们都被骗了?除夕前的血案:连杀6人的杨功讯给社会出了一道难题...从源码层面深度剖析 Spring 循环依赖微服务开发平台Spring Cloud Blade部署实践中财办两位局长在人民日报撰文,讲清“内循环”与“外循环”要义趣图:部署软件时误用了循环依赖Twitter下架部分微服务,是微服务错了?参会者调研结果出炉:微服务、集群调度、研发效能最受关注|QCon北京站闭幕Redis实现微博好友功能微服务(关注,取关,共同关注)100美元引发的血案:谁应对这场悲剧负责?Java 微服务随机掉线排查过程Flag Boot:基于范畴论的新一代极简开源微服务框架解禁今宵博诱莱我被微服务坑掉了CTO职位字节跳动在 Rust 微服务方向的探索和实践 | QCon让无服务器微服务超越容器,开发工具初创公司Fermyon 推出 WebAssembly 云Serverless时代的微服务开发指南:华为云提出七大实践新标准马蜂窝如何利用 APISIX 网关实现微服务架构升级两千年秦兵马俑冒充人类作者,ChatGPT等滥用引担忧,一文综述AI生成文本检测方法一瓶酒引发的血案!目击者曝光多伦多8名少女杀人案细节!如何更好地干掉微服务架构复杂性?大江健三郎:我的血管里流淌着中国文学的血液布碌仑公寓突发血案:75岁老翁被活活打死 头被砸的血肉模糊!GitHub 前 CTO:全面微服务是最大的架构错误!网友:这不是刚改完吗...这年头靠稿费能养活自己吗?
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。