spring-cloud-netflix-core引發(fā)的一次內(nèi)存溢出分析

發(fā)現(xiàn)問(wèn)題

公司線(xiàn)上的服務(wù)運(yùn)行一段時(shí)間后就出現(xiàn)某個(gè)服務(wù)節(jié)點(diǎn)無(wú)響應(yīng)皮迟,查看內(nèi)存監(jiān)控,對(duì)應(yīng)的Jvm的堆耗盡秸架。好在服務(wù)是多節(jié)點(diǎn),線(xiàn)上dump運(yùn)行服務(wù)的Jvm快照咆蒿,下載到本地進(jìn)行分析东抹。
使用MAT打開(kāi)快照文件,此處省略掉使用MAT的過(guò)程沃测,分析發(fā)現(xiàn)有大量的com.netflix.servo.monitor.BasicTimer未釋放缭黔,且被org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache占用。

分析問(wèn)題

在工程中查找到ServoMonitorCache類(lèi)蒂破,發(fā)現(xiàn)在spring-cloud-netflix-core包下馏谨,然后打開(kāi)該jar包,查看其spring.factories去查看是那里自動(dòng)配置生成了該類(lèi)寞蚌,找到org.springframework.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration中自動(dòng)配置田巴,然后再搜索那里使用了該類(lèi),在org.springframework.cloud.netflix.metrics.MetricsInterceptorConfiguration中發(fā)現(xiàn)了ServoMonitorCache對(duì)象的使用挟秤。看到metrics就明白抄伍,是對(duì)服務(wù)的監(jiān)控對(duì)象艘刚。代碼如下:

@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;
                /*
                  *此處為關(guān)鍵代碼
                  *編號(hào)1
                  */
        @Bean
        MetricsClientHttpRequestInterceptor spectatorLoggingClientHttpRequestInterceptor(
                Collection<MetricsTagProvider> tagProviders,
                ServoMonitorCache servoMonitorCache) {
            return new MetricsClientHttpRequestInterceptor(tagProviders,
                    servoMonitorCache, this.metricName);
        }

        @Bean
        BeanPostProcessor spectatorRestTemplateInterceptorPostProcessor() {
            return new MetricsInterceptorPostProcessor();
        }
                //編號(hào)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;
            }
        }
    }
}

在上面代碼中編號(hào)1處,自動(dòng)配置生成了MetricsClientHttpRequestInterceptor攔截器截珍,然后把ServoMonitorCache采用構(gòu)造器注入傳入了攔截器攀甚;然后代碼編號(hào)2處的postProcessAfterInitialization函數(shù)中,把該攔截器賦值給了RestTemplate岗喉;很熟悉的對(duì)象秋度,Spring的Rest服務(wù)訪(fǎng)問(wèn)客戶(hù)端,公司的微服務(wù)采用Restful接口钱床,使用該對(duì)象作為客戶(hù)端荚斯。
然后進(jìn)入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();
                        //編號(hào)3
            for (MetricsTagProvider tagProvider : tagProviders) {
                for (Map.Entry<String, String> tag : tagProvider
                        .clientHttpRequestTags(request, response).entrySet()) {
                    builder.add(Tags.newTag(tag.getKey(), tag.getValue()));
                }
            }
                        //編號(hào)4
            MonitorConfig.Builder monitorConfigBuilder = MonitorConfig
                    .builder(metricName);
            monitorConfigBuilder.withTags(builder);

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

編號(hào)3處代碼,發(fā)現(xiàn)對(duì)象tagProviders事期,回過(guò)去看代碼也是該攔截器構(gòu)造時(shí)傳入的參數(shù)滥壕;現(xiàn)在去看一下這個(gè)對(duì)象是什么,因?yàn)樵搶?duì)象是構(gòu)造器注入的兽泣,說(shuō)明也是由spring容器配置生成的绎橘,所以繼續(xù)在autoconfig文件中查找,發(fā)現(xiàn)在org.springframework.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration中自動(dòng)配置生成:

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

進(jìn)入DefaultMetricsTagProvider該對(duì)象代碼唠倦,核心代碼如下:

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

發(fā)現(xiàn)其就是分解了Http的客戶(hù)端請(qǐng)求称鳞,其中關(guān)鍵就是method(get、post稠鼻、delete等http方法)冈止、status狀態(tài)、clientName訪(fǎng)問(wèn)的服務(wù)域名枷餐、uri訪(fǎng)問(wèn)路徑(包含參數(shù))靶瘸。

然后,返回去看代碼編號(hào)4處毛肋,生成了一個(gè)對(duì)象com.netflix.servo.monitor.MonitorConfig,主要就是name和tags怨咪,name默認(rèn)的就是restclient(可以在屬性文件中修改);tags就是DefaultMetricsTagProvider中那些tag標(biāo)簽。
然后進(jìn)入ServoMonitorCache.getTimer函數(shù):

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

此處就很簡(jiǎn)單了润匙,先在緩存中查找該MonitorConfig對(duì)象有沒(méi)有诗眨,沒(méi)有則新增一個(gè)BasicTimer,若有就更新該BasicTimer的參數(shù)孕讳,題外話(huà)匠楚,BasicTimer就存儲(chǔ)了各個(gè)接口的訪(fǎng)問(wèn)最大時(shí)間、最小時(shí)間厂财、平均時(shí)間等芋簿。
分析到這里就明白了,我們公司的內(nèi)部服務(wù)直接互相訪(fǎng)問(wèn)時(shí)璃饱,采用了簽名校驗(yàn)与斤,即在訪(fǎng)問(wèn)時(shí),都在URL后增加一個(gè)簽名參數(shù)荚恶,密鑰只有公司的各個(gè)服務(wù)節(jié)點(diǎn)上配置撩穿,簽名校驗(yàn)通過(guò)則允許訪(fǎng)問(wèn),不通過(guò)則直接拒絕訪(fǎng)問(wèn)谒撼,這樣可提高一下接口的安全等級(jí)食寡;簽名機(jī)制中,明文混入了一個(gè)隨機(jī)數(shù)廓潜,增強(qiáng)簽名的安全性抵皱,這樣就導(dǎo)致了每次的接口訪(fǎng)問(wèn)url都不一樣善榛,然后在DefaultMetricsTagProvider中解析的uri也就都不一樣,最終導(dǎo)致了MonitorConfig對(duì)象不一樣叨叙,所以接口調(diào)用一次锭弊,生成一個(gè)BasicTimer對(duì)象,久而久之也就打爆Jvm堆內(nèi)存擂错。

解決方案

  • 改變簽名機(jī)制味滞,將簽名放入PostBody中
  • 去掉該攔截器
    因?yàn)楣痉?wù)的接口監(jiān)控已有其他第三方組件服務(wù)完成,不需使用netflix-core的監(jiān)控钮呀,所以選擇第二種方案剑鞍。
    實(shí)現(xiàn)方法
    回到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即可關(guān)閉該自動(dòng)配置文件類(lèi)蚁署。

最后

一次隱藏比較深的崩潰經(jīng)歷,springboot和springcloud帶來(lái)了極大的開(kāi)發(fā)便捷性蚂四,由本人極力主張將后端開(kāi)發(fā)棧轉(zhuǎn)為springcloud光戈,但便利的同時(shí),也帶來(lái)了更多的不透明遂赠,隨之也就會(huì)出現(xiàn)各種各樣的問(wèn)題久妆。
繼續(xù)提高技術(shù)內(nèi)力、充分學(xué)會(huì)各種分析工具跷睦、掌握正確的代碼閱讀方法筷弦,才能應(yīng)對(duì)未知的問(wèn)題。
歡迎各位提建議抑诸,交流烂琴。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蜕乡,隨后出現(xiàn)的幾起案子奸绷,更是在濱河造成了極大的恐慌,老刑警劉巖层玲,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件健盒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡称簿,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)惰帽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)憨降,“玉大人,你說(shuō)我怎么就攤上這事该酗∈谝” “怎么了士嚎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)悔叽。 經(jīng)常有香客問(wèn)我莱衩,道長(zhǎng),這世上最難降的妖魔是什么娇澎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任笨蚁,我火速辦了婚禮,結(jié)果婚禮上趟庄,老公的妹妹穿的比我還像新娘括细。我一直安慰自己,他們只是感情好戚啥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布奋单。 她就那樣靜靜地躺著,像睡著了一般猫十。 火紅的嫁衣襯著肌膚如雪览濒。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,462評(píng)論 1 302
  • 那天拖云,我揣著相機(jī)與錄音贷笛,去河邊找鬼。 笑死江兢,一個(gè)胖子當(dāng)著我的面吹牛昨忆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播杉允,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼邑贴,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了叔磷?” 一聲冷哼從身側(cè)響起拢驾,我...
    開(kāi)封第一講書(shū)人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎改基,沒(méi)想到半個(gè)月后繁疤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秕狰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年稠腊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸣哀。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖我衬,靈堂內(nèi)的尸體忽然破棺而出叹放,到底是詐尸還是另有隱情饰恕,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布井仰,位于F島的核電站埋嵌,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏俱恶。R本人自食惡果不足惜雹嗦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望速那。 院中可真熱鬧俐银,春花似錦、人聲如沸端仰。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)荔烧。三九已至吱七,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鹤竭,已是汗流浹背踊餐。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臀稚,地道東北人吝岭。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吧寺,于是被迫代替她去往敵國(guó)和親窜管。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354