JVM内存泄漏调查
  Vk9JCTwxP25o 2023年12月19日 38 0

1, 背景

mik-api上线持续运行一段时间后, 在Grafana看板上发现JVM old Gen能回收的内存越来越少, 有内存泄漏的风险

在持续运行10天后, pod重启

JVM内存泄漏调查_拦截器

Heap占比持续增高

JVM内存泄漏调查_拦截器_02

JVM GC能回收的内存持续减少

2, 原因调查

2.1, 对比分析

pod重启后, 每天在同一时间进行内存dump, 使用MAT进行分析, 对比找出异常的对象

dump 命令

jmap -dump:format=b,file=HeapDump <pid>

对比从20230322进行到20230327, 发现ThreadLocal内存异常增长

JVM内存泄漏调查_拦截器_03

2.2, 异常对象分析

使用MAT进行分析;

使用Leak Suspects, 线程对象存在泄漏的分享

JVM内存泄漏调查_拦截器_04

查看Top Consumer, 发现大对象都是线程, 怀疑是线程创建ThreadLocal

JVM内存泄漏调查_JVM_05

查看dominator_tree, 根据大小排序, 发现ThreadLocal对象占据了线程大部分空间, 而在ThreadLocal对象中, LinkedList占据了ThreadLocal大部分空间, LinkedList存的是mik-api访问其他服务的uri

JVM内存泄漏调查_JVM_06

对这个类的调用链进行分析, 发现ThreadLocal的LinkedList Entry主要由MetricsClientHttpRequestInterceptor.UrlTemplateThreadLocal 调用

JVM内存泄漏调查_拦截器_07

JVM内存泄漏调查_ide_08

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, 与线上现象基本一致.

JVM内存泄漏调查_ide_09

持续进行debug, 确认了不同的使用restTemplate访问其他服务的线程的ThreadLocal都存有大量uri, 与与完全现象基本一致.

JVM内存泄漏调查_JVM_10

JVM内存泄漏调查_JVM_11

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;
    }

JVM内存泄漏调查_JVM_12

MetricsClientHttpRequestInterceptor执行后, UrlTemplateThreadLocal中的uri被poll出来生成metrics标签, UrlTemplateThreadLocal 不会存储大量uri.

2.3.4, 代码分析结论

JVM中线程ThreadLocal存储了大量uri是由于拦截器MetricsClientHttpRequestInterceptor没有执行, UrlTemplateThreadLocal 只有push的操作, 没有poll的操作;

修改代码之后确认MetricsClientHttpRequestInterceptor执行后, UrlTemplateThreadLocal 执行了poll, ThreadLocal储存的uri被清除.


Reference:

https://www.jb51.net/article/230735.htm

Spring RestTemplate Interceptor 配置-CSDN博客

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年12月19日 0

暂无评论

推荐阅读
  anLrwkgbyYZS   2023年12月30日   34   0   0 ideciciMaxideMax
Vk9JCTwxP25o
作者其他文章 更多