Apache RocketMQ是一款開源的、分散式的訊息投遞與流資料平臺。出生自阿里巴巴,在阿里巴巴內部經歷了3個版本後,作為Apache
頂級開源專案之一直到現在。在GitHub上有10000+star、5000+fork、170+contributors(在GitHub上提交程式碼並被採納的開發者)
RocketMQ的前世
和大部分元件產生的原因類似,阿里巴巴內部為了適應淘寶 B2C 的更快、更復雜的業務,2001年啟動了“五彩石專案”,阿里巴巴的第一代訊息佇列服務Notify就是在這個背景下產生的。
2010 年,阿里巴巴內部的 Apache ActiveMQ 仍然作為核心技術被廣泛用於各個業務線,而順序訊息、海量訊息堆積、完全自主控制訊息佇列服務,也是阿里巴巴同時期急需的。在這種背景下,2011年,MetaQ 誕生
RocketMQ 雲化
2011年,LinkedIn將Kafka開源。2012年,阿里巴巴參考Kafka的設計,基於對MetaQ的理解和實際使用,研發了一套通用訊息佇列引擎,也就是 RocketMQ。自此才有了第一代真正的RocketMQ,2016年阿里雲上線雲RocketMQ訊息佇列服務。
Apache RocketMQ——金融級訊息佇列,一個擁有亞毫秒級延遲、萬億級訊息容量保證、高訊息容錯設計的中介軟體,在阿里巴巴、VIPKID、微眾銀行、民生銀行、螞蟻金服、滴滴等國內知名網際網路公司的實踐中,有著完美的表現。
隨著RocketMQ 5.0的釋出,藉助OpenMessaging提供跨平臺、多語言的能力,將會打通 Prometheus、ELK 等上游元件,透過訊息、Streaming 等形式將資料扭轉到 Flink、Elasticsearch、Hbase、Spark等下游元件。屆時整個生態體系將會更加完美、便捷。
RocketMQ支援3種訊息:普通訊息(併發訊息)、順序訊息、事務訊息
RocketMQ支援3種傳送方式:同步傳送、非同步傳送、單向傳送。
生產者概述
傳送訊息的一方被稱為生產者,它在整個RocketMQ的生產和消費體系中扮演的角色如圖所示。
生產者組: 一個邏輯概念,在使用生產者例項的時候需要指定一個組名。一個生產者組可以生產多個Topic的訊息。
生產者例項: 一個生產者組部署了多個程式,每個程式都可以稱為一個生產者例項。
Topic: 主題名字,一個Topic由若干Queue組成。
RocketMQ 客戶端中的生產者有兩個獨立實現類 :org.apache.rocketmq.client.producer.DefaultMQProducer 和org.apache.rocketmq.client.producer.TransactionMQProducer 。 前者用於生產普通訊息、順序訊息、單向訊息、批次訊息、延遲訊息,後
者主要用於生產事務訊息
訊息結構和訊息型別
訊息類的核心欄位
public class Message implements Serializable {
private static final long serialVersionUID = 8445773977080406428L;
// 主題名字,可以透過RocketMQ Console建立
private String topic;
// 目前沒用
private int flag;
// 訊息擴充套件資訊,Tag、keys、延遲級別都儲存在這裡
private Map<String, String> properties;
// 訊息體,位元組陣列。需要注意生產者使用什麼編碼,消費者也必須使用相同編碼解碼,否則會產生亂碼
private bytel[] body;
// 設定訊息的 key,多個 key 可 以 用MessageConst.KEY_SEPARATOR(空格)分隔或者直接用另一個過載方法。如果 Broker 中 messageIndexEnable=true 則會根據 key建立訊息的Hash索引,幫助使用者進行快速查詢。
public void setKeys(String keys) {}
public void setKeys(Collection<String> keys){}
// 訊息過濾的標記,使用者可以訂閱某個Topic的某些 Tag,這樣Broker只會把訂閱了topic-tag的訊息傳送給消費者。
public void setTags(String tags) {}
// 設定延遲級別,延遲多久消費者可以消費
public void setDelayTimeLevel(int level) { )
public void setTopic(String topic) { }
// 如果還有其他擴充套件資訊,可以存放在這裡。內部是一個Map,重複呼叫會覆蓋舊值。
public void putUserProperty(final String name, final String value) {...}
}
普通訊息
普通訊息也稱為併發訊息,和傳統的佇列相比,併發訊息沒有順序,但是生產消費都是並行進行的,單機效能可達十萬級別的TPS
分割槽有序訊息
與Kafka中的分割槽類似,把一個Topic訊息分為多個分割槽“儲存”和消費,在一個分割槽內的訊息就是傳統的佇列,遵循FIFO(先進先出)原則。
全域性有序訊息
如果把一個 Topic 的分割槽數設定為 1,那麼該Topic 中的訊息就是單分割槽,所有訊息都遵循FIFO(先進先出)的原則。
延遲訊息
訊息傳送後,消費者要在一定時間後,或者指定某個時間點才可以消費。在沒有延遲訊息時,基本的做法是基於定時計劃任務排程,定時傳送訊息。在 RocketMQ中只需要在傳送訊息時設定延遲級別即可實現
事務訊息
主要涉及分散式事務,即需要保證在多個操作同時成功或者同時失敗時,消費者才能消費訊息。RocketMQ透過傳送Half訊息、處理本地事務、提交(Commit)訊息或者回滾(Rollback)訊息優雅地實現分散式事務。
生產者高可用
客戶端保證
第一種保證機制:重試機制
RocketMQ 支援同步、非同步傳送,不管哪種方式都可以在配置失敗後重試,如果單個 Broker 發生故障,重試會選擇其他 Broker 保證訊息正常傳送。
配置項 retryTimesWhenSendFailed表示同步重試次數,預設為 2次,加上正常傳送 1次,總共3次機會。
同步傳送的重試: 程式碼可以參考org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(),每次傳送失敗後,除非傳送被打斷否則都會執行重試程式碼。
非同步傳送重試: 程式碼可以參考org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageAsync()
重試是在通訊層非同步傳送完成的,當operationComplete()方法返回的response值為null時,會重新執行重試程式碼。返回值 response為 null 通常是因為客戶端收到 TCP請求解包失敗,或者沒有找到匹配的request
生產者配置項 retryTimesWhenSendAsyncFailed 表示非同步重試的次數,預設為 2 次,加上正常傳送的1次,總共有3次傳送機會。
第二種保證機制:客戶端容錯
RocketMQ Client會維護一個“Broker-傳送延遲”關係,根據這個關係選擇一個傳送延遲級別較低的 Broker 來傳送訊息,這樣能最大限度地利用 Broker 的能力,剔除已經當機、不可用或者傳送延遲級別較高的 Broker,儘量保證訊息的正常傳送。
這種機制主要體現在傳送訊息時如何選擇 Queue,原始碼在 MQFaultStrategy.selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName)方法中
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
//第一步:獲取一個在延遲上可以接受,並且和上次傳送相同的Broker。首先獲取一個自增序號 index,透過取模獲取Queue的位置下標 Pos。如果 Pos對應的 Broker的延遲時間是可以接受的,並且是第一次傳送,或者和上次傳送的Broker相同,則將Queue返回。
int index = tpInfo.getSendWhichQueue().incrementAndGet();
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0)
pos = 0;
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
if (latencyFaultTolerance.isAvailable(mq.getBrokerName()))
return mq;
}
//第二步:如果第一步沒有選中一個Broker,則選擇一個延遲較低的Broker。
final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
final MessageQueue mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().incrementAndGet() % writeQueueNums);
}
return mq;
} else {
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
return tpInfo.selectOneMessageQueue();
}
//第三步:如果第一、二步都沒有選中一個Broker,則隨機選擇一個Broker
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
tpInfo.selectOneMessageQueue(lastBrokerName) 該方法的功能就是隨機選擇一個Broker
public MessageQueue selectOneMessageQueue(final String lastBrokerName)
{
//第一步 如果沒有上次使用的Broker作為參考,那麼隨機選擇一個Broker。
if (lastBrokerName == null) {
return selectOneMessageQueue ();
} else {//第二步 如果存在上次使用的Broker,就選擇非上次使用的 Broker,目的是均勻地分散Broker的壓力
int index = this.sendwhichQueue.getAndIncrement();
for (int i = 0;i < this.messageQueueList.size(); i++){
int pos = Math.abs(index++) this.messageQueueList.size();
if (pos <0)
pos =0;
MessageQueue mg = this.messageQueueList.get(pos);
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
//第三步 如果第一、二步都沒有選中一個Broker,則採用兜底方案——隨機選擇一個Broker
return selectOneMessageQueue();
}
}
Broker 端保證
資料同步方式保證:在後面 Broker章節中會講到 Broker主從複製分為兩種:同步複製和非同步複製。同步複製是指訊息傳送到MasterBroker後,同步到Slave Broker才算傳送成功;非同步複製是指訊息傳送到Master Broker,即為傳送成功。在生產環境中,建議至少部署2個Master和2個Slave,下面分為幾種情況詳細描述。
- 1個Slave掉電。Broker同步複製時,生產第一次傳送失敗,重試到另一組Broker後成功;Broker非同步複製時,生產正常不受影響。
- 2個Slave掉電。Broker同步複製時,生產失敗;Broker非同步複製時,生產正常不受影響。
- 1個Master掉電。Broker 同步複製時,生產第一次失敗,重試到另一組 Broker後成功;Broker非同步複製時的做法與同步複製相同。
- 2個Master掉電。全部生產失敗。
- 同一組Master和Slave掉電。Broker同步複製時,生產第一次傳送失敗,重試到另一組Broker後成功;Broker非同步複製時,生產正常不受影響。
- 2組機器都掉電:全部生產失敗。
綜上所述,想要做到絕對的高可靠,將 Broker 配置的主從同步進行複製即可,只要生產者收到訊息儲存成功的反饋,訊息就肯定不會丟失。一般適用於金融領域的特殊場景。絕大部分場景都可以配置Broker主從非同步複製,這樣效率極高。