skywalking 引起 spring-cloud-gateway 的内存溢出 skywalking的bug

大家好,我是烤鸭:

   又是个线上问题记录,这次坑惨了,开源软件也不是万能的,还是要做好压测和灰度。

问题

上游反馈大量超时,不止某一个服务,查看服务没有问题,猜测是网络或者环境问题。

想到网关接入了skywaling(已接入24小时),回滚后问题消失。

堆内存在某个时间点后上升且无法回收。
在这里插入图片描述

Full GC 时间变得特别长…这个就是上游超时的原因

在这里插入图片描述

环境

cloud版本

<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.2.3.RELEASE</version>

skywalking 版本

<artifactId>java-agent-sniffer</artifactId>
<groupId>org.apache.skywalking</groupId>
<version>8.9.0</version>

复现

说实话,当时我本地起了,压测了一天,并没有出现OOM的情况,事实证明,还是量不够大。

后来同事找到了病根(下面两种情况原因是一样的)

  1. TracingContext中的activeSpanStack在某些场景下没有清空,就是下面这个。

在这里插入图片描述

或者是

  1. IgnoredTracerContext的 stackDepth 不等于0

在这里插入图片描述

最终的结果都是 ContextManager.Context/RuntimeContext 未清空,导致内存泄露
在这里插入图片描述

调试

可以参考这篇文章 https://www.jianshu.com/p/ba9254f38fa5

因为我只想调试网关相关包,把下载失败的包和编译失败的包都注释掉了,再在网关项目的导入module。

导入完了,结构如下图,该注释的注释,能编译打包就行。

在这里插入图片描述

断点打在 gateway-2.1.x-plugin的几个拦截器,可以看到debug成功

在这里插入图片描述

源码解析

剩下就跟着代码一步一步走了。

几个拦截器的顺序是 NettyRoutingFilterInterceptor -> HttpClientFinalizerSendInterceptor -> HttpClientFinalizerResponseConnectionInterceptor

可以看到 NettyRoutingFilterInterceptor 初次进入会执行 ContextManager.createLocalSpan

创建span对象(全链路用到的流转对象,感兴趣的可以看看谷歌的dapper https://blog.csdn.net/ruizhikai_ztq/article/details/123663633)

@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,MethodInterceptResult result) throws Throwable {ServerWebExchange exchange = (ServerWebExchange) allArguments[0];EnhancedInstance enhancedInstance = getInstance(exchange);AbstractSpan span = ContextManager.createLocalSpan("SpringCloudGateway/RoutingFilter");if (enhancedInstance != null && enhancedInstance.getSkyWalkingDynamicField() != null) {ContextManager.continued((ContextSnapshot) enhancedInstance.getSkyWalkingDynamicField());}span.setComponent(SPRING_CLOUD_GATEWAY);
}

createLocalSpan,这里的两个实现,是否忽略trace,由于我引入了

apm-trace-ignore-plugin-8.9.0.jar 这个包,实现会走 ignore的,也就是复现里的第二种情况

在这里插入图片描述

这个方法里有一个栈深度 stackDepth 字段,只要创建span就会自增,删除span就会自减。

@Override
public AbstractSpan createLocalSpan(String operationName) {stackDepth++;return NOOP_SPAN;
}@Override
public boolean stopSpan(AbstractSpan span) {stackDepth--;if (stackDepth == 0) {ListenerManager.notifyFinish(this);}return stackDepth == 0;
}

一般来说的话,方法的Interceptor的 beforeMethod 会执行

ContextManager.createLocalSpan();

afterMethod 会执行

AbstractSpan span = ContextManager.activeSpan();
ContextManager.stopSpan(span);

但是很多中间件的某些场景都是异步的,尤其网关是响应式的,所以入口和出口也可能在不同的类里。

比如网关的 createLocalSpan 是在 NettyRoutingFilterInterceptor

而 stopSpan 是在 HttpClientFinalizerSendInterceptor

再看下上面的 stopSpan 方法的调用的地方

stopSpan 方法返回值是根据 stackDepth 是否为0来的,如果 stackDepth != 0,返回false

在这里插入图片描述

那这种就有点危险了,如果有方法触发了 createLocalSpan 而后续没有执行 stopSpan 就会出现内存无法回收。

比如只执行了 NettyRoutingFilterInterceptor 而没有执行 HttpClientFinalizerSendInterceptor

网关异常代码

这种问题很长时间都没有人反馈,说明还是小众的。主要是我们写的不规范也有一定的原因。(不要问,问就是开源全锅)

public class CorsResponseHeaderFilter implements GlobalFilter{@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {return chain.filter(exchange).then(Mono.defer(() -> {exchange.getResponse().getHeaders().entrySet().stream().filter(kv -> !CollectionUtils.isEmpty(kv.getValue())).filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_MAX_AGE))).forEach(kv -> {kv.setValue(new ArrayList<String>() {{add(kv.getValue().get(0));}});});return chain.filter(exchange);}));}}

这段代码主要是解决网关跨域的问题,记得有一些后台页面对返回的头有限制,所以做了这个逻辑处理,过滤一些响应头和指定格式。

乍一看没啥问题,问题就出现在 Mono.defer,一般我们使用的多的是Mono.just。

看一下官方文章这俩有啥区别 https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html#create-java.util.function.Consumer-

简单介绍一下常用的api:

Mono.just 饿汉式:立即执行

在这里插入图片描述

Mono.defer 懒汉式:发布之后,等待订阅者来执行(有延迟)

在这里插入图片描述

Mono.create 完全自主控制:发布之后,自己添加/移除监听器,并且手动写回调

在这里插入图片描述

问题解决

饶了一大圈,在本应该skywalking 的gateway interceptor 走完了之后,stackDepth 为0。

而 Mono.defer 操作,又进入了 NettyRoutingFilterInterceptor,执行了 createLocalSpan,stackDepth ++,再之后的CONTEXT就无法remove了,造成内存泄漏。

访问两次之后就会出现这种情况了。
在这里插入图片描述

同事已经给修了。

https://github.com/apache/skywalking-java/pull/133

同一个链路上 ServerWebExchange.getAttributes() 是一直有的,进入的时候放一次,再次进入判断一下如果是同一个链路的话,就不再执行后面的代码了(避免重复创建span)

总结

开源项目就是有这样的魅力,发现其中问题,再fix提交。

不过线上运行也确实是坑啊,之前有别的网关已经接过了,没问题,就直接上了。

但是每个网关项目本身也不一样,一个小小的过滤器有这么大的能量。

额外说一句,一定要灰度!!!

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

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

相关文章

长连接检测 监控的一点思考 java实现

大家好&#xff0c;我是烤鸭&#xff1a; 怎么监控长链接服务器的稳定&#xff0c;除了探活服务之外&#xff0c;怎么保证长链接的收发正常&#xff0c;这篇文章考虑下这个。 问题来源 运营反馈部分直播间无法收到弹幕、点赞消息&#xff0c;第一时间进行复现&#xff0c;发现…

rabbitmq 启动失败 dump日志分析

大家好&#xff0c;我是烤鸭&#xff1a; rabiitmq 突然宕机&#xff0c;并且无法启动。同事反馈测试环境 rabbitmq 有一个节点突然掉了&#xff0c;并且无法启动。 现象 集群有一个节点宕机。 去对应的机器上执行启动命令 ./rabbitmq-server -detached发现进程不在&#x…

项目实战-药品采购系统-day01

目标&#xff1a;项目背景&#xff0c;需求&#xff0c;环境的搭建 难点&#xff1a;环境的搭建 1.学习方法&#xff1a; 一个项目&#xff1a;架构师、高级程序员、一般程序员 难度很大但是普通的程序员做很简单 所以自己对自己的定位要准确&#xff08;一般程序员&#xff09…

《深入理解Java虚拟机》-读书笔记(第一、第二部分)

大家好&#xff0c;我是烤鸭&#xff1a; 《深入理解Java虚拟机》-读书笔记&#xff08;第一、第二部分&#xff09;。 第一部分&#xff1a;走进Java 第1章 走进Java 1.1 概述 摆脱了硬件平台的束缚&#xff0c;实现了“一次编写&#xff0c;到处运行”的理想&#xff1b;…

BZOJ1189: [HNOI2007]紧急疏散evacuate(二分答案,最大流)

Description 发生了火警&#xff0c;所有人员需要紧急疏散&#xff01;假设每个房间是一个N M的矩形区域。每个格子如果是.&#xff0c;那么表示这是一块空地&#xff1b;如果是X&#xff0c;那么表示这是一面墙&#xff0c;如果是D&#xff0c;那么表示这是一扇门&#xff0c;…

[vue]vue渲染模板时怎么保留模板中的HTML注释呢?

[vue]vue渲染模板时怎么保留模板中的HTML注释呢&#xff1f; <template comments>... </template>个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

redis设置为null问题

查看源码后发现&#xff0c;redis没有删除方法&#xff0c;本想给他设置为null,但是redis报错&#xff0c;所有仔细想了一下&#xff0c;发现redis提供了一个时间限制方法&#xff0c;所有可以让redis的时间限制为1s&#xff0c;就想当于删除redis中的这个K。 转载于:https://w…

nacos 返回 403 unknown user 太他么坑了 源码解析

大家好&#xff0c;我是烤鸭&#xff1a; nacos 真的是有点意思&#xff0c;有时候哪怕某个jar包版本冲突了都可能导致莫名其妙的错误&#xff0c;源码走一波吧。 当前版本 <dependencies><dependency><groupId>org.springframework.boot</groupId>&…

[vue] Vue.observable你有了解过吗?说说看

[vue] Vue.observable你有了解过吗&#xff1f;说说看 让一个对象可响应。Vue 内部会用它来处理 data 函数返回的对象。返回的对象可以直接用于渲染函数和计算属性内&#xff0c;并且会在发生改变时触发相应的更新&#xff1b;也可以作为最小化的跨组件状态存储器。个人简介 …

[vue] 你知道style加scoped属性的用途和原理吗?

[vue] 你知道style加scoped属性的用途和原理吗&#xff1f; 在标签上绑定了自定义属性&#xff0c;防止css全局污染 但是很多时候使用ui框架如果加scope就不能覆盖&#xff0c;这个时候一般写sass 会在最外层包裹该组件名的id 就可以不使用scoped 了个人简介 我是歌谣&#x…

研发效能提升 maven依赖扫描 版本统一 漏洞版本提醒

大家好&#xff0c;我是烤鸭&#xff1a; 整点干货&#xff0c;代码级别的版本统一&#xff0c;以及漏洞版本的扫描。 背景 两个方面吧。 项目整体的架构不统一&#xff0c;springboot/cloud 配置/注册中心也用的不统一&#xff0c;版本更是五花八门&#xff0c;怎么快速的找…

[vue] 你期待vue3.0有什么功能或者改进的地方?

[vue] 你期待vue3.0有什么功能或者改进的地方&#xff1f; 用尤大的话说就是各种速度提升n倍。我希望在依赖node_modules能够做出调整&#xff0c;文件数目太多&#xff0c;开启项目每次都要下载个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&…

Spring boot actuator端点启用和暴露

1.启用端点 默认情况下&#xff0c;除了shutdown端点是关闭的&#xff0c;其它的都是启用的。配置一个端点的启用&#xff0c;使用management.endpoint..enabled属性&#xff0c;下面的例子是启用shutdown端点&#xff1a; management.endpoint.shutdown.enabledtrue如果你个人…

java执行sql慢 navicat不慢 见鬼了

大家好&#xff0c;我是烤鸭&#xff1a; 有点意思的问题&#xff0c;代码提示接口超时(10s)&#xff0c;接口逻辑很简单&#xff0c;就一个sql查询。本来也想是sql慢&#xff0c;可是拿sql去Navicat执行下&#xff0c;一点不慢(50ms)。 环境 DB&#xff1a;SqlServer 连接池…

[vue] vue边界情况有哪些?

[vue] vue边界情况有哪些&#xff1f; 访问根实例、访问父组件、子组件个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

函数的基础

函数的初识&#xff1a; 封装一个功能。 def 函数名(): 函数体 函数的返回值&#xff1a;return 1,结束函数。 2&#xff0c;返回给执行者&#xff08;函数名()&#xff09;值。 return ----> None return 单个值----> 单个值 return 多个值----> &#xff08;多…

windows docker mongodb

大家好&#xff0c;我是烤鸭&#xff1a; 今天翻博客&#xff0c;发现4年前的一篇草稿&#xff0c;抽空给完善下。原本草稿写的是linux下mongo使用&#xff0c;还有java的一些api&#xff0c;现在就用容器实现下。 容器部署 官方网站&#xff1a; https://www.mongodb.com/ w…