高并發(fā)系列:架構(gòu)優(yōu)化之從BAT實際案例看消息中間件的妙用
Part1是什么?為什么?
1什么是消息隊列
說到Java中的隊列應(yīng)該都不會陌生。其具有通過先進(jìn)先出,或者雙端進(jìn)出的方式進(jìn)行數(shù)據(jù)管理;通過阻塞以達(dá)到自動平衡負(fù)載的功能。
消息隊列之所以以隊列命名,起初也是因為其功能和操作,和java的本地隊列有相似之處。所以,我們可以簡單的認(rèn)為消息隊列就是為了滿足分布式下各服務(wù)之間的數(shù)據(jù)傳輸、管理和消費的一種中間服務(wù)。
2為什么要使用消息隊列
問:你們的系統(tǒng)中為什么要引入消息隊列?
我們總歸需要知曉消息隊列的使用價值,以及自己的業(yè)務(wù)場景下的實際痛點才能回答為什么要用消息隊列這個問題,才能回答系統(tǒng)引入消息隊列的價值所在。
系統(tǒng)間解耦
以前幾天在后臺和關(guān)注公號的一個大佬討論的廣告流水更新的操作為例:
廣告檢索系統(tǒng),需要感知廣告貼的信息變動來更新自己的索引,但實際上檢索系統(tǒng)和投放、物料、資產(chǎn)等系統(tǒng)間沒有必要依靠接口對感知行為進(jìn)行強(qiáng)關(guān)聯(lián),且接口的方式在維護(hù)和系統(tǒng)的壓力方面不友好,那么,消息隊列的作用就顯的很重要了,各系統(tǒng)發(fā)布各自的消息,誰需要誰訂閱,達(dá)到目的同時不會增加額外的系統(tǒng)調(diào)用壓力。(注:builder的接口調(diào)用是為了獲取最新的信息,此處可以通過壓縮等方式進(jìn)行優(yōu)化)
因此,當(dāng)系統(tǒng)間無實時數(shù)據(jù)交互要求,但還需要其業(yè)務(wù)信息時,可以用消息隊列來達(dá)到系統(tǒng)間解耦的作用,只要發(fā)布方定義好消息隊列格式,消費方的任何操作均可和發(fā)布方無關(guān),減少了不必要的聯(lián)調(diào)和發(fā)布沖突等影響。
服務(wù)異步化
最典型的一個例子,就是支付場景下的結(jié)果通知功能。
我們知道,一般情況下不管是app push 還是短信通知,都是比較耗時的操作。所以,沒有必要因為這些非核心功能的耗時操作而影響了支付的核心操作,只要我們在支付操作完成之后,將支付結(jié)果發(fā)到短信中心指定的消息topic下,短信中心自然會接收到此消息并保證通知給用戶。
圖片來源于知乎回答
因此使用消息隊列,讓非核心的操作異步化,提高整個業(yè)務(wù)鏈路的高效和穩(wěn)定,是很有效的。
削峰填谷
這個功能使我們本篇關(guān)注的重點,面對特殊場景如秒殺、春晚紅包等萬億級流量的脈沖式壓力下,一種保護(hù)我們系統(tǒng)的服務(wù)免于崩潰的有效手段就是消息隊列。
通過消息中心高性能的存儲和處理能力,將超過系統(tǒng)處理能力的多余流量暫時存儲起來,并在系統(tǒng)處理能力內(nèi)平緩釋放出來,達(dá)到削峰的效果。
比如我們的廣告計費系統(tǒng),面對上萬并發(fā)的商業(yè)貼檢索量,數(shù)千并發(fā)的點擊操作,實時接口的方式一定是不合適的,畢竟廣告行為和支付行為不一樣,支付失敗用戶還可以重試,但用戶的商業(yè)貼點擊行為是不可回放的,本次流量過去就過去了,因此,需要利用消息隊列將扣費請求緩存下來,來保證計費系統(tǒng)的穩(wěn)定。
其他
還如廣播、事務(wù)型、最終一致性等特性,也是消息隊列經(jīng)常用到的功能。
3消息隊列會存在哪些問題
業(yè)務(wù)上增加響應(yīng)延遲
前面提到,消息隊列使得業(yè)務(wù)非核心流程異步化,可以提高整個業(yè)務(wù)操作的時效性和流暢度,提升用戶操作體驗。但,也是因為數(shù)據(jù)進(jìn)入隊列的原因,不可避免的會耽擱消費速度。導(dǎo)致業(yè)務(wù)生效不及時。
比如,之前遇到的商品推薦,產(chǎn)品要求推薦列表中不能出現(xiàn)滿減秒殺的商品,以消除特殊商品對推薦效果產(chǎn)生影響。除了秒殺,我們還需要感知商品的上下架、黑名單、庫存等等,因次,用redis中的bit多個偏移量來維護(hù)一個商品的多個狀態(tài)。然后接收促銷組的消息來變更推薦緩存集群中的商品狀態(tài),但由于消息的延遲,就有可能導(dǎo)致商品狀態(tài)變更不及時的情況發(fā)生。不過只要權(quán)衡之下業(yè)務(wù)和技術(shù)上是可接受的就OK了。
架構(gòu)上引入不穩(wěn)定因素
消息隊列的引入,相當(dāng)于在原有的分布式服務(wù)鏈路中新增了一個系統(tǒng),系統(tǒng)復(fù)雜度也隨之變大了。同時,消息隊列的作用要求其具有高性能和高可用。
所以,面對怎樣部署高可用穩(wěn)定集群、消息發(fā)送不成功怎么重試、broker數(shù)據(jù)同步策略怎么設(shè)置、broker異常導(dǎo)致消息重發(fā)怎么冪等、消費不成功怎么重試等等問題,需要中間件團(tuán)隊和業(yè)務(wù)系統(tǒng)一起努力應(yīng)對。
Part2怎么樣?
4支撐七年雙11零故障的RocketMQ
2020 年雙十一交易峰值達(dá)到 58.3W 筆/秒。RocketMQ為了阿里的交易生態(tài)有很多深度定制,這里我們只介紹其中針對高可用的優(yōu)化。
個人理解,push消費模式只適合于消費速度遠(yuǎn)大于生產(chǎn)速度的場景,如果是大流量并發(fā)場景,基本還是以Pull消費為主。
而pull前broker和client間會進(jìn)行負(fù)載均衡建立連接,那么,一旦Client被Hang住,(沒有宕就不會rebalance,即時宕機(jī)也是默認(rèn)20s才會rebalance),就會讓broker與該client關(guān)聯(lián)的隊列消息無法及時被消費,導(dǎo)致積壓。怎么辦:POP,新的消費模式
POP 消費中并不需要rebalance去分配消費隊列,取而代之的是請求所有的 broker 獲取消息進(jìn)行消費。broker 內(nèi)部會把自身的三個隊列的消息根據(jù)一定的算法分配給等待的 POPClient。即使 PopClient 2 出現(xiàn) hang,但內(nèi)部隊列的消息也會讓 Pop Client1 和 Pop Client2 進(jìn)行消費。這樣避免了消費堆積。[1]
5快手萬億級kafka集群的平滑擴(kuò)容[2]
要實現(xiàn)平滑,則需要讓producer無感的實現(xiàn)partition遷移。
大致原理是將待遷移partition的數(shù)據(jù)和新的partition數(shù)據(jù)進(jìn)行同步并持續(xù)一段時間,直到消費者全部趕上同步的開始節(jié)點,然后再變更路由,刪除原partition,完成遷移。
相同的數(shù)據(jù)同步思路,在facebook的分布式隊列災(zāi)備方案上也有應(yīng)用。
6快手/美團(tuán)對kafka緩存污染的優(yōu)化[3]
kafka的高性能,來源于順序文件讀寫和操作系統(tǒng)緩存pagecache的支持,在單partition,單consumer的場景下,kafka表現(xiàn)的非常優(yōu)秀。但是,如果同一機(jī)器上,存在不同的partition,甚至,消費模式有實時和延遲消費的混合場景,將會出現(xiàn)PageCache資源競爭,導(dǎo)致緩存污染,影響broker的服務(wù)的處理效率。
美團(tuán)應(yīng)對實時/延遲消費緩存污染
將數(shù)據(jù)按照時間維度分布在不同的設(shè)備中,近實時部分的數(shù)據(jù)緩存在 SSD 中,這樣當(dāng)出現(xiàn) PageCache 競爭時,實時消費作業(yè)從 SSD 中讀取數(shù)據(jù),保證實時作業(yè)不會受到延遲消費作業(yè)影響
當(dāng)消費請求到達(dá) Broker 時,Broker 直接根據(jù)其維護(hù)的消息偏移量和設(shè)備的關(guān)系從對應(yīng)的設(shè)備中獲取數(shù)據(jù)并返回,并且在讀請求中并不會將 HDD 中讀取的數(shù)據(jù)回刷到 SSD,防止出現(xiàn)緩存污染。同時訪問路徑明確,不會由于 Cache Miss 而產(chǎn)生的額外訪問開銷。
快手應(yīng)對follower數(shù)據(jù)同步引起的緩存污染
broker 中引入了兩個對象:一個是 block cache;另一個是 flush queue。
Producer 的寫入請求在 broker 端首先會被以原 message 的形式寫入 flush queue 中,之后再將數(shù)據(jù)寫入到 block cache 的一個 block 中,之后整個請求就結(jié)束了。在 flush queue 中的數(shù)據(jù)會由其他線程異步地寫入到磁盤中(會經(jīng)歷 page cache 過程)。保證queue不受follower的影響
consumer 首先會從 block cache 中檢索數(shù)據(jù),如果命中,則直接返回。否則,則從磁盤讀取數(shù)據(jù)。這樣的讀取模式保障了 consumer 的 cache miss 讀并不會填充 block cache,從而避免了產(chǎn)生污染。
總結(jié)
我們可以看出,解決緩存污染的基本出發(fā)點,還是要拆解不同消費速度的任務(wù)、或不同的數(shù)據(jù)生產(chǎn)來源,分而治之的思路避免相互間緩存的影響。
7CMQ在紅包支付場景下的應(yīng)用[4]
紅包操作的背后流程簡化為:從 A 帳號中把余額讀出來,然后做減法操作,再把結(jié)果寫回 A 帳號中;然后拆紅包對 B 帳號做加法操作,把結(jié)果寫到 B 帳號中。
而由于賬務(wù)系統(tǒng)能承載的壓力有限(和賬務(wù)相關(guān)的系統(tǒng)一般都會由于鎖、事務(wù)等原因影響處理效率),可能導(dǎo)致入賬失敗,如果按實時業(yè)務(wù)邏輯,則需要對拆紅包進(jìn)行實時回滾(回滾需要對A的賬戶再進(jìn)行一次加法),而引入CMQ后,業(yè)務(wù)鏈路變成將失敗的請求寫入CMQ,由CMQ的高可用來保證數(shù)據(jù)一致,直到賬務(wù)系統(tǒng)最終入賬成功。簡化了賬務(wù)系統(tǒng)由于系統(tǒng)壓力而導(dǎo)致的入賬失敗而導(dǎo)致紅包賬務(wù)回滾帶來的額外系統(tǒng)操作。
Part3總結(jié)
本篇從消息隊列的作用出發(fā),從阿里雙11、快手、美團(tuán)、微信紅包等案例,就消息隊列本身的優(yōu)化方案和業(yè)務(wù)對消息隊列的高效利用,闡述了消息隊列在高并發(fā)的優(yōu)化場景下的作用。