系列文章彙總(更新中)
- 原始碼解析系列
- 實踐系列
RocketMQ 快速開始
RocketMQ 簡介:Apache RocketMQ是一個分散式訊息傳遞和流媒體平臺,具有低延遲、高效能和可靠性、萬億級容量和靈活的可伸縮性。它提供了多種功能,具體參考: github.com/apache/rock… 。
官方指導手冊快速開始中提到,RocketMQ 安裝需要具體以下條件:
- 64bit OS, 推薦使用 Linux/Unix/Mac
- 64bit JDK 1.8+
- Maven 3.2.x
- 4g+ free disk for Broker server (這個需要特別關注下)
下載安裝和編譯
wget https://archive.apache.org/dist/rocketmq/4.7.0/rocketmq-all-4.7.0-source-release.zip
unzip rocketmq-all-4.7.0-source-release.zip
cd rocketmq-all-4.7.0/
mvn -Prelease-all -DskipTests clean install -U
cd distribution/target/rocketmq-4.7.0/rocketmq-4.7.0
複製程式碼
1、啟動 Name Server
> nohup sh bin/mqnamesrv &
> tail -f ~/logs/rocketmqlogs/namesrv.log
The Name Server boot success...
複製程式碼
2、啟動 Broker
> nohup sh bin/mqbroker -n localhost:9876 &
# nohup sh bin/mqbroker -n localhost:9876 autoCreateTopicEnable=true &
> tail -f ~/logs/rocketmqlogs/broker.log
The broker[%s, 172.30.30.233:10911] boot success...
複製程式碼
autoCreateTopicEnable:使用 RocketMQ 進行發訊息時,必須要指定 topic,對於 topic 的設定有一個開關 autoCreateTopicEnable,一般在開發測試環境中會使用預設設定 autoCreateTopicEnable = true,但是這樣就會導致 topic 的設定不容易規範管理,沒有統一的稽核等等,所以在正式環境中會在 Broker 啟動時設定引數 autoCreateTopicEnable = false。這樣當需要增加 topic 時就需要在 web 管理介面上或者通過 admin tools 新增即可
SpringBoot 整合
RocketMQ 目前沒有提供整合 SpringBoot 的 starter,因此現在接入都是通過引入客戶端進行程式設計。下面來看下 SpringBoot 整合 RocketMQ 的過程。
引入 RocketMQ 客戶端依賴
github 上目前更新的最新版本是 4.7.0 版本,這裡就使用最新版本:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.0</version>
</dependency>
複製程式碼
提供生產者的自動配置類
/**
* @author: guolei.sgl (glmapper_2018@163.com) 2020/4/5 5:17 PM
* @since:
**/
@Configuration
public class MQProducerConfiguration {
public static final Logger LOGGER = LoggerFactory.getLogger(MQProducerConfiguration.class);
@Value("${rocketmq.producer.groupName}")
private String groupName;
@Value("${rocketmq.producer.namesrvAddr}")
private String namesrvAddr;
@Value("${rocketmq.producer.maxMessageSize}")
private Integer maxMessageSize;
@Value("${rocketmq.producer.sendMsgTimeout}")
private Integer sendMsgTimeout;
@Value("${rocketmq.producer.retryTimesWhenSendFailed}")
private Integer retryTimesWhenSendFailed;
@Bean
@ConditionalOnMissingBean
public DefaultMQProducer defaultMQProducer() throws RuntimeException {
DefaultMQProducer producer = new DefaultMQProducer(this.groupName);
producer.setNamesrvAddr(this.namesrvAddr);
producer.setCreateTopicKey("AUTO_CREATE_TOPIC_KEY");
//如果需要同一個 jvm 中不同的 producer 往不同的 mq 叢集傳送訊息,需要設定不同的 instanceName
//producer.setInstanceName(instanceName);
//如果傳送訊息的最大限制
producer.setMaxMessageSize(this.maxMessageSize);
//如果傳送訊息超時時間
producer.setSendMsgTimeout(this.sendMsgTimeout);
//如果傳送訊息失敗,設定重試次數,預設為 2 次
producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
try {
producer.start();
LOGGER.info("producer is started. groupName:{}, namesrvAddr: {}", groupName, namesrvAddr);
} catch (MQClientException e) {
LOGGER.error("failed to start producer.", e);
throw new RuntimeException(e);
}
return producer;
}
}
複製程式碼
- groupName: 傳送同一類訊息的設定為同一個 group,保證唯一, 預設不需要設定,rocketmq 會使用 ip@pid(pid代表jvm名字) 作為唯一標示。
- namesrvAddr:Name Server 地址
- maxMessageSize:訊息最大限制,預設 4M
- sendMsgTimeout:訊息傳送超時時間,預設 3 秒
- retryTimesWhenSendFailed:訊息傳送失敗重試次數,預設 2 次
提供消費者的自動配置類
@Configuration
public class MQConsumerConfiguration {
public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumerConfiguration.class);
@Value("${rocketmq.consumer.namesrvAddr}")
private String namesrvAddr;
@Value("${rocketmq.consumer.groupName}")
private String groupName;
@Value("${rocketmq.consumer.consumeThreadMin}")
private int consumeThreadMin;
@Value("${rocketmq.consumer.consumeThreadMax}")
private int consumeThreadMax;
// 訂閱指定的 topic
@Value("${rocketmq.consumer.topics}")
private String topics;
@Value("${rocketmq.consumer.consumeMessageBatchMaxSize}")
private int consumeMessageBatchMaxSize;
@Autowired
private MQConsumeMsgListenerProcessor mqMessageListenerProcessor;
@Bean
@ConditionalOnMissingBean
public DefaultMQPushConsumer defaultMQPushConsumer() throws RuntimeException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
consumer.setNamesrvAddr(namesrvAddr);
consumer.setConsumeThreadMin(consumeThreadMin);
consumer.setConsumeThreadMax(consumeThreadMax);
consumer.registerMessageListener(mqMessageListenerProcessor);
// 設定 consumer 第一次啟動是從佇列頭部開始消費還是佇列尾部開始消費
// 如果非第一次啟動,那麼按照上次消費的位置繼續消費
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 設定消費模型,叢集還是廣播,預設為叢集
consumer.setMessageModel(MessageModel.CLUSTERING);
// 設定一次消費訊息的條數,預設為 1 條
consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
try {
// 設定該消費者訂閱的主題和tag,如果是訂閱該主題下的所有tag,使用*;
consumer.subscribe(topics, "*");
// 啟動消費
consumer.start();
LOGGER.info("consumer is started. groupName:{}, topics:{}, namesrvAddr:{}",groupName,topics,namesrvAddr);
} catch (Exception e) {
LOGGER.error("failed to start consumer . groupName:{}, topics:{}, namesrvAddr:{}",groupName,topics,namesrvAddr,e);
throw new RuntimeException(e);
}
return consumer;
}
}
複製程式碼
引數參考上述生產者部分。這裡配置只是啟動的消費端的監聽,具體的消費需要再實現一個 MessageListenerConcurrently 介面。
/**
* @author: guolei.sgl (glmapper_2018@163.com) 2020/4/5 5:21 PM
* @since:
**/
@Component
public class MessageListenerHandler implements MessageListenerConcurrently {
private static final Logger LOGGER = LoggerFactory.getLogger(MessageListenerHandler.class);
private static String TOPIC = "DemoTopic";
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
if (CollectionUtils.isEmpty(msgs)) {
LOGGER.info("receive blank msgs...");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
MessageExt messageExt = msgs.get(0);
String msg = new String(messageExt.getBody());
if (messageExt.getTopic().equals(TOPIC)) {
// mock 消費邏輯
mockConsume(msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
private void mockConsume(String msg){
LOGGER.info("receive msg: {}.", msg);
}
}
複製程式碼
使用客戶端傳送訊息
使用客戶端傳送訊息的邏輯比較簡單,就是拿到 DefaultMQProducer 物件,呼叫 send 方法,支援同步、非同步、oneway 等多種呼叫方式。
@RestController
public class TestController {
private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
private static String TOPIC = "DemoTopic";
private static String TAGS = "glmapperTags";
@Autowired
private DefaultMQProducer defaultMQProducer;
@RequestMapping("send")
public String test() throws Throwable {
Message msg = new Message(TOPIC, TAGS, ("Say Hello RocketMQ to Glmapper").getBytes(RemotingHelper.DEFAULT_CHARSET));
// 呼叫客戶端傳送訊息
SendResult sendResult = defaultMQProducer.send(msg);
LOGGER.info("sendResult: {}.",sendResult);
return "SUCCESS";
}
}
複製程式碼
測試
這裡的測試應用是將生產端和消費端放在一起的,所以配置如下:
spring.application.name=test-rocket
server.port=8008
#producer
rocketmq.producer.isOnOff=on #該應用是否啟用生產者
rocketmq.producer.groupName=${spring.application.name}
rocketmq.producer.namesrvAddr=sofa.cloud.alipay.net:9876
rocketmq.producer.maxMessageSize=4096
rocketmq.producer.sendMsgTimeout=3000
rocketmq.producer.retryTimesWhenSendFailed=2
#consumer
rocketmq.consumer.isOnOff=on #該應用是否啟用消費者
rocketmq.consumer.groupName=${spring.application.name}
rocketmq.consumer.namesrvAddr=sofa.cloud.alipay.net:9876
rocketmq.consumer.topics=DemoTopic
rocketmq.consumer.consumeThreadMin=20
rocketmq.consumer.consumeThreadMax=64
rocketmq.consumer.consumeMessageBatchMaxSize=1
複製程式碼
啟動程式,檢視日誌輸出:
2020-04-05 22:53:15.141 INFO 46817 --- [ main] c.g.b.b.c.MQProducerConfiguration : producer is started. groupName:test-rocket, namesrvAddr: sofa.cloud.alipay.net:9876
2020-04-05 22:53:15.577 INFO 46817 --- [ main] c.g.b.b.c.MQConsumerConfiguration : consumer is started. groupName:test-rocket, topics:DemoTopic, namesrvAddr:sofa.cloud.alipay.net:9876
複製程式碼
這裡看到,生產者和消費者自動配置已經生效並啟動完成。通過 curl localhost:8008/send 來觸發訊息傳送:
2020-04-05 22:54:21.654 INFO 46817 --- [nio-8008-exec-1] c.g.b.boot.controller.TestController : sendResult: SendResult [sendStatus=SEND_OK, msgId=1E0FC3A2B6E118B4AAC21983B3C50000, offsetMsgId=64583D7C00002A9F0000000000011788, messageQueue=MessageQueue [topic=DemoTopic, brokerName=sofa.cloud.alipay.net, queueId=6], queueOffset=50].
2020-04-05 22:54:21.658 INFO 46817 --- [MessageThread_1] c.g.b.b.p.MessageListenerHandler : receive msg: Say Hello RocketMQ to Glmapper.
複製程式碼
看到傳送訊息的日誌和接受訊息的日誌。
使用 hook 攔截訊息
RocKetMQ 中提供了兩個 hook 介面:SendMessageHook 和 ConsumeMessageHook 介面,可以用於在訊息傳送之前、之後,訊息消費之前、之後對訊息進行攔截,官方文件中並沒有關於這部分的描述,那麼這裡我們就來看下如何使用這兩個 hook 介面來搞點事情。
SendMessageHook
自定義一個 ProducerTestHook ,程式碼如下:
public class ProducerTestHook implements SendMessageHook {
public static final Logger LOGGER = LoggerFactory.getLogger(ProducerTestHook.class);
@Override
public String hookName() {
return ProducerTestHook.class.getName();
}
@Override
public void sendMessageBefore(SendMessageContext sendMessageContext) {
LOGGER.info("execute sendMessageBefore. sendMessageContext:{}", sendMessageContext);
}
@Override
public void sendMessageAfter(SendMessageContext sendMessageContext) {
LOGGER.info("execute sendMessageAfter. sendMessageContext:{}", sendMessageContext);
}
}
複製程式碼
在上面生產者的自動配置類中,將 ProducerTestHook 註冊給 producer。
// 註冊 SendMessageHook
producer.getDefaultMQProducerImpl().registerSendMessageHook(new ProducerTestHook());
複製程式碼
ConsumeMessageHook
自定義一個 ConsumerTestHook ,程式碼如下:
public class ConsumerTestHook implements ConsumeMessageHook {
public static final Logger LOGGER = LoggerFactory.getLogger(ConsumerTestHook.class);
@Override
public String hookName() {
return ConsumerTestHook.class.getName();
}
@Override
public void consumeMessageBefore(ConsumeMessageContext consumeMessageContext) {
LOGGER.info("execute consumeMessageBefore. consumeMessageContext: {}",consumeMessageContext);
}
@Override
public void consumeMessageAfter(ConsumeMessageContext consumeMessageContext) {
LOGGER.info("execute consumeMessageAfter. consumeMessageContext: {}",consumeMessageContext);
}
}
複製程式碼
在上面消費者的自動配置類中,將 ConsumerTestHook 註冊給 consumer
// 註冊 ConsumeMessageHook
consumer.getDefaultMQPushConsumerImpl().registerConsumeMessageHook(new ConsumerTestHook());
複製程式碼
執行結果如下:
execute sendMessageBefore. sendMessageContext:org.apache.rocketmq.client.hook.SendMessageContext@a50ea34
execute sendMessageAfter. sendMessageContext:org.apache.rocketmq.client.hook.SendMessageContext@a50ea34
sendResult: SendResult [sendStatus=SEND_OK, msgId=0A0FE8F8C02F18B4AAC21C1275FB0000, offsetMsgId=64583D7C00002A9F0000000000011850, messageQueue=MessageQueue [topic=DemoTopic, brokerName=sofa.cloud.alipay.net, queueId=5], queueOffset=50].
execute consumeMessageBefore. consumeMessageContext: org.apache.rocketmq.client.hook.ConsumeMessageContext@6482209a
receive msg: Say Hello RocketMQ to Glmapper.
execute consumeMessageAfter. consumeMessageContext: org.apache.rocketmq.client.hook.ConsumeMessageContext@6482209a
複製程式碼
遇到的一些問題
整合過程中遇到幾個問題記錄如下:
1、Broker 啟動失敗。
我在測試時遇到的情況是,在 Name Server 啟動之後,再啟動 Boker 時,ssh 連線會直接提示 connect conversation fail. 通過 dmesg | egrep -i -B100 'killed process'
檢視程式被 kill 的記錄,得到如下日誌:
[2257026.030741] Memory cgroup out of memory: Kill process 110719 (systemd) score 0 or sacrifice child
[2257026.031888] Killed process 100735 (sh) total-vm:15708kB, anon-rss:176kB, file-rss:1800kB, shmem-rss:0kB
[2257026.133506] Memory cgroup out of memory: Kill process 110719 (systemd) score 0 or sacrifice child
[2257026.133539] Killed process 100745 (vsar) total-vm:172560kB, anon-rss:22936kB, file-rss:1360kB, shmem-rss:0kB
[2257026.206872] Memory cgroup out of memory: Kill process 104617 (java) score 3 or sacrifice child
[2257026.207742] Killed process 104617 (java) total-vm:9092924kB, anon-rss:4188528kB, file-rss:496kB, shmem-rss:0kB
複製程式碼
那這裡看到的結論是發生了 OOM,這裡是啟動時沒喲分配到足夠的空間導致的(預設配置檔案初始記憶體設定的太大了)。解決辦法是:進入到編譯之後的 distribution/target/apache-rocketmq/bin 目錄,找到 runbroker.sh 和 runserver.sh 兩個指令碼檔案,這兩個指令碼理解啟動時預設指定的引數是非常大的(4g/8g/2g),我線下測試機器總共才 1c2g,所以適當的調整了下引數:
- runserver.sh
JAVA_OPT="${JAVA_OPT} -server -Xms128m -Xmx256m -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
複製程式碼
- runbroker.sh
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m"
複製程式碼
修改後重新啟動 namesrv 和 broker ,正常了
$ jps
98633 Jps
55689 BrokerStartup
54906 NamesrvStartup
複製程式碼
2、No Topic Route Info,xxx
這個在官方的 FAQ 裡面有提到,說明遇到的頻次一定是很高的。官方給出的方案可以詳解這裡 rocketmq.apache.org/docs/faq/ 第4條。我是通過 If you can’t find this topic, create it on a broker via admin tools command updateTopic or web console. 這個解決的:
sh mqadmin updateTopic -b localhost:10911 -n localhost:9876 -t DemoTopic # 執行此指令,建立 DemoTopic
RocketMQLog:WARN No appenders could be found for logger (io.netty.util.internal.PlatformDependent0).
RocketMQLog:WARN Please initialize the logger system properly.
create topic to localhost:10911 success.
TopicConfig [topicName=DemoTopic, readQueueNums=8, writeQueueNums=8, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
複製程式碼
總結
之前在做 SOFATracer 整合訊息元件時有看過 RocketMQ 的部分程式碼,但是在實際操作時還是饒了不少彎路。總體來看,SpringBoot 整合 RocketMQ 還是比較簡單的,在此記錄一下。如果文中有描述有誤的地方,還請各位大佬留言指正。