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{
    @Override
    public 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提交。

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

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

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

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
你可以通过以下步骤来使用 SkyWalking 客户端 JavaScript(skywalking-client-js): 1. 首先,确保你的项目中已经安装了 Node.js 环境。 2. 在你的项目目录中,使用以下命令安装 skywalking-client-js: ``` npm install skywalking-client-js ``` 3. 在你的 JavaScript 代码中,导入并初始化 SkyWalking 客户端: ```javascript const { Tracer, Segment, Span } = require('skywalking-client-js'); // 初始化 Tracer Tracer.initialize({ serviceName: 'your-service-name', // 设置你的服务名称 directServers: 'your-skywalking-collector-url:your-collector-port', // 设置 SkyWalking Collector 的地址和端口 }); // 创建 Segment,并开始记录 const segment = new Segment('your-segment-name'); segment.start(); // 创建 Span,并开始记录 const span = new Span('your-span-name'); span.start(); // 执行你的业务逻辑 // ... // 结束 Span 和 Segment 的记录 span.end(); segment.end(); // 发送数据到 SkyWalking Collector Tracer.stop(); ``` 在上述代码中,你需要替换以下内容: - `your-service-name`:你的服务名称,可以自定义。 - `your-skywalking-collector-url:your-collector-port`:SkyWalking Collector 的地址和端口,通常是 `localhost:11800`。 - `your-segment-name` 和 `your-span-name`:你的 Segment 和 Span 的名称,可以自定义。 4. 运行你的 JavaScript 代码,确保 SkyWalking Collector 能够接收到数据并进行监控。 这样,你就可以使用 SkyWalking 客户端 JavaScript 进行性能监控和分析了。更多详细的用法和配置可以参考 skywalking-client-js 的官方文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烤鸭的世界我们不懂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值