背景
某一天,和我們配合的中臺組給我們部門發了一組新的MQ配置,用于支付回調消息的接收,原來我們的某個項目已經有一個MQ,所以項目需要適配兩個MQ(該項目都是作為消費者的角色)。
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;
}
}
測試
開發環境驗證通過,發布到測試環境時,出現了以下異常
一下子就精神了,這就是臭名昭著的內存溢出
回顧以往出現內存溢出,往往有以下幾種
內存溢出
堆空間溢出
java.lang.OutOfMemoryError: Java heap space
出現的原因一般是
- 數據突增。比如突然創建了大對象,超出了最大堆空間內存,可能還來不及回收,也可能根本就無法滿足。
- 對象堆積。一般是程序編碼有問題,導致創建的對象一直堆積在堆內存,無法被GC探測回收。
永久代溢出
java.lang.OutOfMemoryError: PermGen space
元空間溢出
java.lang.OutOfMemoryError: Metaspace
元空間的概念是在jdk1.8提出來的,用來取代以前的永久代。永久代
遇到這種問題,冷靜,接著一步步校驗
查看jvm啟動參數
java -server -Xmx512M -Xms512M -Denv=FAT -XX:+UseCodeCacheFlushing -XX:+HeapDumpOnOutOfMemoryError -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-OmitStackTraceInFastThrow -jar /usr/local/application/**.jar
可以看出,啟動參數限制了最大堆內存是515M,因為是測試環境,部署了很多個項目,保險起見設置的,平時也都正常。
那就是說調大最大堆內存就可以,接下來試一下把最大堆內存調整為1G。
更改啟動參數,本地運行后,仍然會報錯
呃。。。。
查看VisualVm
這時候打開VisualVm看看,可以看到設置的最大堆大小在1000MB,而已使用的堆內存大小才100多MB,此時能夠篤定是創建了大對象而導致的內存溢出。
斷點調試
這一步開始來斷點,排查大對象從哪里來,此時查看報錯的源碼,發現確實是因為大對象的創建導致
代碼在com.rabbitmq.client.impl.Frame
類中,Frame是指AMQP協議層面的通信幀。
對于Frame的理解,可以查看其它博客:https://blog.csdn.net/usagoole/article/details/83048009
從上圖可以看到,輸入流讀取的字節數為1345270062,這時候即創建了一個大小為1345270062(1.2G)的字節數組,于是乎出現內存溢出。
至于為什么會突然讀取到這么大的字節數,重新調試,我把斷點打在com.rabbitmq.client.impl.SocketFrameHandler
系統有兩個MQ,原有的MQ一切正常,從支付回調MQ開始,就開始報錯了,所以初步懷疑是這個MQ賬號的問題,或許是賬號不對?沒有遠程登錄的權限?
理解源碼
Rabbitmq是基于socket連接讀取的輸入流,再將它轉成字節數組。
先熟悉一下com.rabbitmq.client.impl.Frame
幀(Frame),AMQP協議層面的通信幀
上圖從左到右依次為幀類型、通道編號、幀大小、內容、結束標記組成一個幀
從上面調試的代碼可以看出,我們是打算取出payload這一段內容時,超出了長度。
再看看以下代碼,
readInt()的作用是,讀取四個輸入字節,并做了位移運算,返回一個整型值。
一個int存儲的是32位的整型數據,32bit = 4 * 1byte,即表明每次從輸入流里讀取4個字節的數據;
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));
}
斷點可以看出,返回的整型值,也就是payload的長度,達到了1345270062,這樣下一步創建byte對象的時候,就出現內存溢出的事故。
但是為什么會出現這個大對象,回過頭去分析readInt()
,in.read()
將16進制的網絡字節碼 轉為10進制的數組,正
是因為讀取的數據有問題,才導致位移運算后得到一個比較大的整型值。
抓包
圍繞著上面這個問題,此時需要抓個包看看,采取的是邊斷點邊抓包的方式。
打開抓包工具,過濾器設置指定ip為MQ的host
-
先斷點到111行,接著啟動程序
image.png -
當打到該斷點的時候,看到幀大小比較大的時候,進入readInt()
image.png
可以看到此時讀取的4個數值分別是80、47、49、46,由于是網絡字節碼轉過來的,故轉為16進制后,對應為
```
DEC:80 47 49 46
HEX:50 2F 31 2E
```
- 查看抓包
從抓包可以看到,字節碼對上了,而且看到響應碼為400,Bad Request!!!
這也驗證了一開始提到的猜測:MQ賬號有問題,于是咨詢了中臺組,最終發現,是因為1.0部門給的端口有問題,導致socket無法連接!
分析的過程非常有趣,雖然結果很狗血。。