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

问题表现

最近的迭代转测后遇到了一个比较有意思的问题。在测试环境整体运行还算平稳,但是过一段时间之后,就开始有接口超时了,日志中出现非常多的 “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 {@AutowiredBooFeignClient 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 {@AutowiredFooFeignClient 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() 调用上了。

总结

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/83941.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/83941.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode - 852. 山脉数组的峰顶索引

题目 852. 山脉数组的峰顶索引 - 力扣&#xff08;LeetCode&#xff09; 思路 使用二分查找来定位峰顶 对于中间元素&#xff0c;比较它与其右侧元素的大小&#xff1a; 如果 arr[mid] < arr[mid1]&#xff0c;说明我们在上坡阶段&#xff0c;峰顶在右侧 如果 arr[mid…

国产ARM/RISCV与OpenHarmony物联网项目(二)网关数据显示

本文需要Web服务器开发基础&#xff0c;可参考下述博文&#xff1a; 物联网网关Web服务器--lighttpd服务器部署与应用测试 物联网网关Web服务器--CGI开发接口 一、数据显示界面与功能设计 1、功能设计说明 程序代码结构如下&#xff0c;调用关系见彩色部分标示。 数据显示界面…

Robyn高性能Web框架系列01:Robyn快速入门

Robyn快速入门 安装 Robyn1、仅安装基础 HTTP 路由功能2、带扩展功能的安装 第一个Robyn程序1、创建Robyn应用2、Say Hello!3、启动Robyn应用 Python世界从来不缺少对于性能的追求&#xff0c;Robyn就是其中之一&#xff0c;它将 Python 的异步功能与 Rust 相结合&#xff0c;在…

微信小程序 -----无限新增删除,同时算出总合算金额。

<view class="refuelMoney-main" style="padding-bottom: 200rpx;"><!-- <view class="add_record">添加加油记录</view> --><view class="refuel-itemTextArea"><text style="width: 35%;&quo…

linux “Permission Denied“解决方案

Linux 编译错误排查 在软件开发过程中&#xff0c;编译错误和版本控制问题是开发者每天都会遇到的挑战。本文将结合实际案例&#xff0c;详细讲解 Linux 环境下常见编译错误的排查方法 权限拒绝错误&#xff08;Permission Denied&#xff09; 当执行脚本或程序时&#xff0…

【慧游鲁博】【15】后台管理系统功能完善:仪表盘、多模态交互日志、简单问答词条管理

文章目录 本次更新多模态交互日志效果涉及代码文件 仪表盘&#xff08;部分&#xff09;效果涉及代码文件 简单问答服务词条管理效果涉及代码文件 本次更新 代码真的太多太多了&#xff0c;不放代码了 多模态交互日志 数据概览与筛选功能 时间范围筛选&#xff1a;提供"…

【力扣 简单 C】21. 合并两个有序链表

目录 题目 解法一&#xff1a;迭代 解法二&#xff1a;递归 题目 解法一&#xff1a;迭代 struct ListNode* merge(struct ListNode* head1, struct ListNode* head2) {struct ListNode* virHead malloc(sizeof(*virHead));struct ListNode* curNode virHead;struct List…

【开源工具】Windows屏幕控制大师:息屏+亮度调节+快捷键一体化解决方案

🖥️ 从零打造Windows屏幕控制大师:息屏+亮度调节+快捷键一体化解决方案 🌈 个人主页:创客白泽 - CSDN博客 🔥 系列专栏:🐍《Python开源项目实战》 💡 热爱不止于代码,热情源自每一个灵感闪现的夜晚。愿以开源之火,点亮前行之路。 🐋 希望大家多多支持,我们一…

pyhton基础【11】函数一

目录 一.函数说明 二.函数介绍 函数简介 作用 函数分类 三.自定义函数 定义函数 调用函数 pass关键字 定义一次执行多次 一.函数说明 Python中的函数是一个重要的编程概念&#xff0c;它允许编程者封装代码块以实现特定的功能。函数的作用和应用场景非常广泛&#xf…

使用Kotlin开发后端服务的核心方法

一、开发步骤 选择框架 Kotlin后端开发常用框架包括Spring Boot、Ktor和Micronaut。Spring Boot生态成熟&#xff0c;适合企业级应用&#xff1b;Ktor轻量且协程友好&#xff0c;适合高性能异步服务&#xff1b;Micronaut以低内存占用和快速启动见长。 搭建项目结构 通过Grad…

java面试总结-20250616

题目1: 求一个int类型正整数二进制中最高位1的位置&#xff1f; 比如10&#xff0c;二进制位1010&#xff0c;最高位1所在位置位4。 解体思路&#xff1a; 使用高位扩散&#xff0c;将1010扩散位1111使用二分法&#xff0c;计算32位二进制中1111前面0的位数n&#xff1b;结果…

Black自动格式化工具

文章目录 一、Black自动格式化工具二、格式化行为的核心内容1. 统一缩进和空格规则2. 括号换行&#xff1a;一致的多行结构展开3. 字符串风格统一4. 函数/类定义中的空行规则5. import 排序&#xff08;建议搭配 isort&#xff09;6. 注释不动、换行优雅7. 可配置项极少&#x…

项目拓展-简易SQL监控,P6SPY拦截所有jdbc连接并打印执行SQL

介绍一下P6spy驱动 p6spy 是一款开源的数据库监控框架&#xff0c;主要用于 拦截和记录应用程序与数据库之间的所有交互&#xff08;如 SQL 语句、参数、执行时间等&#xff09; 它通过包装现有的 JDBC 驱动&#xff08;如 MySQL JDBC 驱动&#xff09;&#xff0c;在不修改业…

洛谷B3951 [GESP样题 五级] 小杨的队列

题目描述 小杨的班级里共有 N N N 名同学&#xff0c;学号从 0 0 0 至 N − 1 N-1 N−1。某节课上&#xff0c;老师要求同学们进行列队。具体来说&#xff0c;老师会依次点名 M M M 名同学&#xff0c;让他们加入队伍。每名新入队的同学需要先站到队伍末尾&#xff08;刚开…

Java编程之外观模式

前言 想象你要去一家很复杂的餐厅吃饭&#xff0c;但不想自己点菜、排队、找位置&#xff0c;也不想管厨房、洗碗、送餐这些后端流程。你只需要告诉餐厅服务员“我要一份牛排套餐”&#xff0c;然后坐等就好。这个服务员&#xff0c;就是外观模式&#xff08;Facade Pattern&a…

告别 Java 开发困境!飞算 JavaAI 开发助手开启智能编程新时代

在 Java 开发的世界里&#xff0c;需求不明确、加班写重复代码、被 BUG 搞得焦头烂额&#xff0c;是许多开发者难以摆脱的 “三座大山”。需求文档模糊不清&#xff0c;让开发者在项目起始阶段就陷入迷茫&#xff1b;大量重复性的代码编写工作&#xff0c;不仅消耗时间和精力&a…

Node.js 中两种模块导出方式区别

两种模块到处方式 exports.xxx ... module.exports ... 1. exports.xxx ... exports 是 module.exports 的一个引用&#xff08;快捷方式&#xff09;。 当你写 exports.foo function() {}&#xff0c;实际上就是给 module.exports 对象添加了一个 foo 属性。 这种方式…

电脑出问题了,无网络环境下一键快速重装系统

在电脑使用过程中&#xff0c;系统故障、卡顿、崩溃等问题屡见不鲜。面对这些情况&#xff0c;重装系统往往是解决问题的最有效手段之一。然而对于刚接触计算机操作的新用户来说&#xff0c;如何安全、稳定地完成系统重装&#xff0c;仍是一个颇具挑战的任务。 这一款专为新手…

基于区块链的去中心化身份验证系统:原理、实现与应用

前言 在数字化时代&#xff0c;身份验证是网络安全和隐私保护的核心环节。传统的身份验证系统依赖于中心化的机构&#xff0c;如政府、银行或互联网服务提供商&#xff0c;这些机构存储和管理用户的个人信息。然而&#xff0c;中心化系统存在诸多问题&#xff0c;如数据泄露风险…

React forwardRef 与 useImperativeHandle 深度解析

在React开发中&#xff0c;组件间的通信是一个核心话题。虽然props和state能够处理大部分场景&#xff0c;但有时我们需要更直接的方式来操作子组件。今天我们来深入探讨两个强大的React Hook&#xff1a;forwardRef和useImperativeHandle。 forwardRef&#xff1a;传递引用的…