消息中間件的作用就是用來異步化并發能力的一個載體,不僅如此,它仍然需要在架構上保證很多能力,高可用,高并發,可擴展,可靠性,完整性,保證順序等,光是這些都已經讓各種設計者比較頭疼了; 更有一些變態的需求,例如慢消費,不可重復等需要花的設計代價是相當高的,所以不要盲目的迷信開源大牛,對于很多機制,幾乎都要重建;建立一個符合所有業務,好用,通用的私有云,沒那么簡單。
如果說一個支付系統每天要處理億級業務單的話,那么消息中間件的處理能力至少得達到近百億,因為很多系統都是依賴于中間件的集群能力,并且要保證不能出錯,so,讓我們從架構的一些層面上來一點點來分析中間件是怎么做到的?
高可用 ( High Availability )
高可用是一個永恒的話題,這個也是在金融界是否靠譜的一個衡量標準,要知道,金融界的架構師們會想方設法的讓數據不會丟失,哪怕是一條數據,但是事實上,這個東西從理論上來講,得靠人品。。。這個不是忽悠。
舉一個例子來說,互聯網數據架構中一份數據至少要存三份才叫高保證,但是事實上,谷歌的比利時數據中心在8.13日遭到雷劈后數據中心永久丟失0.000001%,不到0.05%的磁盤未能修復,這里要說的是,天時地利人和很重要,極限條件下沒有什么不可能,一定會有架構漏洞,下面看一下mq高可用的一般做法:
下圖是activemq的 HA 方案:
activemq的HA 通過master/slave的failover進行托管,其中主從切換可以通過多種方式進行切換:
1:通過一個nfs或其它共享磁盤設備進行一個共享鎖,通過對共享文件鎖的占有,來標記master的狀態,當m掛掉以后,對應的slave會占有shared_lock而轉換為master
2:通過zookeeper進行集群的管理,比較常見,這里不再介紹
下圖是metaq的HA方案
如上圖,如出一轍,也是通過zk管理broker的主從結點。
當然這個只是其中的一個failover機制,只能保證消息在broker掛掉時轉換到slave上,但是不能保證在這中間過程中的消息的丟失
當消息從broker流經時,很有可能因為宕機或是其它硬件故障而導致后,就有可能導致消息丟失掉,這個時候,就需要有相關的存儲介質對消息的進行一個保障了
那么我們舉kafka的存儲機制作為一個參考,要知道消息中間件對存儲的依賴不但要求速度快,并且要求IO的需求成本非常低,kafka自己設計了一套存儲機制來滿足上述的需求,這里簡單介紹一下。
首先kafka中的topic在分布式部署下分做多個分區,分區的就相當于消息進行了一個負載,然后由多臺機器進行路由,舉個例子: 一個topic,debit_account_msg會切分為 debit_account_msg_0, debit_account_msg_1, debit_account_msg_2。。。等N個partition,每個partition會在本地生成一個目錄比如/debit_account_msg/topic
里面的文件會分出很多segment,每個segment會定義一個大小,比如500mb一個segment,一個file分為index和log二個部分
00000000000000000.index
00000000000000000.log
00000000000065535.index
00000000000065535.log
其中數字代表msgId的值的索引起點,對應的數據結構如下圖:
1,0代表msgId為1的消息,0代表在這個文件中的偏移量,讀取到這個文件后再尋找到查詢到對應的segment log文件讀取對應的msg信息,對應的信息是一個固定格式消息體:
顯然,這種機制單純應用肯定是不能滿足高并發IO的,首先二分查找segmentfile,然后再通過offset找到對應數據,再讀取msgsize,再讀取報體,至少是4次磁盤io,開銷較大,但是在拉取時候是使用的順序讀取,基本上影響不大。
除了要上面所說的查詢外。其實在寫入磁盤之前都是在os上的pagecache上進行讀寫的,然后通過異步線程對硬盤進行定時的flush(LRU策略),但其實這個風險很大的,因為一旦os宕掉,會導致數據的丟失,尤其是在進行慢消費多積壓很多數據的情況下,但是kafka他弟metaq對這塊已經做了很多改造,對這些分區文件進行了replication機制(阿里內部使用),所以在這個層面上再怎么遭雷劈丟消息的機率就會比較小了,當然也不排除主機房光纜被人挖掉會有什么樣的情況發生。
說了這么多,似乎看起來的比較完美和美好,但是實際上運維成本似乎很大。因為這些都是文件,一旦發生問題,需要人工去處理起來相當麻煩,而且是在一臺一臺機器上,需要比較大的運維成本去做一些運維規范以及api調用設施等。
所以,在這塊我們可以通過改造,將數據存儲在一些nosql上,比如mongoDB上,當然mysql也是可以,但是io能力和nosqldb完全不在一個水平線上,除非我們有強烈的事務處理機制,而在金融里的確對這塊要求比較相當嚴謹。像在支付寶后面就使用了metaq,因為之前的中間件tbnotify在處理慢消費的情況下會很被動,而metaq在這塊會有極大的優勢,為什么,請聽后面分解。
高并發
最開始大家使用mq很大部分工程師都用于解決性能
和異步化
的問題,其實對于同一個點來說,一個io調度
其實并不是那么耗資源,廢話少說讓我們看下mq里的一些高并發點,首先在這里先介紹一下幾個比較有名的中間件背景:
activemq當時就是專門的企業級解決方案,遵守jee里的jms規范,其實性能也還是不錯的,但是拉到互聯網里就是兔子抱西瓜,無能為力了
rabbitmq采用erlang語言編寫,遵守AMQP協議規范,更具有跨平臺性質,模式傳遞模式要更豐富,并且在分布式
rocketmq(metaq3.0現今最新版本, kafka也是metaq的前身,最開始是linkedIn開源出來的日志消息系統 ),metaq基本上把kafka的原理和機制用java寫了一遍,經過多次改造,支持事務,發展速度很快,并且在阿里和國內有很比較好的社區去做這塊的維護 。
性能比較,這里從網上找一些數據,僅供參考:
說實話來講,這些數據級別來講,相差沒有太離譜,但是我們可以通過分析一些共性來講,這些主要性能差別在哪里?
rocketmq是metaq的后繼者,除了在一些新特性和機制方面有改進外,性能方面的原理都差不多,下面說下這些高性能的一些亮點:
rocketmq的消費主要采用pull機制,所以對于broker來講,很多消費的特性都不需要在broker上實現,只需要通過consumer來拉取相關的數據即可,而像activemq,rabbitmq都是采取比較老的方式讓broker去dispatch消息,當然些也是jms或amqp的一些標準投遞方式
文件存儲是順序存儲的,所以來拉消息的時候只需要通過調用segment的數據就可以了,并且consumer在做消費的時候是最大程度的去消費信息,不太可能產生積壓,而且可以通過設置io調度算法,像noop模式,可以提高一些順序讀取的性能 。
通過pagecache
去命中在os緩存中的數據達到一個熱消費.
metaq的批量磁盤IO以及網絡IO,盡量讓數據在一次io中運轉,消息起來都是批量的,這樣對io的調度不太需要消耗太多資源
NIO傳輸,如下圖,這個是最初metaq的一個架構,最初metaq使用的是taobao內部的gecko和notify-remoting集成的一些高性能的NIO框架去分發消息:
消費隊列的輕量化,要知道我們的消息能力是通過隊列來獲取的
看下面的圖:
metaq在消費的物理隊列上添加了邏輯隊列,隊列對應的磁盤數據是串行化的,隊列的添加不會添加磁盤的 iowait
負擔,寫入可以順序,但是在讀取的時候仍然需要去用隨機讀,首先是邏輯隊列 ,然后再讀取磁盤,所以 pagecache
很重要,盡量讓內存大一些,這塊分配就會充分得到利用。
其實做到上面這些已經基本上能保證我們的性能在一個比較高的水平; 但是有時候性能并不是最重要的,最重要的是要和其它的架構特性做一個最佳的平衡,畢竟還有其它的機制要滿足。因為在業界基本上最難搞定的三個問題:高并發,高可用,一致性是互相沖突的。
可擴展
這是一個老生常談的問題,對于一般系統或是中間件,可以較好的擴展,但是在消息中間件這塊,一直是一個麻煩事,為什么?
先說下activemq的擴展起來的局限性,因為activemq的擴展需要業務性質,作為broker首先要知道來源和目的地,但是這些消息如果都是分布式傳輸的話,就會變的復雜,下面看一下activemq的負載是怎么玩轉的:
我們假設producer去發topicA的消息,如果正常情況下所有的consumer都連到每一個broker上的,辣么假如broker上有producer上的消息過來,是可以transfer到對應的consumer上的。
但是如果像圖中 broker2中如果沒有對應的消息者連接到上面,這種情況下怎么辦呢?因為假設同一個topic的應用系統(producer)和依賴系統 (consumer)節點很多,那又該如何擴容呢?activemq是可以做上圖中正常部分,但是需要改變producer,broker,consumer的對應的配置,相當麻煩。
當然activemq也可以通過multicast的方式來做動態的查找(也有人提到用lvs或f5做負載,但是對于consumer一樣存在較大的問題,而且這種負載配置對于topic的分發,沒實質性作用),但是,仍然會有我說的這個問題,如果topic太大,每個broker都需要連接所有的producer或是consumer, 不然就會出現我說的情況,擴容這方面activemq是相當的麻煩
下面來說一下metaq是如何做這塊事情的,看圖說話 :
metaq上是以topic為分區的,在這個層面來講,我們只要配置topic的分區有多少個就好了,這樣切片起來就是有個'業務'概念作為路由規則;一般一個broker機器上配置有多個topic,每個topic在一個機器上一般是只有一個分區,假如機器不夠了,也是可以支持多個分區的,一般來說,我們可以通過業務id來取模自定義分區,通過獲取發區參數即可。
可靠性
可靠性是消息中間件的重要特性,看下mq是怎么流轉這些消息的,拿activemq來先來做下參考,它是基于push&push機制。
如何保證每次的消息發送都被消費到?Activemq的生產者發送消息后都需要收到一條broker的ack才會確認消收到,同樣對于broker到consumer也是同樣的保障。
Metaq的機制也是同樣的,但是broker到consumer是通過pull的方式,所以它的到達保障要看consumer的能力如何,但是一般情況下,應用服務器集群不太可能出現雪崩效應。
如何保證消息的冪等性?目前來說基本上activemq,metaq都不能保證消息的冪等性,這就需要一些業務來保證了。因為一旦broker超時,就會重試,重試的話都會產生新的消息,有可能broker已經落地消息了,所以這種情況下沒法保證同一筆業務流水產生二條消息出來
消息的可靠性如何保證?這點上activemq和metaq基本上機制一樣:
生產者保證:生產數據后到broker后必須要持久化才能返回ACK
broker保證:metaq服務器接收到消息后,通過定時刷新到硬盤上,然后這些數據都是通過同步/異步復制到slave上,來保證宕機后也不會影響消費.
activemq也是通過數據庫或是文件存儲在本地,做本地的恢復
消費者保證:消息的消費者是一條接著一條地消費消息,只有在成功消費一條消息后才會接著消費下一條。如果在消費某條消息失敗(如異常),則會嘗試重試消費這條消息(默認最大5次),超過最大次數后仍然無法消費,則將消息存儲在消費者的本地磁盤,由后臺線程繼續做重試。而主線程繼續往后走,消費后續的消息。因此,只有在MessageListener確認成功消費一條消息后,meta的消費者才會繼續消費另一條消息。由此來保證消息的可靠消費。
一致性
mq的一致性我們討論二個場景:
1:保證消息不會被多次發送/消費
2:保證事務
剛才上面介紹的一些mq都是不能保證一致性的,為什么不去保證?代價比較大,只能說,這些都是可以通過改造源碼來進行保證的,而且方案比較相對來說不是太復雜,但是額外的開銷比較大,比如通過額外的緩存集群來保證某段時間的不重復性,相信后面應該會有一些mq帶上這個功能。
Activemq支持二種事務,一個是JMS transaction,一個是XA分布式事務,如果帶上事務的話,在交互時會生成一個transactionId去到broker,broker實現一些TM去分配事務處理,metaq也支持本地事務和XA,遵守JTA標準這里activemq和metaq的事務保證都是通過redo日志方式來完成的,基本上一致。
這里的分布式事務只在broker階段后保證,在broker提交之前會把prepare的消息存儲在本地文件中,到commit階段才將消息寫入隊列,最后通過TM實現二階段提交。
Kotlin 開發者社區
國內第一Kotlin 開發者社區公眾號,主要分享、交流 Kotlin 編程語言、Spring Boot、Android、React.js/Node.js、函數式編程、編程思想等相關主題。
越是喧囂的世界,越需要寧靜的思考。