干貨分享-Rabbitmq 引發(fā)的內(nèi)存溢出及排查思路

背景

某一天,和我們配合的中臺(tái)組給我們部門發(fā)了一組新的MQ配置僧家,用于支付回調(diào)消息的接收笆搓,原來我們的某個(gè)項(xiàng)目已經(jīng)有一個(gè)MQ,所以項(xiàng)目需要適配兩個(gè)MQ(該項(xiàng)目都是作為消費(fèi)者的角色)瓢谢。

spring rabbitmq使用的版本是

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <version>2.1.5.RELEASE</version>
</dependency>

兼容多MQ的代碼

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Slf4j
@Configuration
public class RabbitConfig1 {
    @Bean(name = "connectionFactory1")
    @Primary
    public ConnectionFactory connectionFactory1 (
            @Value("${spring.rabbitmq.host}") String host,
            @Value("${spring.rabbitmq.port}") int port,
            @Value("${spring.rabbitmq.username}") String username,
            @Value("${spring.rabbitmq.password}") String password
    ) {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        return connectionFactory;
    }
    @Bean(name = "rabbitTemplate1")
    @Primary
    public RabbitTemplate rabbitTemplate1 (
            @Qualifier("connectionFactory1") ConnectionFactory connectionFactory
    ) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        return rabbitTemplate;
    }
    @Bean(name = "listenerContainerFactory1")
    public SimpleRabbitListenerContainerFactory listenerContainerFactory1 (
            SimpleRabbitListenerContainerFactoryConfigurer configurer,
            @Qualifier("connectionFactory1") ConnectionFactory connectionFactory
    ) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        configurer.configure(factory, connectionFactory);
        return factory;
    }
}

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
@ConditionalOnProperty(name = "pay.callback.message.config.enable", havingValue = "true")
public class RabbitConfig2 {
    @Bean(name = "connectionFactory2")
    public ConnectionFactory connectionFactory2(
            @Value("${pay.callback.rabbitmq.host}") String host,
            @Value("${pay.callback.rabbitmq.port}") int port,
            @Value("${pay.callback.rabbitmq.userName}") String userName,
            @Value("${pay.callback.rabbitmq.password}") String password
    ) {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
        connectionFactory.setUsername(userName);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost("/");
        return connectionFactory;
    }
    @Bean(name = "listenerContainerFactory2")
    public SimpleRabbitListenerContainerFactory listenerContainerFactory2 (
            SimpleRabbitListenerContainerFactoryConfigurer configurer,
            @Qualifier("connectionFactory2") ConnectionFactory connectionFactory
    ) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        configurer.configure(factory, connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
        factory.setDefaultRequeueRejected(false);
        return factory;
    }
}

測試

開發(fā)環(huán)境驗(yàn)證通過,發(fā)布到測試環(huán)境時(shí)驮瞧,出現(xiàn)了以下異常

image

一下子就精神了氓扛,這就是臭名昭著的內(nèi)存溢出
回顧以往出現(xiàn)內(nèi)存溢出,往往有以下幾種

內(nèi)存溢出

堆空間溢出

java.lang.OutOfMemoryError: Java heap space

出現(xiàn)的原因一般是

  • 數(shù)據(jù)突增论笔。比如突然創(chuàng)建了大對象采郎,超出了最大堆空間內(nèi)存,可能還來不及回收狂魔,也可能根本就無法滿足蒜埋。
  • 對象堆積。一般是程序編碼有問題最楷,導(dǎo)致創(chuàng)建的對象一直堆積在堆內(nèi)存整份,無法被GC探測回收。

永久代溢出

java.lang.OutOfMemoryError: PermGen space

元空間溢出

java.lang.OutOfMemoryError: Metaspace

元空間的概念是在jdk1.8提出來的籽孙,用來取代以前的永久代烈评。永久代

遇到這種問題,冷靜蚯撩,接著一步步校驗(yàn)

查看jvm啟動(dòng)參數(shù)

java -server -Xmx512M -Xms512M -Denv=FAT -XX:+UseCodeCacheFlushing -XX:+HeapDumpOnOutOfMemoryError -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-OmitStackTraceInFastThrow -jar /usr/local/application/**.jar

可以看出础倍,啟動(dòng)參數(shù)限制了最大堆內(nèi)存是515M烛占,因?yàn)槭菧y試環(huán)境胎挎,部署了很多個(gè)項(xiàng)目,保險(xiǎn)起見設(shè)置的忆家,平時(shí)也都正常犹菇。

那就是說調(diào)大最大堆內(nèi)存就可以,接下來試一下把最大堆內(nèi)存調(diào)整為1G芽卿。

image.png

更改啟動(dòng)參數(shù)揭芍,本地運(yùn)行后,仍然會(huì)報(bào)錯(cuò)


image

呃卸例。称杨。肌毅。。

查看VisualVm

這時(shí)候打開VisualVm看看姑原,可以看到設(shè)置的最大堆大小在1000MB悬而,而已使用的堆內(nèi)存大小才100多MB,此時(shí)能夠篤定是創(chuàng)建了大對象而導(dǎo)致的內(nèi)存溢出锭汛。

image.png

斷點(diǎn)調(diào)試

這一步開始來斷點(diǎn)笨奠,排查大對象從哪里來,此時(shí)查看報(bào)錯(cuò)的源碼唤殴,發(fā)現(xiàn)確實(shí)是因?yàn)榇髮ο蟮膭?chuàng)建導(dǎo)致
代碼在com.rabbitmq.client.impl.Frame 類中般婆,F(xiàn)rame是指AMQP協(xié)議層面的通信幀。

對于Frame的理解朵逝,可以查看其它博客:https://blog.csdn.net/usagoole/article/details/83048009

image

從上圖可以看到蔚袍,輸入流讀取的字節(jié)數(shù)為1345270062,這時(shí)候即創(chuàng)建了一個(gè)大小為1345270062(1.2G)的字節(jié)數(shù)組配名,于是乎出現(xiàn)內(nèi)存溢出页响。

至于為什么會(huì)突然讀取到這么大的字節(jié)數(shù),重新調(diào)試段誊,我把斷點(diǎn)打在com.rabbitmq.client.impl.SocketFrameHandler

image.png

系統(tǒng)有兩個(gè)MQ闰蚕,原有的MQ一切正常,從支付回調(diào)MQ開始连舍,就開始報(bào)錯(cuò)了没陡,所以初步懷疑是這個(gè)MQ賬號(hào)的問題,或許是賬號(hào)不對索赏?沒有遠(yuǎn)程登錄的權(quán)限盼玄?

理解源碼

Rabbitmq是基于socket連接讀取的輸入流,再將它轉(zhuǎn)成字節(jié)數(shù)組潜腻。
先熟悉一下com.rabbitmq.client.impl.Frame
幀(Frame)埃儿,AMQP協(xié)議層面的通信幀

image

上圖從左到右依次為幀類型、通道編號(hào)融涣、幀大小童番、內(nèi)容、結(jié)束標(biāo)記組成一個(gè)幀

從上面調(diào)試的代碼可以看出威鹿,我們是打算取出payload這一段內(nèi)容時(shí)剃斧,超出了長度。

再看看以下代碼忽你,
readInt()的作用是幼东,讀取四個(gè)輸入字節(jié),并做了位移運(yùn)算,返回一個(gè)整型值根蟹。
一個(gè)int存儲(chǔ)的是32位的整型數(shù)據(jù)脓杉,32bit = 4 * 1byte,即表明每次從輸入流里讀取4個(gè)字節(jié)的數(shù)據(jù)简逮;

int payloadSize = is.readInt();

public final int readInt() throws IOException {
    int ch1 = in.read();
    int ch2 = in.read();
    int ch3 = in.read();
    int ch4 = in.read();
    if ((ch1 | ch2 | ch3 | ch4) < 0)
        throw new EOFException();
    return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}

image.png

斷點(diǎn)可以看出丽已,返回的整型值,也就是payload的長度买决,達(dá)到了1345270062沛婴,這樣下一步創(chuàng)建byte對象的時(shí)候,就出現(xiàn)內(nèi)存溢出的事故督赤。

但是為什么會(huì)出現(xiàn)這個(gè)大對象嘁灯,回過頭去分析readInt()in.read()將16進(jìn)制的網(wǎng)絡(luò)字節(jié)碼 轉(zhuǎn)為10進(jìn)制的數(shù)組躲舌,正
是因?yàn)樽x取的數(shù)據(jù)有問題丑婿,才導(dǎo)致位移運(yùn)算后得到一個(gè)比較大的整型值。

抓包

圍繞著上面這個(gè)問題没卸,此時(shí)需要抓個(gè)包看看羹奉,采取的是邊斷點(diǎn)邊抓包的方式。

  • 打開抓包工具约计,過濾器設(shè)置指定ip為MQ的host

  • 先斷點(diǎn)到111行诀拭,接著啟動(dòng)程序

    image.png
  • 當(dāng)打到該斷點(diǎn)的時(shí)候,看到幀大小比較大的時(shí)候煤蚌,進(jìn)入readInt()

    image.png
可以看到此時(shí)讀取的4個(gè)數(shù)值分別是80耕挨、47、49尉桩、46筒占,由于是網(wǎng)絡(luò)字節(jié)碼轉(zhuǎn)過來的,故轉(zhuǎn)為16進(jìn)制后蜘犁,對應(yīng)為

```
DEC:80  47  49  46
HEX:50  2F  31  2E    

```
  • 查看抓包
image.png

從抓包可以看到翰苫,字節(jié)碼對上了,而且看到響應(yīng)碼為400这橙,Bad RequestW嘁ぁ!析恋!

這也驗(yàn)證了一開始提到的猜測:MQ賬號(hào)有問題良哲,于是咨詢了中臺(tái)組盛卡,最終發(fā)現(xiàn)助隧,是因?yàn)?.0部門給的端口有問題,導(dǎo)致socket無法連接!

分析的過程非常有趣并村,雖然結(jié)果很狗血巍实。。

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哩牍,一起剝皮案震驚了整個(gè)濱河市棚潦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌膝昆,老刑警劉巖丸边,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異荚孵,居然都是意外死亡妹窖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門收叶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骄呼,“玉大人,你說我怎么就攤上這事判没◎烟眩” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵澄峰,是天一觀的道長嫉沽。 經(jīng)常有香客問我,道長俏竞,這世上最難降的妖魔是什么耻蛇? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮胞此,結(jié)果婚禮上臣咖,老公的妹妹穿的比我還像新娘。我一直安慰自己漱牵,他們只是感情好夺蛇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酣胀,像睡著了一般刁赦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上闻镶,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天甚脉,我揣著相機(jī)與錄音,去河邊找鬼铆农。 笑死牺氨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播猴凹,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼夷狰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了郊霎?” 一聲冷哼從身側(cè)響起沼头,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎书劝,沒想到半個(gè)月后进倍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡购对,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年背捌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洞斯。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毡庆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出烙如,到底是詐尸還是另有隱情么抗,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布亚铁,位于F島的核電站蝇刀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏徘溢。R本人自食惡果不足惜吞琐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望然爆。 院中可真熱鬧站粟,春花似錦、人聲如沸曾雕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剖张。三九已至切诀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搔弄,已是汗流浹背幅虑。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留顾犹,地道東北人倒庵。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓褒墨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哄芜。 傳聞我的和親對象是個(gè)殘疾皇子貌亭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355