背景
某一天,和我們配合的中臺(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)了以下異常
一下子就精神了氓扛,這就是臭名昭著的內(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芽卿。
更改啟動(dòng)參數(shù)揭芍,本地運(yùn)行后,仍然會(huì)報(bào)錯(cuò)
呃卸例。称杨。肌毅。。
查看VisualVm
這時(shí)候打開VisualVm看看姑原,可以看到設(shè)置的最大堆大小在1000MB悬而,而已使用的堆內(nèi)存大小才100多MB,此時(shí)能夠篤定是創(chuàng)建了大對象而導(dǎo)致的內(nèi)存溢出锭汛。
斷點(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
從上圖可以看到蔚袍,輸入流讀取的字節(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
系統(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é)議層面的通信幀
上圖從左到右依次為幀類型、通道編號(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));
}
斷點(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
```
- 查看抓包
從抓包可以看到翰苫,字節(jié)碼對上了,而且看到響應(yīng)碼為400这橙,Bad RequestW嘁ぁ!析恋!
這也驗(yàn)證了一開始提到的猜測:MQ賬號(hào)有問題良哲,于是咨詢了中臺(tái)組盛卡,最終發(fā)現(xiàn)助隧,是因?yàn)?.0部門給的端口有問題,導(dǎo)致socket無法連接!
分析的過程非常有趣并村,雖然結(jié)果很狗血巍实。。