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

发现问题

公司线上的服务运行一段时间后就出现某个服务节点无响应,查看内存监控,对应的Jvm的堆耗尽。好在服务是多节点,线上dump运行服务的Jvm快照,下载到本地进行分析。
使用MAT打开快照文件,此处省略掉使用MAT的过程,分析发现有大量的com.netflix.servo.monitor.BasicTimer未释放,且被org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache占用。

分析问题

在工程中查找到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;很熟悉的对象,Spring的Rest服务访问客户端,公司的微服务采用Restful接口,使用该对象作为客户端。
然后进入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后增加一个签名参数,密钥只有公司的各个服务节点上配置,签名校验通过则允许访问,不通过则直接拒绝访问,这样可提高一下接口的安全等级;签名机制中,明文混入了一个随机数,增强签名的安全性,这样就导致了每次的接口访问url都不一样,然后在DefaultMetricsTagProvider中解析的uri也就都不一样,最终导致了MonitorConfig对象不一样,所以接口调用一次,生成一个BasicTimer对象,久而久之也就打爆Jvm堆内存。

解决方案

  • 改变签名机制,将签名放入PostBody中
  • 去掉该拦截器
    因为公司服务的接口监控已有其他第三方组件服务完成,不需使用netflix-core的监控,所以选择第二种方案。
    实现方法
    回到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即可关闭该自动配置文件类。

最后

一次隐藏比较深的崩溃经历,springboot和springcloud带来了极大的开发便捷性,由本人极力主张将后端开发栈转为springcloud,但便利的同时,也带来了更多的不透明,随之也就会出现各种各样的问题。
继续提高技术内力、充分学会各种分析工具、掌握正确的代码阅读方法,才能应对未知的问题。
欢迎各位提建议,交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342