1, 背景
mik-api上线持续运行一段时间后, 在Grafana看板上发现JVM old Gen能回收的内存越来越少, 有内存泄漏的风险
在持续运行10天后, pod重启
Heap占比持续增高
JVM GC能回收的内存持续减少
2, 原因调查
2.1, 对比分析
pod重启后, 每天在同一时间进行内存dump, 使用MAT进行分析, 对比找出异常的对象
dump 命令
jmap -dump:format=b,file=HeapDump <pid>
对比从20230322进行到20230327, 发现ThreadLocal内存异常增长
2.2, 异常对象分析
使用MAT进行分析;
使用Leak Suspects, 线程对象存在泄漏的分享
查看Top Consumer, 发现大对象都是线程, 怀疑是线程创建ThreadLocal
查看dominator_tree, 根据大小排序, 发现ThreadLocal对象占据了线程大部分空间, 而在ThreadLocal对象中, LinkedList占据了ThreadLocal大部分空间, LinkedList存的是mik-api访问其他服务的uri
对这个类的调用链进行分析, 发现ThreadLocal的LinkedList Entry主要由MetricsClientHttpRequestInterceptor.UrlTemplateThreadLocal
调用
2.3, 代码分析
2.3.1, MetricsClientHttpRequestInterceptor作用分析
应用 a 使用 rest template 通过 http 方式调用 应用 b,应用项目中开启了 actuator,api 使用的是 micrometer;
在 client 调用时,actuator 会产生一个 name 为 http.client.requests 的 metrics,此 metric 的 tag 中包含点目标的 uri。
UrlTemplateThreadLocal
会被push调用应用b时的 uri, 在记录metrics的时候会把UrlTemplateThreadLocal
中的uri poll出来, 生metrics标签.
由于内存每个线程的UrlTemplateThreadLocal
都存在大量uri, 怀疑UrlTemplateThreadLocal
没有进行poll操作或者remove操作.
class MetricsClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private static final ThreadLocal<Deque<String>> urlTemplate = new UrlTemplateThreadLocal();
private final MeterRegistry meterRegistry;
private final RestTemplateExchangeTagsProvider tagProvider;
private final String metricName;
private final AutoTimer autoTimer;
MetricsClientHttpRequestInterceptor(MeterRegistry meterRegistry, RestTemplateExchangeTagsProvider tagProvider,String metricName, AutoTimer autoTimer) {
this.tagProvider = tagProvider;
this.meterRegistry = meterRegistry;
this.metricName = metricName;
this.autoTimer = (autoTimer != null) ? autoTimer : AutoTimer.DISABLED;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)throws IOException {
if (!this.autoTimer.isEnabled()) {
return execution.execute(request, body);
}
long startTime = System.nanoTime();
ClientHttpResponse response = null;
try {
response = execution.execute(request, body);
return response;
}
finally {
getTimeBuilder(request, response).register(this.meterRegistry).record(System.nanoTime() - startTime,TimeUnit.NANOSECONDS);
if (urlTemplate.get().isEmpty()) {
urlTemplate.remove();
}
}
}
//把url push到urlTemplateThreadLocal之中
UriTemplateHandler createUriTemplateHandler(UriTemplateHandler delegate) {
return new UriTemplateHandler() {
@Override
public URI expand(String url, Map<String, ?> arguments) {
urlTemplate.get().push(url);
return delegate.expand(url, arguments);
}
@Override
public URI expand(String url, Object... arguments) {
urlTemplate.get().push(url);
return delegate.expand(url, arguments);
}
};
}
private Timer.Builder getTimeBuilder(HttpRequest request, ClientHttpResponse response) {
return this.autoTimer.builder(this.metricName)
// 把url push从urlTemplateThreadLocal之中 poll出来
.tags(this.tagProvider.getTags(urlTemplate.get().poll(), request, response))
.description("Timer of RestTemplate operation");
}
private static final class UrlTemplateThreadLocal extends NamedThreadLocal<Deque<String>> {
private UrlTemplateThreadLocal() {
super("Rest Template URL Template");
}
@Override
protected Deque<String> initialValue() {
return new LinkedList<>();
}
}
}
2.3.2, 代码Debug跟踪
本地进行代码debug, 根据断点分析, 代码有进入UrlTemplateThreadLocal
push, 而没有进行UrlTemplateThreadLocal
poll和remove.
线程的ThreadLocal也存有大量uri, 与线上现象基本一致.
持续进行debug, 确认了不同的使用restTemplate访问其他服务的线程的ThreadLocal都存有大量uri, 与与完全现象基本一致.
2.3.3, 错误代码分析
MetricsClientHttpRequestInterceptor是用来拦截restTemplate请求, 在请求中增加metrics埋点, 现在拦截器没有生效;
查看restTemplateConfig配置, RestTemplate自身的拦截器被代码自定义拦截器覆盖, 导致MetricsClientHttpRequestInterceptor没有生效, 没有执行poll操作的代码.
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = builder.requestFactory(this::httpComponentsClientHttpRequestFactory).build();
//直接覆盖了RestTemplate自身的拦截器
restTemplate.setInterceptors(Collections.singletonList(tokenInterceptor));
return restTemplate;
}
2.3.3, 代码修复及验证
代码做如下修改之后, debug验证
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = builder.requestFactory(this::httpComponentsClientHttpRequestFactory).build();
if(CollectionUtils.isEmpty(restTemplate.getInterceptors())){
restTemplate.setInterceptors(Collections.singletonList(tokenInterceptor));
}
//仅仅增加代码自定义拦截器
restTemplate.getInterceptors().add(tokenInterceptor);
return restTemplate;
}
MetricsClientHttpRequestInterceptor执行后, UrlTemplateThreadLocal
中的uri被poll出来生成metrics标签, UrlTemplateThreadLocal
不会存储大量uri.
2.3.4, 代码分析结论
JVM中线程ThreadLocal存储了大量uri是由于拦截器MetricsClientHttpRequestInterceptor没有执行, UrlTemplateThreadLocal
只有push的操作, 没有poll的操作;
修改代码之后确认MetricsClientHttpRequestInterceptor执行后, UrlTemplateThreadLocal
执行了poll, ThreadLocal储存的uri被清除.
Reference: