3.1 萬億級數據洪峰下的分布式消息引擎
前言
通過簡單回顧阿里中間件(Aliware)消息引擎的發展史,本文開篇于雙11消息引擎面臨的低延遲挑戰,通過經典的應用場景闡述可能會面臨的問題 - 響應慢,雪崩,用戶體驗差,繼而交易下跌。為了應對這些不可控的洪峰數據,中間件團隊通過大量研究和實踐,推出了低延遲高可用解決方案,在分布式存儲領域具有一定的普適性。在此基礎上,通過對現有有限資源的規劃,又推出了分級的容量保障策略,通過限流、降級,甚至熔斷技術,能夠有效保障重點業務的高吞吐,成功的支撐集團包括海外業務平緩舒暢地度過雙11高峰。與此同時,在一些對高可靠、高可用要求極為苛刻的場景下,中間件團隊又重點推出了基于多副本機制的高可用解決方案,能夠動態識別機器宕機、機房斷網等災難場景,自動實現主備切換。整個切換過程對用戶透明,運維開發人員無需干預,極大地提升消息存儲的可靠性以及整個集群的高可用性。
?
1. 消息引擎家族史
阿里中間件消息引擎發展到今日,前前后后經歷了三代演進。第一代,推模式,數據存儲采用關系型數據庫。在這種模式下,消息具有很低的延遲特性,尤其在阿里淘寶這種高頻交易場景中,具有非常廣泛地應用。第二代,拉模式,自研的專有消息存儲。能夠媲美Kafka的吞吐性能,但考慮到淘寶的應用場景,尤其是其交易鏈路等高可靠場景,消息引擎并沒有一位的追求吞吐,而是將穩定可靠放在首位。因為采用了長連接拉模式,在消息的實時方面絲毫不遜推模式。在前兩代經歷了數年線上堪比工況的洗禮后,中間件團隊于2011年研發了以拉模式為主,兼有推模式的高性能、低延遲消息引擎RocketMQ。并在2012年進行了開源,經歷了6年雙11核心交易鏈路檢驗,愈久彌堅。目前已經捐贈給阿帕奇基金會(ASF),有望成為繼ActiveMQ,Kafka之后,Apache社區第三個重量級分布式消息引擎。時至今日,RocketMQ很好的服務了阿里集團大大小小上千個應用,在雙11當天,更有不可思議的萬億級消息流轉,為集團大中臺的穩定發揮了舉足輕重的作用。
2. 低延遲可用性探索
疾風吹征帆,倏爾向空沒。千里在俄頃,三江坐超忽。—孟浩然
?
2.1 低延遲與可用性
隨著Java語言生態的完善,JVM性能的提高,C和C++已經不再是低延遲場景唯一的選擇。本章節重點介紹RocketMQ在低延遲可用性方面的一些探索。
應用程序的性能度量標準一般從吞吐量和延遲兩方面考量。吞吐量是指程序在一段時間內能處理的請求數量。延遲是指端到端的響應時間。低延遲在不同的環境下有不同的定義,比如在聊天應用中低延遲可以定義為200ms內,在交易系統中定義為10ms內。相對于吞吐量,延遲會受到很多因素的影響,如CPU、網絡、內存、操作系統等。
根據Little's law,當延遲變高時,駐留在分布式系統中的請求會劇增,導致某些節點不可用,不可用的狀態甚至會擴散至其它節點,造成整個系統的服務能力喪失,這種場景又俗稱雪崩。所以打造低延遲的應用程序,對提升整個分布式系統可用性有很大的裨益。
?
2.2 低延遲探索之路
RocketMQ作為一款消息引擎,最大的作用是異步解耦和削峰填谷。一方面,分布式應用會利用RocketMQ來進行異步解耦,應用程序可以自如地擴容和縮容。另一方面,當洪峰數據來臨時,大量的消息需要堆積到RocketMQ中,后端程序可以根據自己的消費速度來進行數據的讀取。所以保證RocketMQ寫消息鏈路的低延遲至關重要。
在今年雙11期間,天貓發布了紅包火山的新玩法。該游戲對延遲非常敏感,只能容忍50ms內的延遲,在壓測初期RocketMQ寫消息出現了大量50~500ms的延遲,導致了在紅包噴發的高峰出現大量的失敗,嚴重影響前端業務。下圖為壓測紅包集群在壓測時寫消息延遲熱力圖統計。
作為一款純Java語言開發的消息引擎,RocketMQ自主研發的存儲組件,依賴Page Cache進行加速和堆積,意味著它的性能會受到JVM、GC、內核、Linux內存管理機制、文件IO等因素的影響。如下圖所示,一條消息從客戶端發送出,到最終落盤持久化,每個環節都有產生延遲的風險。通過對線上數據的觀察,RocketMQ寫消息鏈路存在偶發的高達數秒的延遲。
2.2.1? JVM停頓
JVM(Java虛擬機)在運行過程中會產生很多停頓,常見的有GC、JIT、取消偏向鎖(RevokeBias)、RedefineClasses(AOP)等。對應用程序影響最大的則是GC停頓。RocketMQ盡量避免Full GC,但Minor GC帶來的停頓是難以避免的。針對GC調優是一個很伽利略的問題,需要通過大量的測試來幫助應用程序調整GC參數,比如可以通過調整堆大小,GC的時機,優化數據結構等手段進行調優。
對于其它JVM停頓,可以通過-XX:+PrintGCApplicationStoppedTime將JVM停頓時間輸出到GC日志中。通過-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1輸出具體的停頓原因,并進行針對性的優化。比如在RocketMQ中發現取RevokeBias產生了大量的停頓,通過-XX:-UseBiasedLocking關閉了偏向鎖特性。
另外,GC日志的輸出會發生文件IO,有時候也會造成不必要的停頓,可以將GC日志輸出到tmpfs(內存文件系統)中,但tmpfs會消耗內存,為了避免內存被浪費可以使用-XX:+UseGCLogFileRotation滾動GC日志。
除了GC日志會產生文件IO,JVM會將jstat命令需要的一些統計數據輸出到/tmp(hsperfdata)目錄下,可通過-XX:+PerfDisableSharedMem關閉該特性,并使用JMX來代替jstat。
?
2.2.2? 鎖——同步的“利”器
作為一種臨界區的保護機制,鎖被廣泛用于多線程應用程序的開發中。但鎖是一把雙刃劍,過多或不正確的使用鎖會導致多線程應用的性能下降。
Java中的鎖默認采用的是非公平鎖,加鎖時不考慮排隊問題,直接嘗試獲取鎖,若獲取失敗自動進行排隊。非公平鎖會導致線程等待時間過長,延遲變高。倘若采取公平鎖,又會對應用帶來較大性能損失。
另一方面,同步會引起上下文切換,這會帶來一定的開銷。上下文切換一般是微秒級,但當線程數過多,競爭壓力大時,會產生數十毫秒級別的開銷。可通過LockSupport.park來模擬產生上下文切換進行測試。
為了避免鎖帶來的延遲,利用CAS原語將RocketMQ核心鏈路無鎖化,在降低延遲的同時顯著提高吞吐量。
?
2.2.3? 內存——沒那么快
受限于Linux的內存管理機制,應用程序訪問內存時有時候會產生高延遲。Linux中內存主要有匿名內存和Page Cache兩種。
Linux會用盡可能多的內存來做緩存,大多數情形下,服務器可用內存都較少。可用內存較少時,應用程序申請或者訪問新的內存頁會引發內存回收,當后臺內存回收的速度不及分配內存的速度時,會進入直接回收(Direct Reclaim),應用程序會自旋等待內存回收完畢,產生巨大的延遲,如下圖所示。
另一方面,內核也會回收匿名內存頁,匿名內存頁被換出后下一次訪問會產生文件IO,導致延遲,如下圖所示。
上述兩種情況產生的延遲可以通過內核參數(vm.extra_free_kbytes和vm.swappiness)調優加以避免。
Linux對內存的管理一般是以頁為單位,一頁一般為4k大小,當在同一頁內存上產生讀寫競爭時,會產生延遲,對于這種情況,需要應用程序自行協調內存的訪問加以避免。
?
2.2.4? Page Cache——利與弊??
Page Cache是文件的緩存,用于加速對文件的讀寫,它為RocketMQ提供了更強大的堆積能力。RocketMQ將數據文件映射到內存中,寫消息的時候首先寫入Page Cache,并通過異步刷盤的模式將消息持久化(同時也支持同步刷盤),消息可以直接從Page Cache中讀取,這也是業界分布式存儲產品通常采用的模式,如下圖所示:
該模式大多數情況讀寫速度都比較迅速,但當遇到操作系統進行臟頁回寫,內存回收,內存換入換出等情形時,會產生較大的讀寫延遲,造成存儲引擎偶發的高延遲。
針對這種現象,RocketMQ采用了多種優化技術,比如內存預分配,文件預熱,mlock系統調用,讀寫分離等,來保證利用Page Cache優點的同時,消除其帶來的延遲。
?
2.3 優化成果
RocketMQ通過對上述情況的優化,成功消除了寫消息高延遲的情形,并通過了今年雙11的考驗。優化后寫消息耗時熱力圖如下圖所示。
優化后RocketMQ寫消息延遲99.995%在1ms內,100%在100ms內,如下圖所示。
3. 容量保障三大法寶
他強任他強,清風拂山崗。他橫任他橫,明月照大江。—九陽真經心法
?
有了低延遲的優化保障,并不意味著消息引擎就可以高枕無憂。為了給應用帶來如絲般順滑的體驗,消息引擎必須進行靈活的容量規劃。如何讓系統能夠在洶涌澎湃的流量洪峰面前談笑風生?降級、限流、熔斷三大法寶便有了用武之地。丟卒保車,以降級、暫停邊緣服務、組件為代價保障核心服務的資源,以系統不被突發流量擊垮為第一要務。正所謂,他強任他強,清風拂山崗。他橫任他橫,明月照大江!
從架構的穩定性角度看,在有限資源的情況下,所能提供的單位時間服務能力也是有限的。假如超過承受能力,可能會帶來整個服務的停頓,應用的Crash,進而可能將風險傳遞給服務調用方造成整個系統的服務能力喪失,進而引發雪崩。另外,根據排隊理論,具有延遲的服務隨著請求量的不斷提升,其平均響應時間也會迅速提升,為了保證服務的SLA,有必要控制單位時間的請求量。這就是限流為什么愈發重要的原因。限流這個概念,在學術界又被稱之為Traffic Shaping。最早起源于網絡通訊領域,典型的有漏桶(leaky bucket)算法和令牌桶(token bucket)算法。
漏桶算法基本思路是有一個桶(會漏水),水以恒定速率滴出,上方會有水滴(請求)進入水桶。如果上方水滴進入速率超過水滴出的速率,那么水桶就會溢出,即請求過載。
令牌桶算法基本思路是同樣也有一個桶,令牌以恒定速率放入桶,桶內的令牌數有上限,每個請求會acquire一個令牌,如果某個請求來到而桶內沒有令牌了,則這個請求是過載的。很顯然,令牌桶會存在請求突發激增的問題。
無論是漏桶、令牌桶,抑或其它變種算法,都可以看做是一種控制速度的限流,工程領域如Guava里的RateLimiter,Netty里的TrafficShaping等也都屬于此。除此之外,還有一種控制并發的限流模式,如操作系統里的信號量,JDK里的Semaphore。
異步解耦,削峰填谷,作為消息引擎的看家本領,Try your best本身就是其最初的設計初衷(RPC、應用網關、容器等場景下,控制速度應成為流控首選)。但即便如此,一些必要的流控還是需要考量。不過與前面介紹的不同,RocketMQ中并沒有內置Guava、Netty等拆箱即用的速度流控組件。而是通過借鑒排隊理論,對其中的慢請求進行容錯處理。這里的慢請求是指排隊等待時間以及服務時間超過某個閾值的請求。對于離線應用場景,容錯處理就是利用滑動窗口機制,通過緩慢縮小窗口的手段,來減緩從服務端拉的頻率以及消息大小,降低對服務端的影響。而對于那些高頻交易,數據復制場景,則采取了快速失敗策略,既能預防應用連鎖的資源耗盡而引發的應用雪崩,又能有效降低服務端壓力,為端到端低延遲帶來可靠保障。
服務降級是一種典型的丟卒保車,二八原則實踐。而降級的手段也無外乎關閉,下線等“簡單粗暴”的操作。降級目標的選擇,更多來自于服務QoS的定義。消息引擎早期對于降級的處理主要來自兩方面,一方面來自于用戶數據的收集,另一方面來自引擎組件的服務QoS設定。對于前者,通過運維管控系統推送應用自身QoS數據,一般會輸出如下表格。而引擎組件的服務QoS,如服務于消息問題追溯的鏈路軌跡組件,對于核心功能來說,定級相對較低,可在洪峰到來之前提前關閉。
談到熔斷,不得不提經典的電力系統中的保險絲,當負載過大,或者電路發生故障或異常時,電流會不斷升高,為防止升高的電流有可能損壞電路中的某些重要器件或貴重器件,燒毀電路甚至造成火災。保險絲會在電流異常升高到一定的高度和熱度的時候,自身熔斷切斷電流,從而起到保護電路安全運行的作用。
同樣,在分布式系統中,如果調用的遠程服務或者資源由于某種原因無法使用時,沒有這種過載保護,就會導致請求的資源阻塞在服務器上等待從而耗盡系統或者服務器資源。很多時候剛開始可能只是系統出現了局部的、小規模的故障,然而由于種種原因,故障影響的范圍越來越大,最終導致了全局性的后果。而這種過載保護就是大家俗稱的熔斷器(Circuit Breaker)。Netflix公司為了解決該問題,開源了它們的熔斷解決方案Hystrix。
上述三幅圖,描述了系統從初始的健康狀態到高并發場景下阻塞在下游的某個關鍵依賴組件的場景。這種情況很容易誘發雪崩效應。而通過引入Hystrix的熔斷機制,讓應用快速失敗,繼而能夠避免最壞情況的發生。
借鑒Hystrix思路,中間件團隊自研了一套消息引擎熔斷機制。在大促壓測備戰期間,曾經出現過由于機器硬件設備導致服務不可用。如果采用常規的容錯手段,是需要等待30秒時間,不可用機器才能從列表里被摘除。但通過這套熔斷機制,能在毫秒范圍內識別并隔離異常服務。進一步提升了引擎的可用性。
?
4. 高可用解決方案
昔之善戰者,先為不可勝,以待敵之可勝。不可勝在己,可勝在敵。故善戰者,能為不可勝,不能使敵之必可勝。故曰:勝可知,而不可為。—孫武
雖然有了容量保障的三大法寶作為依托,但隨著消息引擎集群規模的不斷上升,到達一定程度后,集群中機器故障的可能性隨之提高,嚴重降低消息的可靠性以及系統的可用性。與此同時,基于多機房部署的集群模式也會引發機房斷網,進一步降低消息系統的可用性。為此,阿里中間件(Aliware)重點推出了基于多副本的高可用解決方案,動態識別機器故障、機房斷網等災難場景,實現故障自動恢復;整個恢復過程對用戶透明,無需運維人員干預,極大地提升了消息存儲的可靠性,保障了整個集群的高可用性。
高可用性幾乎是每個分布式系統在設計時必須要考慮的一個重要特性,在遵循CAP原則(即:一致性、可用性和分區容錯性三者無法在分布式系統中被同時滿足,并且最多只能滿足其中兩個)基礎上,業界也提出了一些針對分布式系統通用的高可用解決方案,如下圖所示:
其中,行代表了分布式系統中通用的高可用解決方案,包括冷備、Master/Slave、Master/Master、兩階段提交以及基于Paxos算法的解決方案;列代表了分布式系統所關心的各項指標,包括數據一致性、事務支持程度、數據延遲、系統吞吐量、數據丟失可能性、故障自動恢復方式。
從圖中可以看出,不同的解決方案對各項指標的支持程度各有側重。基于CAP原則,很難設計出一種高可用方案能同時夠滿足所有指標的最優值,以Master/Slave為例,一般滿足如下幾個特性:
1) Slave是Master的備份,可以根據數據的重要程度設置Slave的個數。
數據寫請求命中Master,讀請求可命中Master或者Slave。
2) 寫請求命中Master之后,數據可通過同步或者異步的方式從Master復制到Slave上;其中同步復制模式需要保證Master和Slave均寫成功后才反饋給客戶端成功;異步復制模式只需要保證Master寫成功即可反饋給客戶端成功。
數據通過同步或者異步方式從Master復制到Slave上,因此Master/Slave結構至少能保證數據的最終一致性;異步復制模式下,數據在Master寫成功后即可反饋給客戶端成功,因此系統擁有較低的延遲和較高的吞吐量,但同時會帶來Master故障丟數據的可能性;如期望異步復制模式下Master故障時數據仍不丟,Slave只能以Read-Only的方式等待Master的恢復,即延長了系統的故障恢復時間。相反,Master/Slave結構中的同步復制模式會以增大數據寫入延遲、降低系統吞吐量的代價來保證機器故障時數據不丟,同時降低系統故障恢復時間。
?
5. RocketMQ高可用架構
RocketMQ基于原有多機房部署的集群模式,利用分布式鎖和通知機制,借助Controller組件,設計并實現了Master/Slave結構的高可用架構,如下圖所示:
其中,Zookeeper作為分布式調度框架,需要至少在A、B、C三個機房部署以保證其高可用,并為RocketMQ高可用架構提供如下功能:
1) 維護持久節點(PERSISTENT),保存主備狀態機;
2) 維護臨時節點(EPHEMERAL),保存RocketMQ的當前狀態;
3) 當主備狀態機、服務端當前狀態發生變更時,通知對應的觀察者。
RocketMQ以Master/Slave結構實現多機房對等部署,消息的寫請求會命中Master,然后通過同步或者異步方式復制到Slave上進行持久化存儲;消息的讀請求會優先命中Master,當消息堆積導致磁盤壓力大時,讀請求轉移至Slave。
RocketMQ直接與Zookeeper進行交互,體現在:
1) 以臨時節點的方式向Zookeeper匯報當前狀態;
2) 作為觀察者監聽Zookeeper上主備狀態機的變更。當發現主備狀態機變化時,根據最新的狀態機更改當前狀態;
RocketMQ HA Controller是消息引擎高可用架構中降低系統故障恢復時間的無狀態組件,在A、B、C三個機房分布式部署,其主要職責體現在:
1) 作為觀察者監聽Zookeeper 上RocketMQ當前狀態的變更;
2) 根據集群的當前狀態,控制主備狀態機的切換并向Zookeeper匯報最新主備狀態機。
出于對系統復雜性以及消息引擎本身對CAP原則適配的考慮,RocketMQ高可用架構的設計采用了Master/Slave結構,在提供低延遲、高吞吐量消息服務的基礎上,采用主備同步復制的方式避免故障時消息的丟失。數據同步過程中,通過維護一個遞增的全局唯一SequenceID來保證數據強一致。同時引入故障自動恢復機制以降低故障恢復時間,提升系統的可用性。
?
5.1 可用性評估
系統可用性(Availability)是信息工業界用來衡量一個信息系統提供持續服務的能力,它表示的是在給定時間區間內系統或者系統某一能力在特定環境中能夠正常工作的概率。簡單地說, 可用性是平均故障間隔時間(MTBF)除以平均故障間隔時間(MTBF)和平均故障修復時間(MTTR)之和所得的結果, 即:
通常業界習慣用N個9來表征系統可用性,比如99.9%代表3個9的可用性,意味著全年不可用時間在8.76小時以內;99.999%代表5個9的可用性,意味著全年不可用時間必須保證在5.26分鐘以內,缺少故障自動恢復機制的系統將很難達到5個9的高可用性。
?
5.2 RocketMQ 高可用保障
通過可用性計算公式可以看出,要提升系統的可用性,需要在保障系統健壯性以延長平均無故障時間的基礎上,進一步加強系統的故障自動恢復能力以縮短平均故障修復時間。RocketMQ高可用架構設計并實現了Controller組件,按照單主狀態、異步復制狀態、半同步狀態以及最終的同步復制狀態的有限狀態機進行轉換。在最終的同步復制狀態下,Master和Slave任一節點故障時,其它節點能夠在秒級時間內切換到單主狀態繼續提供服務。相比于之前人工介入重啟來恢復服務,RokcetMQ高可用架構賦予了系統故障自動恢復的能力,能極大縮短平均故障恢復時間,提升系統的可用性。
?
下圖描述了RocketMQ高可用架構中有限狀態機的轉換:
- 第一個節點啟動后,Controller控制狀態機切換為單主狀態,通知啟動節點以Master角色提供服務。
- 第二個節點啟動后,Controller控制狀態機切換成異步復制狀態。Master通過異步方式向Slave復制數據。
- 當Slave的數據即將趕上Master,Controller控制狀態機切換成半同步狀態,此時命中Master的寫請求會被Hold住,直到Master以異步方式向Slave復制了所有差異的數據。
- 當半同步狀態下Slave的數據完全趕上Master時,Controller控制狀態機切換成同步復制模式,Mater開始以同步方式向Slave復制數據。該狀態下任一節點出現故障,其它節點能夠在秒級內切換到單主狀態繼續提供服務。
Controller組件控制RocketMQ按照單主狀態,異步復制狀態,半同步狀態,同步復制狀態的順序進行狀態機切換。中間狀態的停留時間與主備之間的數據差異以及網絡帶寬有關,但最終都會穩定在同步復制狀態下。
?
展望
雖然經歷了這么多年線上堪比工況的苛刻檢驗,阿里中間件消息引擎仍然存在著優化空間,如團隊正嘗試通過優化存儲算法、跨語言調用等策略進一步降低消息低延遲存儲。面對移動物聯網、大數據、VR等新興場景,面對席卷全球的開放與商業化生態,團隊開始著手打造第4代消息引擎,多級協議QoS,跨網絡、跨終端、跨語言支持,面向在線應用更低的響應時間,面向離線應用更高的吞吐,秉持取之于開源,回饋于開源的思想,相信RocektMQ朝著更健康的生態發展。
參考文獻
[1]Ryan Barrett.?http://snarfed.org/transactions_across_datacenters_io.html
[2]http://www.slideshare.net/vimal25792/leaky-bucket-tocken-buckettraffic-shaping
[3]http://systemdesigns.blogspot.com/2015/12/rate-limiter.html
[4]Little J D C, Graves S C. Little's law[M]//Building intuition. Springer US, 2008: 81-100.
[5]https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html-single/Performance_Tuning_Guide/index.html
[6]http://highscalability.com/blog/2012/3/12/google-taming-the-long-latency-tail-when-more-machines-equal.html
[7]https://www.azul.com/files/EnablingJavaInLatencySensitiveEnvs_DotCMSBootcamp_Nashville_23Oct20141.pdf