1)消息隊列概述
消息隊列中間件是分布式系統中重要的組件,主要解決應用耦合、異步消息、流量削鋒等問題。實現高性能、高可用、可伸縮和最終一致性架構。是大型分布式系統不可缺少的中間件。
目前在生產環境,使用較多的消息隊列有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等。
2)消息隊列應用場景
下面詳細介紹一下消息隊列在實際應用中常用的使用場景。場景分為異步調用、系統解耦、流量削鋒和消息通訊四個場景。
(1)異步調用
假設你有一個系統調用鏈路,是系統A調用系統B,一般耗時20ms;系統B調用系統C,一般耗時200ms;系統C調用系統D,一般耗時2s,如下圖所示。
現在最大的問題就是:
用戶一個請求過來巨慢無比,因為走完一個鏈路,需要耗費20ms + 200ms + 2000ms(2s) = 2220ms,也就是2秒多的時間。但是實際上,鏈路中的系統A調用系統B,系統B調用系統C,這兩個步驟起來也就220ms。就因為引入了系統C調用系統D這個步驟,導致最終鏈路執行時間是2秒多,直接將鏈路調用性能降低了10倍,這就是導致鏈路執行過慢的罪魁禍首。
那此時我們可以思考一下,是不是可以將系統D從鏈路中抽離出去做成異步調用呢?
其實很多的業務場景是可以允許異步調用的。
舉個例子,你平時點個外賣,咔嚓一下子下訂單然后付款了,此時賬戶扣款、創建訂單、通知商家給你準備菜品。接著,是不是需要找個騎手給你送餐?那這個找騎手的過程,是需要一套復雜算法來實現調度的,比較耗時。但是其實稍微晚個幾十秒完成騎手的調度都是ok的,因為實際并不需要在你支付的一瞬間立馬給你找好騎手,也沒那個必要。
那么我們是不是就可以把找騎手給你送餐的這個步驟從鏈路中抽離出去,做成異步化的,哪怕延遲個幾十秒,但是只要在一定時間范圍內給你找到一個騎手去送餐就可以了。
這樣是不是就可以讓你下訂單點外賣的速度變得超快?支付成功之后,直接創建好訂單、賬戶扣款、通知商家立馬給你準備做菜就ok了,這個過程可能就幾百毫秒。然后后臺異步化的耗費可能幾十秒通過調度算法給你找到一個騎手去送餐,但是這個步驟不影響我們快速下訂單。
當然我們不是說那些大家熟悉的外賣平臺的技術架構就一定是這么實現的,只不過是用一個生活中常見的例子給大家舉例說明而已。所以上面的鏈路也是同理,如果業務流程支持異步化的話,是不是就可以考慮把系統C對系統D的調用抽離出去做成異步化的,不要放在鏈路中同步依次調用。這樣,實現思路就是系統A -> 系統B -> 系統C,直接就耗費220ms后直接成功了。然后系統C就是發送個消息到MQ中間件里,由系統D消費到消息之后慢慢的異步來執行這個耗時2s的業務處理。通過這種方式直接將核心鏈路的執行性能提升了10倍。整個過程,如下圖所示。
場景說明:用戶注冊后,需要發送注冊郵件和發送注冊信息,傳統的做法有兩種:串行方式、并行方式
串行方式
將注冊信息寫入數據庫成功后,發送注冊郵件,然后發送注冊短信,而所有任務執行完成后,返回信息給客戶端。
并行方式
將注冊信息寫入數據庫成功后,同時進行發送注冊郵件和發送注冊短信的操作。而所有任務執行完成后,返回信息給客戶端。同串行方式相比,并行方式可以提高執行效率,減少執行時間。
由上可以看出,傳統串行和并行的方式會受到系統性能的局限,那么如何解決這個問題?我們需要引入消息隊列,將不是必須的業務邏輯,異步進行處理,由此改造出來的流程如下:
根據上述的流程,用戶的響應時間基本相當于將用戶數據寫入數據庫的時間,發送注冊郵件、發送注冊短信的消息在寫入消息隊列后,即可返回執行結果,寫入消息隊列的時間很快,幾乎可以忽略。
(2)系統解耦
假設你有個系統A,這個系統A會產出一個核心數據,現在下游有系統B和系統C需要這個數據。那簡單,系統A就是直接調用系統B和系統C的接口發送數據給他們就好了。整個過程,如下圖所示。
但是現在要是來了系統D、系統E、系統F、系統G,等等,十來個其他系統慢慢的都需要這份核心數據呢?如下圖所示。
大家可別以為這是開玩笑,一個大規模系統,往往會拆分為幾十個甚至上百個子系統,每個子系統又對應N多個服務,這些系統與系統之間有著錯綜復雜的關系網絡。如果某個系統產出一份核心數據,可能下游無數的其他系統都需要這份數據來實現各種業務邏輯。此時如果你要是采取上面那種模式來設計系統架構,那么絕對你負責系統A的同學要被煩死了。
先是來一個人找他要求發送數據給一個新的系統H,系統A的同學要修改代碼然后在那個代碼里加入調用新系統H的流程。一會那個系統B是個陳舊老系統要下線了,告訴系統A的同學:別給我發送數據了,接著系統A再次修改代碼不再給這個系統B。
然后如果要是某個下游系統突然宕機了呢?
系統A的調用代碼里是不是會拋異常?那系統A的同學會收到報警說異常了,結果他還要去care是下游哪個系統宕機了。
所以在實際的系統架構設計中,如果全部采取這種系統耦合的方式,在某些場景下絕對是不合適的,系統耦合度太嚴重。并且互相耦合起來并不是核心鏈路的調用,而是一些非核心的場景(比如上述的數據消費)導致了系統耦合,這樣會嚴重的影響上下游系統的開發和維護效率。
因此在上述系統架構中,就可以采用MQ中間件來實現系統解耦。
系統A就把自己的一份核心數據發到MQ里,下游哪個系統感興趣自己去消費即可,不需要了就取消數據的消費,如下圖所示。
場景說明:用戶下單后,訂單系統需要通知庫存系統。
傳統的做法為:訂單系統調用庫存系統的接口。如下圖所示:
傳統方式:調用庫存接口
傳統方式具有如下缺點:
-1. 假設庫存系統訪問失敗,則訂單減少庫存失敗,導致訂單創建失敗
-2. 訂單系統同庫存系統過度耦合
如何解決上述的缺點呢?需要引入消息隊列,引入消息隊列后的架構如下圖所示:
訂單系統:用戶下單后,訂單系統進行數據持久化處理,然后將消息寫入消息隊列,返回訂單創建成功
庫存系統:使用拉/推的方式,獲取下單信息,庫存系統根據訂單信息,進行庫存操作。
假如在下單時庫存系統不能正常使用。也不影響正常下單,因為下單后,訂單系統寫入消息隊列就不再關心其后續操作了。由此實現了訂單系統與庫存系統的應用解耦。
(3)削峰填谷
假設你有一個系統,平時正常的時候每秒可能就幾百個請求,系統部署在8核16G的機器的上,正常處理都是ok的,每秒幾百請求是可以輕松抗住的。但是如下圖所示,在高峰期一下子來了每秒鐘幾千請求,瞬時出現了流量高峰,此時你的選擇是要搞10臺機器,抗住每秒幾千請求的瞬時高峰嗎?
那如果瞬時高峰每天就那么半個小時,接著直接就降低為了每秒就幾百請求,如果你線上部署了很多臺機器,那么每臺機器就處理每秒幾十個請求就可以了,這不是有點浪費機器資源嗎?大部分時候,每秒幾百請求,一臺機器就足夠了,但是為了抗那每天瞬時的高峰,硬是部署了10臺機器,每天就那半個小時有用,別的時候都是浪費資源的。
但是如果你就部署一臺機器,那會導致瞬時高峰時,一下子壓垮你的系統,因為絕對無法抗住每秒幾千的請求高峰。此時我們就可以用MQ中間件來進行流量削峰。所有機器前面部署一層MQ,平時每秒幾百請求大家都可以輕松接收消息。
一旦到了瞬時高峰期,一下涌入每秒幾千的請求,就可以積壓在MQ里面,然后那一臺機器慢慢的處理和消費。等高峰期過了,再消費一段時間,MQ里積壓的數據就消費完畢了。
這個就是很典型的一個MQ的用法,用有限的機器資源承載高并發請求,如果業務場景允許異步削峰,高峰期積壓一些請求在MQ里,然后高峰期過了,后臺系統在一定時間內消費完畢不再積壓的話,那就很適合用這種技術方案。
流量削鋒也是消息隊列中的常用場景,一般在秒殺或團搶活動中使用廣泛。
應用場景:秒殺活動,一般會因為流量過大,導致流量暴增,應用掛掉。為解決這個問題,一般需要在應用前端加入消息隊列。
????1)可以控制參與活動的人數;
????2)可以緩解短時間內高流量對應用的巨大壓力;
流量削鋒處理方式系統圖如下:
????1)服務器在接收到用戶請求后,首先寫入消息隊列。這時如果消息隊列中消息數量超過最大數量,則直接拒絕用戶請求或返回跳轉到錯誤頁面;
????2)秒殺業務根據秒殺規則讀取消息隊列中的請求信息,進行后續處理。
3)常用消息隊列
本部分內容介紹常用的消息中間件(Active MQ,Rabbit MQ,Zero MQ,Kafka)以及他們的特點。
ActiveMQ
ActiveMQ是Apache出品,最流行的,能力強勁的開源消息總線。ActiveMQ 是一個完全支持JMS1.1和J2EE 1.4規范的 JMS Provider實現,盡管JMS規范出臺已經是很久的事情了,但是JMS在當今的J2EE應用中間仍然扮演著特殊的地位。
ActiveMQ特性如下:
● 多種語言和協議編寫客戶端。
????? 語言: Java,C,C++,C#,Ruby,Perl,Python,PHP。
????? 應用協議:OpenWire,Stomp REST,WSNotification,XMPP,AMQP
● 完全支持JMS1.1和J2EE 1.4規范 (持久化,XA消息,事務)
●?對Spring的支持。
????? ActiveMQ可以很容易內嵌到使用Spring的系統里面,而且也支持Spring2.0的特性
●?通過了常見J2EE服務器(如 Geronimo,JBoss 4,GlassFish,WebLogic)的測試,其中通過JCA 1.5 resource adaptors的配置,可以讓ActiveMQ可以自動的部署到任何兼容J2EE 1.4 商業服務器上
●?支持多種傳送協議:in-VM,TCP,SSL,NIO,UDP,JGroups,JXTA
●?支持通過JDBC和journal提供高速的消息持久化
●?從設計上保證了高性能的集群,客戶端-服務器,點對點
●?支持Ajax
●?支持與Axis的整合
●?可以很容易得調用內嵌JMS provider,進行測試
缺點:
沒法確認ActiveMQ可以支撐互聯網公司的高并發、高負載以及高吞吐的復雜場景,在國內互聯網公司落地極少。而且使用較多的是一些傳統企業,用ActiveMQ做異步調用和系統解耦。
RabbitMQ
RabbitMQ是流行的開源消息隊列系統,用erlang語言開發。RabbitMQ是AMQP(高級消息隊列協議)的標準實現。支持多種客戶端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX,持久化。用于在分布式系統中存儲轉發消息,在易用性、擴展性、高可用性等方面表現不俗。
上圖中有幾個重要概念:
Broker:簡單來說就是消息隊列服務器實體。
Exchange:消息交換機,它指定消息按什么規則,路由到哪個隊列。
Queue:消息隊列載體,每個消息都會被投入到一個或多個隊列。
Binding:綁定,它的作用就是把Exchange和Queue按照路由規則綁定起來。
Routing Key:路由關鍵字,Exchange根據這個關鍵字進行消息投遞。
vhost:虛擬主機,一個broker里可以開設多個vhost,用作不同用戶的權限分離。
producer:消息生產者,就是投遞消息的程序。
consumer:消息消費者,就是接受消息的程序。
channel:消息通道,在客戶端的每個連接里,可建立多個channel,每個channel代表一個會話任務。
消息隊列的使用過程,如下:
客戶端連接到消息隊列服務器,打開一個channel。
客戶端聲明一個exchange,并設置相關屬性。
客戶端聲明一個queue,并設置相關屬性。
客戶端使用routing key,在exchange和queue之間建立好綁定關系。
客戶端投遞消息到exchange。
exchange接收到消息后,就根據消息的key和已經設置的binding,進行消息路由,將消息投遞到一個或多個隊列里。
優點:
(1)可以支撐高并發、高吞吐、性能很高,同時有非常完善便捷的后臺管理界面可以使用;
(2)支持集群化、高可用部署架構、消息高可靠支持,功能較為完善;
(3)國內各大互聯網公司落地燈大規模RabbitMQ集群支撐自身業務的較多;
(4)RabbitMQ的開源社區很活躍,較高頻率的迭代版本,以修復發現的bug以及各種優化;
缺點:
基于erlang語言開發的,導致較為難以分析里面的源碼,也較難進行深層次的源碼定制和改造。
RocketMQ
RocketMQ 是什么?
RocketMQ是阿里開源的消息中間件,它是純Java開發,具有高吞吐量、高可用性、適合大規模分布式系統應用的特點。
?1.是一個隊列模型的消息中間件,具有高性能、高可靠、高實時、分布式特點。
?2.Producer、Consumer隊列都可以分布式。
?3.Producer向一些隊列輪流發送消息,隊列集合稱為 Topic,Consumer 如果做廣播消費,則一個consumer實例消費這個Topic 對應的所有隊列,如果做集群消費,則多個Consumer 實例平均消費這個topic對應的隊列集合。(默認是集群消費)
?4.能夠保證嚴格的消息順序(因為性能原因,不能保證消息不重復,因為總有網絡不可達的情況發生,需業務端保證)。
?5.提供豐富的消息拉取模式
?6.高效的訂閱者水平擴展能力
?7.實時的消息訂閱機制
?8.億級消息堆積能力
?9.較少的依賴
RocketMQ的基本概念:
1.Name Server
它是一個幾乎無狀態節點,可集群部署,節點之間無任何信息同步。(2.X版本之前rocketMQ使用zookeeper做topic路由管理)。Name Server 是專為 RocketMQ設計的輕量級名稱服務,代碼小于1000行,具有簡單、可集群橫吐擴展、無狀態等特點。將要支持的主備自動切換功能會強依賴 Name Server。
2. Broker
Broker部署相對復雜,Broker分為Master與Slave,一個Master可以對應多個Slave,但是一個Slave只能對應一個Master,Master與Slave的對應關系通過指定相同的BrokerName,不同的BrokerId來定義,BrokerId為0表示Master,非0表示Slave。Master也可以部署多個。每個Broker與Name Server 集群中的所有節點建立長連接,定時注冊Topic信息到所有Name Server。
3. Producer
Producer 與Name Server集群中的其中一個節點(隨機選擇,但不同于上一次)建立長連接,定期從Name Server取Topic路由信息,并向提供Topic服務的Master建立長連接,且定時向Master發送心跳。Producer完全無狀態,可集群部署。
4. Consumer
Consumer與Name Server集群中的其中一個節點(隨機選擇,但不同于上一次)建立長連接,定期從Name Server 取Topic路由信息,并向提供Topic服務的Master、Slave建立長連接,且定時向Master、Slave發送心跳。Consumer既可以從Master訂閱消息,也可以從Slave訂閱消息,訂閱規則由Broker配置決定(目前版本沒有找到可配置的地方,可以在原碼里修改)。
5.ProducerGroup
用來表示一個収送消息應用,一個Producer Group下包含多個Producer實例,可以是多臺機器,也可以是一臺機器的多個迕程,或者一個進程的多個Producer對象。一個Producer Group可以發送多個Topic消息,Producer Group作用如下:
????1)標識一類Producer
????2)可以通過運維工具查詢這個發送消息應用下有多個Producer實例
????3)發送分布式事務消息時,如果 Producer中途意外宕機,Broker會主動回調 Producer Group內的任意一臺機器來確認事務狀態。
6.ConsumerGroup
用來表示一個消費消息應用,一個Consumer Group下包含多個Consumer實例,可以是多臺機器,也可以是多個進程,或者是一個進程的多個Consumer對象。一個Consumer Group下的多個Consumer以均攤方式消費消息,如果設置為廣播方式,那么這個 Consumer Group下的每個實例都消費全量數據。
ZeroMQ
號稱史上最快的消息隊列,它實際類似于Socket的一系列接口,他跟Socket的區別是:普通的socket是端到端的(1:1的關系),而ZMQ卻是可以N:M 的關系,人們對BSD套接字的了解較多的是點對點的連接,點對點連接需要顯式地建立連接、銷毀連接、選擇協議(TCP/UDP)和處理錯誤等,而ZMQ屏蔽了這些細節,讓你的網絡編程更為簡單。ZMQ用于node與node間的通信,node可以是主機或者是進程。
引用官方的說法:“ZMQ(以下ZeroMQ簡稱ZMQ)是一個簡單好用的傳輸層,像框架一樣的一個socket library,他使得Socket編程更加簡單、簡潔和性能更高。是一個消息處理隊列庫,可在多個線程、內核和主機盒之間彈性伸縮。ZMQ的明確目標是“成為標準網絡協議棧的一部分,之后進入Linux內核”。現在還未看到它們的成功。但是,它無疑是極具前景的、并且是人們更加需要的“傳統”BSD套接字之上的一層封裝。ZMQ讓編寫高性能網絡應用程序極為簡單和有趣。”
Kafka
Kafka是一種高吞吐量的分布式發布訂閱消息系統,它可以處理消費者規模的網站中的所有動作流數據。 這種動作(網頁瀏覽,搜索和其他用戶的行動)是在現代網絡上的許多社會功能的一個關鍵因素。 這些數據通常是由于吞吐量的要求而通過處理日志和日志聚合來解決。 對于像Hadoop的一樣的日志數據和離線分析系統,但又要求實時處理的限制,這是一個可行的解決方案。Kafka的目的是通過Hadoop的并行加載機制來統一線上和離線的消息處理,也是為了通過集群機來提供實時的消費。
Kafka是一種高吞吐量的分布式發布訂閱消息系統,有如下特性:
(1)通過O(1)的磁盤數據結構提供消息的持久化,這種結構對于即使數以TB的消息存儲也能夠保持長時間的穩定性能。
(2)高吞吐量:即使是非常普通的硬件Kafka也可以支持每秒數百萬的消息。
(3)支持通過Kafka服務器和消費機集群來分區消息。
(4)支持Hadoop并行數據加載。
(5)同時支持離線數據處理和實時數據處理。
(6)Scale out:支持在線水平擴展。
Kafka提供的消息中間件的功能明顯較少一些,但是Kafka的優勢在于專為超高吞吐量的實時日志采集、實時數據同步、實時數據計算等場景來設計。
?一、Kafka 基礎概念 ??
概念一:生產者與消費者
對于 Kafka 來說客戶端有兩種基本類型:生產者(Producer)和消費者(Consumer)。除此之外,還有用來做數據集成的 Kafka Connect API 和流式處理的 Kafka Streams 等高階客戶端,但這些高階客戶端底層仍然是生產者和消費者API,它們只不過是在上層做了封裝。
這很容易理解,生產者(也稱為發布者)創建消息,而消費者(也稱為訂閱者)負責消費or讀取消息。
概念二:主題(Topic)與分區(Partition)
在 Kafka 中,消息以主題(Topic)來分類,每一個主題都對應一個「消息隊列」,這有點兒類似于數據庫中的表。但是如果我們把所有同類的消息都塞入到一個“中心”隊列中,勢必缺少可伸縮性,無論是生產者/消費者數目的增加,還是消息數量的增加,都可能耗盡系統的性能或存儲。
我們使用一個生活中的例子來說明:現在 A 城市生產的某商品需要運輸到 B 城市,走的是公路,那么單通道的高速公路不論是在「A 城市商品增多」還是「現在 C 城市也要往 B 城市運輸東西」這樣的情況下都會出現「吞吐量不足」的問題。所以我們現在引入分區(Partition)的概念,類似“允許多修幾條道”的方式對我們的主題完成了水平擴展。
概念三:Broker 和集群(Cluster)
一個 Kafka 服務器也稱為 Broker,它接受生產者發送的消息并存入磁盤;Broker 同時服務消費者拉取分區消息的請求,返回目前已經提交的消息。使用特定的機器硬件,一個 Broker 每秒可以處理成千上萬的分區和百萬量級的消息。
若干個 Broker 組成一個集群(Cluster),其中集群內某個 Broker 會成為集群控制器(Cluster Controller),它負責管理集群,包括分配分區到 Broker、監控 Broker 故障等。在集群內,一個分區由一個 Broker 負責,這個 Broker 也稱為這個分區的 Leader;當然一個分區可以被復制到多個 Broker 上來實現冗余,這樣當存在 Broker 故障時可以將其分區重新分配到其他 Broker 來負責。下圖是一個樣例:
Kafka 的一個關鍵性質是日志保留(retention),我們可以配置主題的消息保留策略,譬如只保留一段時間的日志或者只保留特定大小的日志。當超過這些限制時,老的消息會被刪除。我們也可以針對某個主題單獨設置消息過期策略,這樣對于不同應用可以實現個性化。
概念四:多集群
隨著業務發展,我們往往需要多集群,通常處于下面幾個原因:
● 基于數據的隔離;
● 基于安全的隔離;
● 多數據中心(容災)
當構建多個數據中心時,往往需要實現消息互通。舉個例子,假如用戶修改了個人資料,那么后續的請求無論被哪個數據中心處理,這個更新需要反映出來。又或者,多個數據中心的數據需要匯總到一個總控中心來做數據分析。
上面說的分區復制冗余機制只適用于同一個 Kafka 集群內部,對于多個 Kafka 集群消息同步可以使用 Kafka 提供的 MirrorMaker 工具。本質上來說,MirrorMaker 只是一個 Kafka 消費者和生產者,并使用一個隊列連接起來而已。它從一個集群中消費消息,然后往另一個集群生產消息。
二、Kafka 的設計與實現
上面我們知道了 Kafka 中的一些基本概念,但作為一個成熟的「消息隊列」中間件,其中有許多有意思的設計值得我們思考,下面我們簡單列舉一些。
討論一:Kafka 存儲在文件系統上
是的,您首先應該知道 Kafka 的消息是存在于文件系統之上的。Kafka 高度依賴文件系統來存儲和緩存消息,一般的人認為 “磁盤是緩慢的”,所以對這樣的設計持有懷疑態度。實際上,磁盤比人們預想的快很多也慢很多,這取決于它們如何被使用;一個好的磁盤結構設計可以使之跟網絡速度一樣快。
現代的操作系統針對磁盤的讀寫已經做了一些優化方案來加快磁盤的訪問速度。比如,預讀會提前將一個比較大的磁盤快讀入內存。后寫會將很多小的邏輯寫操作合并起來組合成一個大的物理寫操作。并且,操作系統還會將主內存剩余的所有空閑內存空間都用作磁盤緩存,所有的磁盤讀寫操作都會經過統一的磁盤緩存(除了直接 I/O 會繞過磁盤緩存)。綜合這幾點優化特點,如果是針對磁盤的順序訪問,某些情況下它可能比隨機的內存訪問都要快,甚至可以和網絡的速度相差無幾。
上述的 Topic 其實是邏輯上的概念,面相消費者和生產者,物理上存儲的其實是 Partition,每一個 Partition 最終對應一個目錄,里面存儲所有的消息和索引文件。默認情況下,每一個 Topic 在創建時如果不指定 Partition 數量時只會創建 1 個 Partition。比如,我創建了一個 Topic 名字為 test ,沒有指定 Partition 的數量,那么會默認創建一個 test-0 的文件夾,這里的命名規則是:<topic_name>-<partition_id>。
任何發布到 Partition 的消息都會被追加到 Partition 數據文件的尾部,這樣的順序寫磁盤操作讓 Kafka 的效率非常高(經驗證,順序寫磁盤效率比隨機寫內存還要高,這是 Kafka 高吞吐率的一個很重要的保證)。
每一條消息被發送到 Broker 中,會根據 Partition 規則選擇被存儲到哪一個 Partition。如果 Partition 規則設置的合理,所有消息可以均勻分布到不同的 Partition中。
討論二:Kafka 中的底層存儲設計
假設我們現在 Kafka 集群只有一個 Broker,我們創建 2 個 Topic 名稱分別為:「topic1」和「topic2」,Partition 數量分別為 1、2,那么我們的根目錄下就會創建如下三個文件夾:
????| --topic1-0
? ? | --topic2-0
? ? | --topic2-1
在 Kafka 的文件存儲中,同一個 Topic 下有多個不同的 Partition,每個 Partition 都為一個目錄,而每一個目錄又被平均分配成多個大小相等的Segment File中,Segment File 又由 index file 和 data file 組成,他們總是成對出現,后綴 ".index" 和 ".log" 分表表示 Segment 索引文件和數據文件。
現在假設我們設置每個 Segment 大小為 500 MB,并啟動生產者向 topic1 中寫入大量數據,topic1-0 文件夾中就會產生類似如下的一些文件:
????| --topic1-0
? ? ? ? | --00000000000000000000.index
? ? ? ? | --00000000000000000000.log
? ? ? ? | --00000000000000368769.index
? ? ? ? | --00000000000000368769.log
? ? ? ? | --00000000000000737337.index
? ? ? ? | --00000000000000737337.log
? ? ? ? | --00000000000001105814.index
? ? ? ? | --00000000000001105814.log
? ? | --topic2-0
? ? | --topic2-1
Segment 是 Kafka 文件存儲的最小單位。Segment 文件命名規則:Partition 全局的第一個 Segment 從 0 開始,后續每個 Segment 文件名為上一個 Segment 文件最后一條消息的 offset 值。數值最大為 64 位 long 大小,19 位數字字符長度,沒有數字用0填充。如 00000000000000368769.index 和 00000000000000368769.log。
以上面的一對 Segment File 為例,說明一下索引文件和數據文件對應關系:
其中以索引文件中元數據<3, 497>為例,依次在數據文件中表示第 3 個 message(在全局 Partition 表示第 368769 + 3 = 368772 個 message)以及該消息的物理偏移地址為 497。
注意該 index 文件并不是從0開始,也不是每次遞增1的,這是因為 Kafka 采取稀疏索引存儲的方式,每隔一定字節的數據建立一條索引,它減少了索引文件大小,使得能夠把 index 映射到內存,降低了查詢時的磁盤 IO 開銷,同時也并沒有給查詢帶來太多的時間消耗。
因為其文件名為上一個 Segment 最后一條消息的 offset ,所以當需要查找一個指定 offset 的 message 時,通過在所有 segment 的文件名中進行二分查找就能找到它歸屬的 segment ,再在其 index 文件中找到其對應到文件上的物理位置,就能拿出該 message 。
由于消息在 Partition 的 Segment 數據文件中是順序讀寫的,且消息消費后不會刪除(刪除策略是針對過期的 Segment 文件),這種順序磁盤 IO 存儲設計是 Kafka 高性能很重要的原因。
????????Kafka 是如何準確的知道 message 的偏移的呢?這是因為在 Kafka 定義了標準的數據存儲結構,在 Partition 中的每一條 message 都包含了以下三個屬性:
????????offset:表示 message 在當前 Partition 中的偏移量,是一個邏輯上的值,唯一確定了 Partition 中的一條 message,可以簡單的認為是一個 id;
????????MessageSize:表示 message 內容 data 的大??;
????????data:message 的具體內容
討論三:生產者設計概要
當我們發送消息之前,先問幾個問題:每條消息都是很關鍵且不能容忍丟失么?偶爾重復消息可以么?我們關注的是消息延遲還是寫入消息的吞吐量?
舉個例子,有一個信用卡交易處理系統,當交易發生時會發送一條消息到 Kafka,另一個服務來讀取消息并根據規則引擎來檢查交易是否通過,將結果通過 Kafka 返回。對于這樣的業務,消息既不能丟失也不能重復,由于交易量大因此吞吐量需要盡可能大,延遲可以稍微高一點。
再舉個例子,假如我們需要收集用戶在網頁上的點擊數據,對于這樣的場景,少量消息丟失或者重復是可以容忍的,延遲多大都不重要只要不影響用戶體驗,吞吐則根據實時用戶數來決定。
不同的業務需要使用不同的寫入方式和配置。具體的方式我們在這里不做討論,現在先看下生產者寫消息的基本流程:
流程如下:
1)首先,我們需要創建一個ProducerRecord,這個對象需要包含消息的主題(topic)和值(value),可以選擇性指定一個鍵值(key)或者分區(partition)。
2)發送消息時,生產者會對鍵值和值序列化成字節數組,然后發送到分配器(partitioner)。
3)如果我們指定了分區,那么分配器返回該分區即可;否則,分配器將會基于鍵值來選擇一個分區并返回。
4)選擇完分區后,生產者知道了消息所屬的主題和分區,它將這條記錄添加到相同主題和分區的批量消息中,另一個線程負責發送這些批量消息到對應的Kafka broker。
5)當broker接收到消息后,如果成功寫入則返回一個包含消息的主題、分區及位移的RecordMetadata對象,否則返回異常。
6)生產者接收到結果后,對于異??赡軙M行重試。
討論四:消費者設計概要
消費者與消費組
假設這么個場景:我們從Kafka中讀取消息,并且進行檢查,最后產生結果數據。我們可以創建一個消費者實例去做這件事情,但如果生產者寫入消息的速度比消費者讀取的速度快怎么辦呢?這樣隨著時間增長,消息堆積越來越嚴重。對于這種場景,我們需要增加多個消費者來進行水平擴展。
Kafka消費者是消費組的一部分,當多個消費者形成一個消費組來消費主題時,每個消費者會收到不同分區的消息。假設有一個T1主題,該主題有4個分區;同時我們有一個消費組G1,這個消費組只有一個消費者C1。那么消費者C1將會收到這4個分區的消息,如下所示:
如果我們增加新的消費者C2到消費組G1,那么每個消費者將會分別收到兩個分區的消息,如下所示:
如果增加到4個消費者,那么每個消費者將會分別收到一個分區的消息,如下所示:
但如果我們繼續增加消費者到這個消費組,剩余的消費者將會空閑,不會收到任何消息:
總而言之,我們可以通過增加消費組的消費者來進行水平擴展提升消費能力。這也是為什么建議創建主題時使用比較多的分區數,這樣可以在消費負載高的情況下增加消費者來提升性能。另外,消費者的數量不應該比分區數多,因為多出來的消費者是空閑的,沒有任何幫助。
Kafka一個很重要的特性就是,只需寫入一次消息,可以支持任意多的應用讀取這個消息。換句話說,每個應用都可以讀到全量的消息。為了使得每個應用都能讀到全量消息,應用需要有不同的消費組。對于上面的例子,假如我們新增了一個新的消費組G2,而這個消費組有兩個消費者,那么會是這樣的:
在這個場景中,消費組G1和消費組G2都能收到T1主題的全量消息,在邏輯意義上來說它們屬于不同的應用。
最后,總結起來就是:如果應用需要讀取全量消息,那么請為該應用設置一個消費組;如果該應用消費能力不足,那么可以考慮在這個消費組里增加消費者。
消費組與分區重平衡
可以看到,當新的消費者加入消費組,它會消費一個或多個分區,而這些分區之前是由其他消費者負責的;另外,當消費者離開消費組(比如重啟、宕機等)時,它所消費的分區會分配給其他分區。這種現象稱為重平衡(rebalance)。重平衡是 Kafka 一個很重要的性質,這個性質保證了高可用和水平擴展。不過也需要注意到,在重平衡期間,所有消費者都不能消費消息,因此會造成整個消費組短暫的不可用。而且,將分區進行重平衡也會導致原來的消費者狀態過期,從而導致消費者需要重新更新狀態,這段期間也會降低消費性能。后面我們會討論如何安全的進行重平衡以及如何盡可能避免。
消費者通過定期發送心跳(hearbeat)到一個作為組協調者(group coordinator)的 broker 來保持在消費組內存活。這個 broker 不是固定的,每個消費組都可能不同。當消費者拉取消息或者提交時,便會發送心跳。
如果消費者超過一定時間沒有發送心跳,那么它的會話(session)就會過期,組協調者會認為該消費者已經宕機,然后觸發重平衡??梢钥吹剑瑥南M者宕機到會話過期是有一定時間的,這段時間內該消費者的分區都不能進行消息消費;通常情況下,我們可以進行優雅關閉,這樣消費者會發送離開的消息到組協調者,這樣組協調者可以立即進行重平衡而不需要等待會話過期。
在 0.10.1 版本,Kafka 對心跳機制進行了修改,將發送心跳與拉取消息進行分離,這樣使得發送心跳的頻率不受拉取的頻率影響。另外更高版本的 Kafka 支持配置一個消費者多長時間不拉取消息但仍然保持存活,這個配置可以避免活鎖(livelock)?;铈i,是指應用沒有故障但是由于某些原因不能進一步消費。
Partition 與消費模型
上面提到,Kafka 中一個 topic 中的消息是被打散分配在多個 Partition(分區) 中存儲的, Consumer Group 在消費時需要從不同的 Partition 獲取消息,那最終如何重建出 Topic 中消息的順序呢?
答案是:沒有辦法。Kafka 只會保證在 Partition 內消息是有序的,而不管全局的情況。
下一個問題是:Partition 中的消息可以被(不同的 Consumer Group)多次消費,那 Partition中被消費的消息是何時刪除的? Partition 又是如何知道一個 Consumer Group 當前消費的位置呢?
無論消息是否被消費,除非消息到期 Partition 從不刪除消息。例如設置保留時間為 2 天,則消息發布 2 天內任何 Group 都可以消費,2 天后,消息自動被刪除。
Partition 會為每個 Consumer Group 保存一個偏移量,記錄 Group 消費到的位置。 如下圖:
為什么 Kafka 是 pull 模型
消費者應該向 Broker 要數據(pull)還是 Broker 向消費者推送數據(push)?作為一個消息系統,Kafka 遵循了傳統的方式,選擇由 Producer 向 broker push 消息并由 Consumer 從 broker pull 消息。一些 logging-centric system,比如 Facebook 的Scribe和 Cloudera 的Flume,采用 push 模式。事實上,push 模式和 pull 模式各有優劣。
push 模式很難適應消費速率不同的消費者,因為消息發送速率是由 broker 決定的。push 模式的目標是盡可能以最快速度傳遞消息,但是這樣很容易造成 Consumer 來不及處理消息,典型的表現就是拒絕服務以及網絡擁塞。而 pull 模式則可以根據 Consumer 的消費能力以適當的速率消費消息。
對于 Kafka 而言,pull 模式更合適。pull 模式可簡化 broker 的設計,Consumer 可自主控制消費消息的速率,同時 Consumer 可以自己控制消費方式——即可批量消費也可逐條消費,同時還能選擇不同的提交方式從而實現不同的傳輸語義。
討論五:Kafka 如何保證可靠性
當我們討論可靠性的時候,我們總會提到保證*這個詞語??煽啃员WC是基礎,我們基于這些基礎之上構建我們的應用。比如關系型數據庫的可靠性保證是ACID,也就是原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability)。
Kafka 中的可靠性保證有如下四點:
1)對于一個分區來說,它的消息是有序的。如果一個生產者向一個分區先寫入消息A,然后寫入消息B,那么消費者會先讀取消息A再讀取消息B。
2)當消息寫入所有in-sync狀態的副本后,消息才會認為已提交(committed)。這里的寫入有可能只是寫入到文件系統的緩存,不一定刷新到磁盤。生產者可以等待不同時機的確認,比如等待分區主副本寫入即返回,后者等待所有in-sync狀態副本寫入才返回。
3)一旦消息已提交,那么只要有一個副本存活,數據不會丟失。
4)消費者只能讀取到已提交的消息。
使用這些基礎保證,我們構建一個可靠的系統,這時候需要考慮一個問題:究竟我們的應用需要多大程度的可靠性?可靠性不是無償的,它與系統可用性、吞吐量、延遲和硬件價格息息相關,得此失彼。因此,我們往往需要做權衡,一味的追求可靠性并不實際。
kafka機制:https://yq.aliyun.com/articles/608566
kafka速度為什么那么快?https://www.cnblogs.com/binyue/p/10308754.html
寫入數據:1)順序寫入? ?2)內存映射文件(Memory Mapped Files)
讀取數據:1)基于sendfile實現Zero Copy? ? 2)批量壓縮
Kafka如何保證消息不丟失不重復?
https://www.cnblogs.com/cherish010/p/9764810.html
https://msd.misuland.com/pd/2884250068896974728
為什么 Kafka 性能高?
1)順序寫磁盤
順序寫磁盤的性能是隨機寫入的性能的6000倍的提升,媲美內存隨機訪問的性能,磁盤不再是瓶頸點。
2)Page Cache
為了優化讀寫性能,Kafka利用了操作系統本身的Page Cache,就是利用操作系統自身的內存而不是JVM空間內存。通過操作系統的Page Cache,Kafka的讀寫操作基本上是基于內存的,讀寫速度得到了極大的提升。
3)零拷貝技術
零拷貝技術,可以有效的減少上下文切換和拷貝次數。
參考:http://www.lxweimin.com/p/ff6788e68a8e
4)引入消息中間件后的缺點
1)、系統可用性降低
首先是你的系統整體可用性絕對會降低,給你舉個例子,我們就拿之前的一幅圖來說明。
比如說一個核心鏈路里面,系統A -> 系統B -> 系統C,然后系統C是通過MQ異步調用系統D的。
看起來很好,你用這個MQ異步化的手段解決了一個核心鏈路執行性能過差的問題。但是你有沒有考慮另外一個問題,就是萬一你依賴的那個MQ中間件突然掛掉了怎么辦?
這個還真的不是異想天開,MQ、Redis、MySQL這些組件都有可能會掛掉。一旦你的MQ掛了,就導致你的系統的核心業務流程中斷了。本來你要是不引入MQ中間件,那其實就是一些系統之間的調用,但是現在你引入了MQ,就導致你多了一個依賴。一旦多了一個依賴,就會導致你的可用性降低。
因此,一旦引入了MQ中間件,你就必須去考慮這個MQ是如何部署的,如何保證高可用性。甚至在復雜的高可用的場景下,你還要考慮如果MQ一旦掛了以后,你的系統有沒有備用兜底的技術方案,可以保證系統繼續運行下去。
2)、系統穩定性降低
還是上面那張圖,大家再來看一下。
不知道大家有沒有發現一個問題,這個鏈路除了MQ中間件掛掉這個可能存在的隱患之外,可能還有一些其他的技術問題。比如說,莫名其妙的,系統C發了一個消息到MQ,結果那個消息因為網絡故障等問題,就丟失了。這就導致系統D沒有收到那條消息。這可就慘了,這樣會導致系統D沒完成自己該做的任務,此時可能整個系統會出現業務錯亂,數據丟失,嚴重的bug,用戶體驗很差等各種問題。
這還只是其中之一,萬一說系統C給MQ發送消息,不小心一抽風重復發了一條一模一樣的,導致消息重復了,這個時候該怎么辦?可能會導致系統D一下子把一條數據插入了兩次,導致數據錯誤,臟數據的產生,最后一樣會導致各種問題。
或者說如果系統D突然宕機了幾個小時,導致無法消費消息,結果大量的消息在MQ中間件里積壓了很久,這個時候怎么辦?即使系統D恢復了,也需要慢慢的消費數據來進行處理。
所以這就是引入MQ中間件的第二個大問題,系統穩定性可能會下降,故障會增多,各種各樣亂七八糟的問題都可能產生。而且一旦產生了一個問題,就會導致系統整體出問題。就需要為了解決各種MQ引發的技術問題,采取很多的技術方案。
3)、分布式一致性問題
引入消息中間件,還有分布式一致性的問題。舉個例子,比如說系統C現在處理自己本地數據庫成功了,然后發送了一個消息給MQ,系統D也確實是消費到了。但是結果不幸的是,系統D操作自己本地數據庫失敗了,那這個時候咋辦?系統C成功了,系統D失敗了,會導致系統整體數據不一致了啊。所以此時又需要使用可靠消息最終一致性的分布式事務方案來保障。
總結:
在面試中要答好這個問題,首先一定要熟悉MQ這個技術的優缺點。了解清楚把他引入系統是為了解決哪些問題的,但是他自身又會帶來哪些問題。此外,對于引入MQ以后,是否對他自身可能引發的問題有一些方案的設計,來保證你的系統高可用、高可靠的運行,保證數據的一致性。這個也有做好相應的準備。這個可使用基于消息的最終一致性來保障系統的穩定。