李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 每日心情
  • Now
  • 系列文章
  • 论坛
  • 左邻右舍
    • 左邻右舍
    • 博友圈
  • 留言
    • 留言
    • 走心评论
  • 关于
    • 关于本站
    • 网站地图
    • 网站统计
    • 另一个网站
    • 我的导航站
    • 赞助
  • 🚇开往
!Destiny
惟坚韧者始能遂其志
  1. 首页
  2. 代码人生
  3. 正文

记一次spring-cloud-netflix-core引发的内存溢出分析

2021年12月7日 约 1,241 字5 分钟 161 1 0
本文最后更新于 2025年11月20日,距今已 307 天,其中的信息可能已经发生变化,请注意甄别。

发现问题

线上服务重启,好在抓到了线上服务的dump文件,下载到本地进行分析。
使用MAT打开快照文件,此处省略掉使用MAT的过程,分析发现有大量的com.netflix.servo.monitor.BasicTimer未释放,且被org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache占用。

file

file

file

分析

在工程中查找到ServoMonitorCache类,发现在spring-cloud-netflix-core包下,然后打开该jar包,查看其spring.factories去查看是那里自动配置生成了该类,找到org.springframework.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration中自动配置,然后再搜索那里使用了该类,在org.springframework.cloud.netflix.metrics.MetricsInterceptorConfiguration中发现了ServoMonitorCache对象的使用。看到metrics就明白,是对服务的监控对象。代码如下:

@Configuration
@ConditionalOnProperty(value = "spring.cloud.netflix.metrics.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnClass({ Monitors.class, MetricReader.class })
public class MetricsInterceptorConfiguration {

    @Configuration
    @ConditionalOnWebApplication
    @ConditionalOnClass(WebMvcConfigurerAdapter.class)
    static class MetricsWebResourceConfiguration extends WebMvcConfigurerAdapter {
        @Bean
        MetricsHandlerInterceptor servoMonitoringWebResourceInterceptor() {
            return new MetricsHandlerInterceptor();
        }

        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(servoMonitoringWebResourceInterceptor());
        }
    }

    @Configuration
    @ConditionalOnClass({ RestTemplate.class, JoinPoint.class })
    @ConditionalOnProperty(value = "spring.aop.enabled", havingValue = "true", matchIfMissing = true)
    static class MetricsRestTemplateAspectConfiguration {

        @Bean
        RestTemplateUrlTemplateCapturingAspect restTemplateUrlTemplateCapturingAspect() {
            return new RestTemplateUrlTemplateCapturingAspect();
        }

    }

    @Configuration
    @ConditionalOnClass({ RestTemplate.class, HttpServletRequest.class })   // HttpServletRequest implicitly required by MetricsTagProvider
    static class MetricsRestTemplateConfiguration {

        @Value("${netflix.metrics.restClient.metricName:restclient}")
        String metricName;
                /*
                  *此处为关键代码
                  *编号1
                  */
        @Bean
        MetricsClientHttpRequestInterceptor spectatorLoggingClientHttpRequestInterceptor(
                Collection<MetricsTagProvider> tagProviders,
                ServoMonitorCache servoMonitorCache) {
            return new MetricsClientHttpRequestInterceptor(tagProviders,
                    servoMonitorCache, this.metricName);
        }

        @Bean
        BeanPostProcessor spectatorRestTemplateInterceptorPostProcessor() {
            return new MetricsInterceptorPostProcessor();
        }
                //编号2
        private static class MetricsInterceptorPostProcessor
                implements BeanPostProcessor, ApplicationContextAware {
            private ApplicationContext context;
            private MetricsClientHttpRequestInterceptor interceptor;

            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) {
                return bean;
            }

            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) {
                if (bean instanceof RestTemplate) {
                    if (this.interceptor == null) {
                        this.interceptor = this.context
                                .getBean(MetricsClientHttpRequestInterceptor.class);
                    }
                    RestTemplate restTemplate = (RestTemplate) bean;
                    // create a new list as the old one may be unmodifiable (ie Arrays.asList())
                    ArrayList<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
                    interceptors.add(interceptor);
                    interceptors.addAll(restTemplate.getInterceptors());
                    restTemplate.setInterceptors(interceptors);
                }
                return bean;
            }

            @Override
            public void setApplicationContext(ApplicationContext context)
                    throws BeansException {
                this.context = context;
            }
        }
    }
}

在上面代码中编号1处,自动配置生成了MetricsClientHttpRequestInterceptor拦截器,然后把ServoMonitorCache采用构造器注入传入了拦截器;然后代码编号2处的postProcessAfterInitialization函数中,把该拦截器赋值给了RestTemplate,这是一个大家都很熟悉的对象。

然后进入MetricsClientHttpRequestInterceptor,核心代码如下:

@Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
            ClientHttpRequestExecution execution) throws IOException {
        long startTime = System.nanoTime();

        ClientHttpResponse response = null;
        try {
            response = execution.execute(request, body);
            return response;
        }
        finally {
            SmallTagMap.Builder builder = SmallTagMap.builder();
                        //编号3
            for (MetricsTagProvider tagProvider : tagProviders) {
                for (Map.Entry<String, String> tag : tagProvider
                        .clientHttpRequestTags(request, response).entrySet()) {
                    builder.add(Tags.newTag(tag.getKey(), tag.getValue()));
                }
            }
                        //编号4
            MonitorConfig.Builder monitorConfigBuilder = MonitorConfig
                    .builder(metricName);
            monitorConfigBuilder.withTags(builder);

            servoMonitorCache.getTimer(monitorConfigBuilder.build())
                    .record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
        }
    }

编号3处代码,发现对象tagProviders,回过去看代码也是该拦截器构造时传入的参数;现在去看一下这个对象是什么,因为该对象是构造器注入的,说明也是由spring容器配置生成的,所以继续在autoconfig文件中查找,发现在org.springframework.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration中自动配置生成:

@Configuration
    @ConditionalOnClass(name = "javax.servlet.http.HttpServletRequest")
    protected static class MetricsTagConfiguration {
        @Bean
        public MetricsTagProvider defaultMetricsTagProvider() {
            return new DefaultMetricsTagProvider();
        }
    }

进入DefaultMetricsTagProvider,核心代码如下:

public Map<String, String> clientHttpRequestTags(HttpRequest request,
           ClientHttpResponse response) {
       String urlTemplate = RestTemplateUrlTemplateHolder.getRestTemplateUrlTemplate();
       if (urlTemplate == null) {
           urlTemplate = "none";
       }

       String status;
       try {
           status = (response == null) ? "CLIENT_ERROR" : ((Integer) response
                   .getRawStatusCode()).toString();
       }
       catch (IOException e) {
           status = "IO_ERROR";
       }

       String host = request.getURI().getHost();
       if( host == null ) {
           host = "none";
       }

       String strippedUrlTemplate = urlTemplate.replaceAll("^https?://[^/]+/", "");

       Map<String, String> tags = new HashMap<>();
       tags.put("method",   request.getMethod().name());
       tags.put("uri",     sanitizeUrlTemplate(strippedUrlTemplate));
       tags.put("status",   status);
       tags.put("clientName", host);

       return Collections.unmodifiableMap(tags);
   }

发现其就是分解了Http的客户端请求,其中关键就是method(get、post、delete等http方法)、status状态、clientName访问的服务域名、uri访问路径(包含参数)。

然后,返回去看代码编号4处,生成了一个对象com.netflix.servo.monitor.MonitorConfig,主要就是name和tags,name默认的就是restclient(可以在属性文件中修改);tags就是DefaultMetricsTagProvider中那些tag标签。

然后进入ServoMonitorCache.getTimer函数:

public synchronized BasicTimer getTimer(MonitorConfig config) {
        BasicTimer t = this.timerCache.get(config);
        if (t != null)
            return t;

        t = new BasicTimer(config);
        this.timerCache.put(config, t);

        if (this.timerCache.size() > this.config.getCacheWarningThreshold()) {
            log.warn("timerCache is above the warning threshold of " + this.config.getCacheWarningThreshold() + " with size " + this.timerCache.size() + ".");
        }

        this.monitorRegistry.register(t);
        return t;
    }

此处就很简单了,先在缓存中查找该MonitorConfig对象有没有,没有则新增一个BasicTimer,若有就更新该BasicTimer的参数,BasicTimer存储了各个接口的访问最大时间、最小时间、平均时间等。

分析到这里就明白了,如果每次的接口访问url都不一样,那么在DefaultMetricsTagProvider中解析的uri也就都不一样,最终导致了MonitorConfig对象不一样,所以接口调用一次,生成一个BasicTimer对象,久而久之也就打爆Jvm堆内存。

而我们线上的服务,由于很多都是通过参数来拼接url来调用内部或者外部的接口。

解决方案

  • 修改调用方式,采用POST方式传参(针对我们的服务,尤其是三方的服务,这种方式明显不适合。)
  • 去掉该拦截器

回到MetricsInterceptorConfiguration,看到如下代码:

@Configuration
@ConditionalOnProperty(value = "spring.cloud.netflix.metrics.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnClass({ Monitors.class, MetricReader.class })
public class MetricsInterceptorConfiguration {

熟悉springboot的一看就明白,只需要将属性spring.cloud.netflix.metrics.enabled置为false即可关闭该自动配置文件类。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/dai-ma-ren-sheng/3770

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
分享到

记一次spring-cloud-netflix-core引发的内存溢出分析

也可使用浏览器菜单中的「分享」功能

微信扫一扫分享

标签: JAVA JVM OOM Spring Cloud SpringBoot 异常
最后更新:2025年11月20日

岁月同一天 9 月 24 日

回望过去的今天,你在写什么

  • 文学
    5 年前 2021年9月24日
    玉楼春·尊前拟把归期说

    玉楼春·尊前拟把归期说 欧阳修 〔宋代〕 尊前拟把归期说,欲语春容先惨咽。人生自是有情痴,此恨不关风与月。 离歌且莫翻新…

  • 7 年前 2019年9月24日
    网站SEO(搜索引擎优化)说明及总结

    SEO是由英文Search Engine Optimization缩写而来, 中文意译为“搜索引擎优化”。SEO是指通过…

  • 7 年前 2019年9月24日
    Xshell 家庭、学校免费版下载

    Xshell功能强大,但是网上一般下载的都是收费的、或者就是破解版的;不过官方提供了针对家庭/学校的免费版,功能是一样的…

相关文章
  • JVM内存结构详解2019年10月18日
  • Spring Boot 2.x使用PostgreSQL数据库2021年10月9日
  • JVM详细参数说明2020年7月27日
  • 深入理解 Java 泛型:从通配符到实战应用全解析2025年10月14日
  • 【收藏】Java问题排查相关工具工具箱2019年10月16日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

还没有评论,快来抢沙发吧~

人们总把人生最美好的阶段用来赚钱,以便在人生最没有价值的阶段,能够享受一点值得怀疑的自由。

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
Redis7+&8.X 全新进阶系列(02):Redis Functions 详解——替代Lua脚本的官方轻量化函数方案 Redis7.x&8.x 全新进阶系列(01):划时代升级总览——从6.x到7.x/8.x全版本变革全景 让WordPress静态化之Rocket‑Nginx WordPress下一代默认主题Ipsum预览 C++之父重磅发声:AI编程正在毁掉一代程序员 支撑全网40%网站的WordPress正在重新拥抱PHP生态
关于主题加载速度优化的一点儿小演进给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能WordPress缓存插件WP Fastest Cache、WP Rocket 、FlyingPress对比关于使用AI的一些思考WordPress下一代默认主题Ipsum预览Kratos+ v1.1.16版本更新说明
出院了~~~ WordPress的自动更新好烦啊 JVM参数中的-D是什么意思 ThreadLocal如何解决内存泄漏问题 C++之父重磅发声:AI编程正在毁掉一代程序员 分库分表正在被淘汰?NewSQL与分库分表的深度博弈与选型指南
最近评论
李锋镝 发布于 1 天前(09月22日) 是的,换风格了,不过我觉得之前的年份命名挺好的,一看就知道哪一年的
obaby 发布于 2 天前(09月22日) 我现在是个假的wp了,哈哈哈 wp终于改了主题的命名风格了
李锋镝 发布于 4 天前(09月20日) 静态博客我之前也用过,但是感觉不是很方便,后来就一直用的WordPress
Sheep5 发布于 4 天前(09月20日) 我直接用静态博客,天然有速度优势。
不凡 发布于 4 天前(09月20日) 主要是wordpress插件丰富,需要什么功能插件,插件市场应有尽有,typecho是性能更好、更轻...
标签聚合
AI IDEA 分布式 多线程 数据库 SpringBoot AI编程 Claude SQL 日常 Spring WordPress JAVA ElasticSearch 架构 Theme K8s MySQL JVM Redis
友情链接
  • Serendipity
  • 九仞之行
  • 皮皮社
  • 彬红茶日记
  • 韩小韩博客
  • 瓦匠个人小站
  • sssr7844的博客
  • 蜗牛工作室
  • 老张博客
  • Honesty
  • 搬砖日记
  • Mr.Sun的博客
  • 若梦博客
  • 林羽凡
  • 临窗旋墨
  • lijie blog
  • 志文工作室
  • 知向前端
  • 韩情脉脉
  • 哥斯拉

COPYRIGHT © 2016-2026 lifengdi.com. ALL RIGHTS RESERVED.

lifengdi.com

Domain age badge for lifengdi.com

Theme Kratos-plus By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号