從notify和metaq初窺消息隊列的前世今生

上一家公司並沒有用消息隊列中間件,大學裏對ActiveMQ有所瞭解但並未深入研究,RocketMQKafka也一直囿於聽聞。不過在原來的項目中經常會用Redis的list實現一個分佈式的阻塞隊列。在短信服務調用中,會將服務的同步調用改爲異步任務存入隊列表批量調度以提高吞吐量,並降低大促期間大批量短信同步調用帶來的壓力。我覺得後面這一點也非常接近消息隊列所要實現的需求。然後受益於隆基前輩的阿里消息中間件架構演進之路:notify和metaq一文,用這篇小筆記總結下我對消息隊列的認識和理解。

1、爲什麼用消息隊列

1.1、解耦

比如對於一個分佈式的電商網站,訂單系統需要接入支付系統的接口進行支付。後面網站又拓展了優惠券模塊需要接入訂單系統。

訂單系統接入優惠券系統

隨着需求和業務的發展,後面加了短信模塊發送短信通知,積分模塊給用戶送積分。可能覺得發短信成本有點高,又替換成了App上的消息通知等。

訂單系統

如果用傳統的RPC接口同步調用方式,可能是這樣的:

void createTrade(...) {
  doCreateTrade(args);                    // 訂單系統自己進行下單操作
  payService.payTrade(tid);               // 付款系統的接口進行扣款操作
  couponService.applyCoupon(couponId);    // 優惠券系統將優惠券使用掉
  smsService.sendSms(content);            // 短信系統發送短信
  pointService.givePoint();               // 積分系統贈送積分
  appPushService.sendPush();              // 應用推送系統發送推送消息
}

任何一個業務需求的添加或改動都需要修改訂單系統的代碼,這對於維護訂單系統的同學來說簡直是噩夢。

如果中間有消息隊列,那訂單系統只需要關係核心的下單付款流程,然後發一個下單消息到消息隊列中,其他系統愛幹嘛幹嘛去。如果網站有新模塊添加進來需要用到下單消息,只需從消息隊列訂閱,訂單系統不用修改代碼。訂單系統的代碼也變得簡潔多了:

void createTrade(...) {
  doCreateTrade(args);                    // 訂單系統自己進行下單操作
  payService.payTrade(tid);               // 付款系統的接口進行扣款操作
  mq.sendMessage(msg);
}

消息隊列

1.2、異步

原來的訂單系統會調用多個外部服務,每個服務的調用都會消耗一定的時間,隨着接入的系統增多,整個下單流程的時間肯定會線性增長。這對於電商這樣的互聯網應用來說肯定是不能容忍的——用戶的耐心是有限的,下個單loading半天鬼才用你家軟件呢。

調用時常

而如果使用消息隊列,優惠券、積分、短信等外部系統可以異步處理消息,可以明顯降低調用時長。

下單時長

1.3、削峯

雙十一這種大促對於所有涉及到電商業務的網站都是大考,我上一家公司是做isv業務的,也不例外。之前的項目主要是爲淘寶店鋪發營銷短信,在雙十一前一兩個禮拜開始就有大量的店鋪發各種促銷短信,那幾天的短信量有時甚至可以頂得上平常大半年的短信量。而系統當時使用的策略是將營銷模塊提交的一條條短信記錄存在短信網關的一張隊列表裏,短信網關調度任務會批量從隊列表拿出短信,處理後再批量地發送給下游的網關通道商——這種方式也確實達到了削峯填谷的作用。

聽之前同事說,項目最早的使用的是同步的RPC調用,短信網關只是對多個下游網關通道商的抽象。後來由於明顯的性能瓶頸才改爲了異步批量發送的方式。

雙十一當晚秒殺對於阿里本身的電商系統也是一場大考驗,爲了保證訂單、付款等系統不被泄洪般的流量沖垮。需要在系統前“造一個蓄水池”,系統再根據自身性能不緊不慢地處理掉“蓄水池”中的數據——細水長流才能可持續發展。

消息隊列

由於新的鏈路上消息隊列和數據庫一樣都扮演着重要角色,這也意味着消息中間件也必須保證高可用。

2、消息怎麼存儲

消息被暫存到中間的消息代理(Message broker)上,消息具體需要如何存儲是首要解決的問題。

消息隊列

2.1、數據庫

就像我之前項目一樣,在數據庫中創建一張隊列表存儲——也就是所謂“工作隊列”,其實就是將任務插入到數據表中,通常需要任務調度進行處理,然後將其標記爲已完成。

Apache下的ActiveMQ就可以選用JDBC方式來做消息持久化(ActiveMQ默認是使用內建的kahadb)。從源代碼中可以看出ActiveMQ會創建一張ACTIVEMQ_MSGS的表:

CREATE TABLE ACTIVEMQ_MSGS(
  ID BIGINT NOT NULL, 
  CONTAINER VARCHAR(250) NOT NULL, 
  MSGID_PROD VARCHAR(250), 
  MSGID_SEQ BIGINT, 
  EXPIRATION BIGINT, 
  MSG BLOB, 
  PRIMARY KEY (ID) 
)

ActiveMQ

但使用隊列表經常會引起性能問題,主要有三個原因:

  1. 輪詢。通常系統都有一個或多個Worker進程來檢查隊列表中是否有消息需要處理。在負載很大的應用程序中,這很容易造成問題。

    解決這個問題的方法要麼寄希望於數據庫的通知功能,可惜MySQL沒有。要麼使用長輪詢降低查詢頻率,這也意味着實時性有所下降,需要根據業務要求對輪詢頻率進行權衡。

  2. 。輪詢後的消息具體處理邏輯通常是這樣的:運行select for update查出需要處理的任務,然後運行update標記成“執行中”,最後處理完了再標記成“已完成”。當然不一定是使用select for update,但是爲了保證消息只被一個工作線程處理,肯定會使用其他同步技術。

    上一個項目裏,worker進程因爲是分用戶部署的,一個worker處理某幾個分庫的用戶數據,所以不涉及到多臺機器爭搶一條數據的問題。只需要在內存中ConcurrentHashMap存儲當前正在running的任務,之後輪詢出來的任務需要先reject掉running的任務,再提交到線程池執行。當然這種策略的問題在於如果任務集中在一個Worker上,會導致負載不均衡。

  3. 數據增長。隨着時間流逝,隊列表可能會堆積數百萬甚至幾億的數據。而真正需要處理的消息可能只有幾百幾千條。

    避免這個問題也很簡單,將處理完的消息刪除掉,如有需要可以轉儲到另一個表裏。這樣就能保證隊列表保持相對合適的數據量。但是面對有大量消息堆積的應用,比如雙十一期間的電商網站,上億的數據量存儲在數據庫中肯定對性能有所影響,因爲數據增長的特別大時,B+樹層級會越深,從而導致I/O次數增加。

2.2、分佈式KV存儲

之前也在用Redis做消息隊列,主要是使用了list的阻塞功能,以實現消息的實時處理:比如用戶的物流信息有變化會立即lpush寫入隊列,後臺會使用brpop的阻塞方式取出消息交給Worker線程處理,這樣避免了高頻率輪詢數據庫導致壓力,也避免了長輪詢導致消息處理不夠及時。

ActiveMQ中還支持LevelDB的持久化方式LevelDB是谷歌開源的一款支持持久化的KV存儲引擎,它使用一種append-only的索引結構LSM-Tree(基於日誌結構的合併樹)來存儲數據。由於先寫入內存的MemTable,然後由Merge線程順序寫入到磁盤中的SSTable中,所以寫入性能會比傳統的關係型數據庫好很多。但是因爲LSM-Tree索引結構會分成多個分段,所以讀取性能會有所犧牲。而經常有消息消費失敗後面臨重試的情況,讀取性能的降低必定會對系統性能有所影響。

2.3、基於文件順序寫的日誌結構

目前業界較爲常用的幾款產品(RocketMQ/Kafka/RabbitMQ)均採用消息刷盤至文件系統來做持久化。

人們普遍認爲“磁盤速度很慢”,但有研究指出順序的磁盤I/O有時甚至快於隨機內存訪問速度。

順序I/O

也正因此,現代操作系統提供了預讀和延遲寫入的技術,所謂“預讀”就是以磁盤塊(新硬盤一個扇區4KB)爲單位讀取,“延遲寫”就是將小單元的寫入聚集成塊寫入磁盤。這其實就和Java中BufferedInputStreamBufferedOutputStream預留8KB緩存原理一樣。

現代操作系統越來越多地用到內存做磁盤數據的緩存,所以Java1.4出現的NIO技術也將原本在JVM層面的緩存轉移到了操作系統層面——也就是MappedByteBuffer相關類。這主要因爲構建在JVM上的緩存有幾個缺點:

1、Java對象的內存開銷更高。

2、堆數據的增加,Java GC觸發的頻率也會更高。

而使用操作系統緩存不僅解決了這些問題,還減少了內核緩存到應用層緩存的拷貝過程。此外重啓應用,緩存仍然有效,而進程內的緩存重啓後需要重建。這也簡化了應用維護緩存的代碼複雜度,因爲所有用於維護緩存和文件系統之間一致性的邏輯現在都在操作系統中。

日誌由於它append-only的特點,所以它的寫入操作都是順序的。

時至今日,已不知日誌概念起源於何時了,因爲它太簡單了。但縱觀日誌的使用,從保證數據庫的ACID到數據庫間的數據複製,日誌結構一直都扮演着重要角色。

起初使用ActiveMQ的Linkedin公司也正是基於日誌的順序寫的思想開發出了以高吞吐量著稱的Kafka

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-K2LdtNCt-1587995268777)(https://kafka.apache.org/images/logo.png)]

3、集團的消息中間件——notify和metaq

看了隆基前輩的阿里消息中間件架構演進之路:notify和metaq這篇文章,讓我對集團這兩個核心的中間件有了初步認識。

notify可以對標ActiveMQ。最早notify也確實是參考ActiveMQ的kaha實現的kv存儲,到後來爲了保證可靠性使用關係型數據庫Oracle存儲消息,隨着集團的去IOE化戰略,才全面遷移到MySQL集羣存儲。notify的Message模型設計如下:

Notify模型設計

而MetaQ則是受Kafka的影響開發的,最早第一版的名字"metamorphosis",是是奧地利作家卡夫卡的名作——變形記,這算是對Kafka的致敬吧。

但MetaQ在設計上和Kafka略有不同。Kafka對於每個Topic都會維護一個分區日誌

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sc8Wf05o-1587995268778)(https://kafka.apache.org/25/images/log_anatomy.png)]

一旦topic數量增多,broker的分區文件數也會隨着增大,本來高性能的順序寫文件會變成隨機寫,吞吐量會有較大的下降。

所以MetaQ2.0時就對存儲層進行了重新設計,結構如下圖所示:

MetaQ存儲的核心是實現一個持久化的分佈式隊列,重新設計後的MetaQ抽象出了CommitLog和Consume queue。其中CommitLog屬於物理隊列,存儲完整的消息數據,不定長記錄,也起到了類似redo log的功能,一旦CommitLog落盤成功,消息就不會丟失;所有Topic的消息都會寫入到同一個CommitLog,哪怕單機一萬個topic,還是能保持順序寫,保障吞吐量。Consume queue可以認爲是邏輯隊列、索引隊列,每個topic的消息在寫完CommitLog之後,都會寫到獨立的Consume queue;隊列裏的每個元素爲定長記錄,元素內容包含該消息在對應CommitLog的offsetsize。基於這樣的存儲結構,MetaQ對客戶端暴露的主要是Consume queue邏輯視圖,提供隊列訪問接口。消費者通過指定Consume queue的位點來讀取消息,通過提交Consume queue的位點來維護消費進度。

也許你會問,Consumer queue也是隨機I/O,爲什麼相比Kafka就能支持更多的Topic呢?原因就是利用了操作系統文件緩存(MappedByteBuffer)。因爲Consumer queue只存儲消息的offsetsize等信息,數據量比較小,所以Consumer queue的讀寫基本都能命中緩存。雖然是隨機I/O但效率上比Kafka更高。

根據阿里雲團隊的壓測結果可以看出,在Topic較多的情況下,Kafka的性能有大幅下降,而RocketMQ相對受影響較小。

Kafka與RocketMQ在多Topic的壓測結果

參考鏈接:

MQ是什麼:https://www.zhihu.com/question/54152397/answer/657234090

5 subtle ways you’re using mysql as a queue and why it’ll bite you:https://www.xaprb.com/blog/2011/09/15/5-subtle-ways-youre-using-mysql-as-a-queue-and-why-itll-bite-you/

The Log: What every software engineer should know about real-time data’s unifying abstraction:https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying

一個簡單的 MySQL 批量事務消息隊列:https://zhuanlan.zhihu.com/p/130494984

Kafka介紹:https://kafka.apache.org/intro

Kafka的持久化:https://kafka.apache.org/documentation/#persistence

阿里消息中間件架構演進之路:notify和metaq:https://www.atatech.org/articles/95456

RocketMQ消息存儲:https://zhuanlan.zhihu.com/p/92125985

RocketMQ vs. ActiveMQ vs. Kafka:http://rocketmq.apache.org/docs/motivation/#rocketmq-vs-activemq-vs-kafka

Notify和Metaq ——學習總結:https://www.atatech.org/articles/36160

Kafka vs. Apache RocketMQ™- Multiple Topic Stress Test Results:https://www.alibabacloud.com/blog/kafka-vs-rocketmq–multiple-topic-stress-test-results_69781

JMS vs. AMQP:http://www.mastertheboss.com/jboss-server/jboss-jms/comparing-messaging-standards-jms-vs-amqp

消息隊列設計精要:https://tech.meituan.com/2016/07/01/mq-design.html

消息隊列RabbitMQ自研之路:https://www.atatech.org/articles/160175

MetaQ再學習-消息存儲:https://www.atatech.org/articles/114508

談談 RocketMQ 消息存儲的設計與實現:https://www.atatech.org/articles/145197

rocketmq詳解之消息存儲機制:https://www.atatech.org/articles/129514

比較RocketMQ與Kafka的消息存儲方式及RocketMQ源碼分析:https://www.atatech.org/articles/127436

其他MQ產品:

https://github.com/akullpp/awesome-java#messaging

HornetQ:https://hornetq.jboss.org/

Pulsar:https://pulsar.apache.org/

ZeroMQ:https://zeromq.org/

TubeMQ:https://tubemq.apache.org/en-us/

qmq:https://github.com/qunarcorp/qmq

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章