Sentinel icon indicating copy to clipboard operation
Sentinel copied to clipboard

memory leak caused by many origin

Open Adol1111 opened this issue 3 years ago • 10 comments

Issue Description

我的用法是这样的,服务A调用服务B(服务C、D、E、F....都可能会调用B),在服务B中用了 SphU.entry(resourceName,ResourceTypeConstants.COMMON_RPC, EntryType.IN),这本身没有问题。但近期给B服务的其他组件也加上了Sentinel,比如调用DB、redis、甚至是调用其他服务的时候(都在B里面)。上线之后内存增长非常快,基本上高峰期10分钟就会一次full gc,young gc的频次也变得频繁了。 image

Type: bug report or feature request

Describe what happened (or what feature you want)

我看了一下,具体的原因,应该是有多个上游的时候,origin数量过多,导致MetricNode数量过多。但感觉里面有一些设计不太合理的地方,比如以下链路,A->B->redis、DB、....等其他资源。按照我的理解,B有多个上游,通过设置origin进行区分,问题不大。但是B去调用其他资源的时候,由于属于A->B的子链路,会导致B->redis、B->DB也会带上origin,但事实上,这些子链路(EntryType.OUT的时候)的origin应该是自己,这个时候去区分origin没有任何意义,反而会增加统计的成本。

Describe what you expected to happen

  1. 是否可以调整为,EntryType为IN的时候才统计origin,避免origin的上下文信息传递到OUT的调用模块。比如以下的代码(不完全是这里,只是粗略的看了 一下,应该还有其他地方要调整)

image

改为

image

  1. 当resource规则没有配置时,是否可以不记录MetricNode

Tell us your environment

1.8.0

Adol1111 avatar May 07 '21 02:05 Adol1111

服务A调用服务B(服务C、D、E、F....都可能会调用B

是Dubbo服务用的sentinel-dubbo-adapter吗

2.当resource规则没有配置时,是否可以不记录MetricNode

这个社区有同学提到过,可参考 #2169

cdfive avatar May 07 '21 05:05 cdfive

@cdfive

是Dubbo服务用的sentinel-dubbo-adapter吗

是公司内部的RPC框架,不过是参考sentinel-dubbo-adapter写的,resourcename和dubbo差不多,也是分了 interfaceResourceName 和 methodResourceName

这个社区有同学提到过,可参考 #2169

  • 这个应该还没有实现吧。如果过滤resourceName,确实会看不到资源的调用情况。
  • 但按照我描述的方案,如果OUT类型不创建origin统计,应该也可以有效降低内存。目前看ContextUtil.enter设置的origin是一个全局变量,会影响所有调用ContextUtil.enter(resourceName, origin) 以后的entry,直到ContextUtil.exit()。如果只是区分来源的话,对于OUT的场景,是否真的有必要也读取这个origin?

Adol1111 avatar May 07 '21 11:05 Adol1111

具体的原因,应该是有多个上游的时候,origin数量过多,导致MetricNode数量过多。但感觉里面有一些设计不太合理的地方,比如以下链路,A->B->redis、DB、....等其他资源。按照我的理解,B有多个上游,通过设置origin进行区分,问题不大。但是B去调用其他资源的时候,由于属于A->B的子链路,会导致B->redis、B->DB也会带上origin,但事实上,这些子链路(EntryType.OUT的时候)的origin应该是自己,这个时候去区分origin没有任何意义,反而会增加统计的成本。

origin是通过ContextUtil.enter(resourceName, origin);设置的,在ClusterBuilderSlot里创建了新的origin的Node。

可参考sentinel-dubbo-adapter中的SentinelDubboProviderFilterSentinelDubboConsumerFilter, 注意只在Provider端调用了ContextUtil.enter,Consumer端没有; 如:A->B->redis,这个链路,B作为provider端,调用了ContextUtil.enter(resourceName, origin);,其中origin是通过A传过来的,即origin是A;接着B作为consumer端调redis的时候,也就是OUT的场景,在SentinelDubboConsumerFilter里并没有调ContextUtil.enter产生新的origin,即复用当前的context,origin还是A。

你是不是在B->redis的时候也调用了ContextUtil.enter(resourceName, origin);?所以产生了新的origin。

目前公司项目我们部门所有微服务都接入了sentinel,也存在一个服务被多个服务调用的情况,没有出现内存增长特别快或者gc频繁的情况。

cdfive avatar May 08 '21 08:05 cdfive

你是不是在B->redis的时候也调用了ContextUtil.enter(resourceName, origin);?所以产生了新的origin。

这个目前没有,只有在provider的时候设置了enter。不过我查了一下,唯一的不同是,设置 ContextUtil.enter(resourceName, origin); 的resourceName,是一个固定值,而dubbo是根据methodResourceName变化的,不知道是否和这个有关系。

接着B作为consumer端调redis的时候,也就是OUT的场景,在SentinelDubboConsumerFilter里并没有调ContextUtil.enter产生新的origin,即复用当前的context,origin还是A

origin确实是A,但是B作为consumer调用其他资源,应该不会经过SentinelDubboConsumerFilter吧?我是这样做的,这里没有额外设置ContextUtil.enter

// 这里通过rpc调用bizMethod,rpc的provider的filter中加入了 ContextUtil.enter
void bizMethod() {
    redisClient.get("test:123", value);
}

redisClient中

Entry entry1 = null;
Entry entry2 = null;
try {
    entry1 = HahasSphU.entry(String.format("redis:%s",redisName), ResourceTypeConstants.COMMON, EntryType.OUT);
    entry2 = HahasSphU.entry(String.format("redis:%s:%s", redisName, "test:%s"), // 刚刚的key,提取的规则
            ResourceTypeConstants.COMMON, EntryType.OUT,
           "test:123"
    );
    return callRedis();
} catch (BlockException e) {
    throw e;
} catch (Throwable e) {
    Tracer.traceEntry(e, entry1);
    Tracer.traceEntry(e, entry2);
    throw e;
} finally {
    if (entry2!=null){
         entry2.exit(1, args)
    } 
    if (entry1!=null){
         entry1.exit()
    }
}

如果我一个服务的redis规则有10种,就会产生20个redis的entry统计,如果还配合origin,比如有10个origin,按照我的理解就会有20*(10+1) = 220种entry统计(看源码好像还包含一个不区分origin的统计),如果用了热点参数限流是不是会更多?

Adol1111 avatar May 08 '21 08:05 Adol1111

比如有10个origin,按照我的理解就会有20*(10+1) = 220

我理解是1个origin仅会创建1个StatisticNode,entry个数只跟资源名有关,这个理解对吗?请@sczyh30 帮忙看看

cdfive avatar May 13 '21 06:05 cdfive

比如有10个origin,按照我的理解就会有20*(10+1) = 220

我理解是1个origin仅会创建1个StatisticNode,entry个数只跟资源名有关,这个理解对吗?请@sczyh30 帮忙看看

可以参考 https://github.com/alibaba/Sentinel/wiki/Sentinel-核心类解析#node

sczyh30 avatar May 13 '21 08:05 sczyh30

  • ClusterBuilderSlot:首先根据 resourceName 创建 ClusterNode,并且 set clusterNode to defaultNode;然后再根据 origin 创建来源节点(类型为 StatisticNode),并且 set originNode to curEntry。
  • 来源节点(类型为 StatisticNode)的维度是 resource * origin,存在每个 ClusterNode 的 originCountMap 里面

所以StatisticNode数量确实会随着origin增加而增加,而且没有区分resource类型IN和OUT,如果是OUT类型,也会区分origin。导致统计的指标过多

@cdfive 这个是否可以考虑优化一下,resource类型为OUT的就不创建来源节点了

Adol1111 avatar May 14 '21 01:05 Adol1111

感觉是String.format("redis:%s",redisName), 这个代码 , 不同的redis命令都会生成一个统计节点, 导致resouce过多了

lazyfighter avatar May 19 '21 09:05 lazyfighter

@lazyfighter redisName 和 key的数量排查过,所有resource合计没有超过100种。但是dump的数据里,metricNode的数量比其他数据高出好几十倍。唯一的变化就是redis接入了Sentinel,接入前的gc频率明显低于接入后的情况。

image

Adol1111 avatar May 24 '21 02:05 Adol1111

如果EntryType.OUT对外调用很多的话,确实会产生很多StatisticNode的origin统计,这个主要是看你需要每个Node的out的orign的统计嘛?毕竟FetchOriginCommandHandler拉取的是每个节点下的origin的统计数据。

liaomengge avatar Jul 07 '22 10:07 liaomengge

给一个小小的建议:在 ContextUtil.enter(resourceName, origin) 之前检查一下origin是否在规则中,如果在规则中则 使 origin=""

305859823 avatar Mar 13 '23 10:03 305859823