馬蜂窩消息總線——面向業(yè)務(wù)的消息服務(wù)設(shè)計(jì)
我們?yōu)槭裁葱枰⒖偩€?
在消息總線上線前,馬蜂窩大部分業(yè)務(wù)中的異步需求是通過(guò) Redis 隊(duì)列來(lái)實(shí)現(xiàn)。隨著消息量增加,經(jīng)常會(huì)出現(xiàn)消息積壓、不同消息之間互相影響的問(wèn)題。為解決這些問(wèn)題,電商研發(fā)團(tuán)隊(duì)開(kāi)始規(guī)劃和設(shè)計(jì)消息總線。
為什么會(huì)有消息總線,而不是讓業(yè)務(wù)系統(tǒng)直接用 PHP 或者其他語(yǔ)言對(duì)接 RabbitMQ,Kafka 這樣的消息系統(tǒng)?
「消息總線和直接使用消息系列有什么實(shí)際的區(qū)別?」,這是很多研發(fā)同學(xué)一開(kāi)始不太理解的地方。假如只是為了用一個(gè)性能更好的消息系統(tǒng)代替 Redis,確實(shí)并不需要消息總線的這個(gè)角色。
但當(dāng)我們從實(shí)際業(yè)務(wù)角度出發(fā),對(duì)公司整體技術(shù)架構(gòu)和開(kāi)發(fā)場(chǎng)景的梳理時(shí),發(fā)現(xiàn)如果直接讓業(yè)務(wù)系統(tǒng)對(duì)接消息系統(tǒng),并不是一個(gè)很好的方式,并且至少會(huì)面臨以下問(wèn)題:
系統(tǒng)分散。各開(kāi)發(fā)團(tuán)隊(duì)需要維護(hù)各自的消息服務(wù),彼此之間相對(duì)隔離。
增加開(kāi)發(fā)難度。用戶(hù)需要關(guān)注具體消息所在消息服務(wù)的配置,關(guān)注不同業(yè)務(wù)的消息可能要對(duì)接不同種類(lèi)的消息系統(tǒng)。
維護(hù)成本高。用戶(hù)需要管理自己消費(fèi)服務(wù)的穩(wěn)定性,處理各種服務(wù)異常,保證消費(fèi)的可靠性。特別對(duì)于 PHP 來(lái)說(shuō),這個(gè)成本還是比較高的。
管理難度大。沒(méi)法對(duì)業(yè)務(wù)消息的創(chuàng)建和訂閱關(guān)系進(jìn)行統(tǒng)一管理,也不方便對(duì)業(yè)務(wù)消息中的敏感數(shù)據(jù)進(jìn)行權(quán)限管理。
不易擴(kuò)展。無(wú)法統(tǒng)一消息系統(tǒng)擴(kuò)展功能(路由、延時(shí)、重試、消費(fèi)確認(rèn)等)的使用。
總體來(lái)說(shuō),直接使用消息系統(tǒng)可以被看成是一個(gè)面向技術(shù)的接入方式;而消息總線則期望通過(guò)隱藏部署、分組和通信等細(xì)節(jié),實(shí)現(xiàn)一個(gè)面向業(yè)務(wù)的接入方式。
架構(gòu)設(shè)計(jì)和技術(shù)實(shí)現(xiàn)
1. 架構(gòu)設(shè)計(jì)
消息總線隱藏了消息發(fā)送、路由、分組、存儲(chǔ)、消費(fèi)負(fù)載、通信、高可用等一些列問(wèn)題。對(duì)使用者來(lái)說(shuō),只需要在發(fā)送端調(diào)用一個(gè) SDK 消息發(fā)送方法,在消費(fèi)端提供一個(gè) PHP 消費(fèi)方法即可。
圖1 馬蜂窩消息總線架構(gòu)設(shè)計(jì)
馬蜂窩消息總線當(dāng)前使用 RabbitMQ 作為消息引擎,在發(fā)送端提供了 SDK,作為消息總線的 Broker 角色,包含了消息路由分組的功能,負(fù)責(zé)消息的 Publish。
消息的訂閱關(guān)系,目前是持久化在 MySQL 中,在消息發(fā)送時(shí)會(huì)根據(jù)訂閱關(guān)系把消息投遞到對(duì)應(yīng)的業(yè)務(wù)消費(fèi)者。
而在消費(fèi)端,并沒(méi)有直接用 PHP 去接入 RabbitMQ,而是使用 Deliver 服務(wù)集群 (Golang 服務(wù)) 來(lái)負(fù)責(zé)把 AMQP 協(xié)議轉(zhuǎn)為 HTTP 協(xié)議,然后通過(guò) PHPService 進(jìn)行消費(fèi) PHP 代碼的執(zhí)行。
這個(gè)方案在設(shè)計(jì)時(shí),同時(shí)考慮到了未來(lái)系統(tǒng)規(guī)模擴(kuò)展后的消息分組,以及關(guān)鍵環(huán)節(jié)的可替代性。
- SDK 充當(dāng)了消息服務(wù) Broker 的角色,可以控制消息的路由、分組。未來(lái)在微服務(wù)體系中可以保持整體架構(gòu)不變,只采用其他方案實(shí)現(xiàn) Broker。
- 可以根據(jù)業(yè)務(wù)場(chǎng)景對(duì)接不同的消息引擎,比如對(duì)業(yè)務(wù)一致性要求高的業(yè)務(wù)使用 RabbitMQ,而對(duì)并發(fā)要求較高的可以使用 Kafka。對(duì)業(yè)務(wù)來(lái)說(shuō)是無(wú)感知的。
- Deliver 和 Application Service 之間可擴(kuò)展更多的通信協(xié)議,支持應(yīng)用更靈活的消費(fèi)方式,包括支持未來(lái)在微服務(wù)中的消費(fèi)服務(wù)。
2. 技術(shù)實(shí)現(xiàn)
1). 減少流轉(zhuǎn)復(fù)雜度
為了保證消息在消息總線內(nèi)各環(huán)節(jié)流轉(zhuǎn)時(shí)減少?gòu)?fù)雜度,能夠被統(tǒng)一處理,消息體被設(shè)計(jì)為統(tǒng)一的結(jié)構(gòu)。主要分為以下 3 個(gè)部分:
圖2 消息體的定義
- Parameter——參數(shù)部分包含消息 ID,來(lái)源,時(shí)間等參數(shù)。
- Conetent——消息內(nèi)容,在 PHP 中使用者可以把消費(fèi)方法的輸入?yún)?shù)放入 Content 中。
- Receiver——標(biāo)注了消息的接收者 (PHP 中為消費(fèi)者的方法)。
2). 在線服務(wù)異步
點(diǎn)對(duì)點(diǎn)模式是業(yè)務(wù)中常用的一種異步模式,
圖3 點(diǎn)對(duì)點(diǎn)消息模式
業(yè)務(wù)應(yīng)用把不需要在同步請(qǐng)求中執(zhí)行的邏輯放到異步去執(zhí)行。發(fā)送消息的業(yè)務(wù)需要明確處理消息的接收者 (消費(fèi)的 PHP 方法)。消息在發(fā)送時(shí)需要明確指定唯一的一個(gè) Receiver。
當(dāng)前通過(guò)消息總線 SDK 提供的 invoke 方法可以指定消費(fèi)的應(yīng)用方法。
3). 解耦
消息的發(fā)布訂閱模式,是一個(gè)標(biāo)準(zhǔn)的業(yè)務(wù)解耦模式。
圖4 發(fā)布訂閱(廣播)
App 1 的應(yīng)用只負(fù)責(zé)發(fā)出消息,至于什么業(yè)務(wù)需要關(guān)注,下游業(yè)務(wù)應(yīng)用自己訂閱該消息就可以。很大程度上減少了上游業(yè)務(wù)和下游業(yè)務(wù)的耦合程度和開(kāi)發(fā)調(diào)試成本。
消息總線使用 DB 來(lái)進(jìn)行消息訂閱關(guān)系的存儲(chǔ),上游業(yè)務(wù)的消息經(jīng)過(guò)消息總線 Broker 時(shí)會(huì)根據(jù)訂閱關(guān)系,裂變?yōu)?Receiver 是訂閱應(yīng)用的多條消息。這樣的消息裂變方式使消息后續(xù)在消息總線流轉(zhuǎn)時(shí)目標(biāo)明確,在進(jìn)行消費(fèi)負(fù)載,消費(fèi)確認(rèn),失敗重試等場(chǎng)景時(shí)可以按照 Receiver 進(jìn)行隔離。
同樣調(diào)用方可以使用 SDK 提供的 pub 方法進(jìn)行消息的發(fā)送,訂閱方通過(guò)消息管理系統(tǒng)進(jìn)行消息訂閱的申請(qǐng)。
4). 防消息干擾
很多使用消息總線的同學(xué)比較關(guān)心不同消息之間是否會(huì)相互干擾,比如由于某個(gè)消息短時(shí)間內(nèi)大量涌入是否會(huì)造成其他消息被阻塞。
通過(guò)前面架構(gòu)的介紹,可以看到所有的消息經(jīng)過(guò) Broker 時(shí)可以進(jìn)行路由、分組。消息總線未來(lái)會(huì)根據(jù)業(yè)務(wù)和消息量來(lái)做一些物理隔離,保障業(yè)務(wù)之間不會(huì)相互影響。
而在一個(gè)分組內(nèi),消息總線也有一些機(jī)制保障分組內(nèi)的不同消息不會(huì)相互影響。
圖5 防消息干擾機(jī)制
消息經(jīng)過(guò) Broker 默認(rèn)會(huì)進(jìn)入一個(gè) Online Queue 的隊(duì)列中,Deliver 集群中會(huì)有多個(gè) Deliver 監(jiān)聽(tīng) Online Queue。在 Deliver 服務(wù)內(nèi),通過(guò) Dispatcher 來(lái)控制消息總并發(fā)消費(fèi)量,以及同類(lèi)型消息的并發(fā)消費(fèi)量。當(dāng)某種類(lèi)型的并發(fā)消息數(shù)量超過(guò)閾值時(shí),就會(huì)被轉(zhuǎn)發(fā)到 Offline Queue,避免消費(fèi) Worker 都被同一個(gè)類(lèi)型的消息占用。而 Offline Queue 會(huì)被獨(dú)立的 Deliver 服務(wù)監(jiān)聽(tīng)進(jìn)行消費(fèi),不影響 Online Queue 的消費(fèi)。
5). 消費(fèi)服務(wù)高可用
為了保證消費(fèi)時(shí)的高可用,Deliever 群在負(fù)責(zé)進(jìn)行消費(fèi)協(xié)議轉(zhuǎn)換之外,也做了一些策略來(lái)保證消費(fèi)端的高可用。
- 熔斷
在消息一段時(shí)間內(nèi)失敗數(shù)量超過(guò)閾值時(shí),停止對(duì)隊(duì)列的消費(fèi),避免由于服務(wù)抖動(dòng)和線上故障引起的大面積消息。
- 消費(fèi)失敗
熔斷后,Deliver 服務(wù)會(huì)對(duì)后端應(yīng)用服務(wù)健康度進(jìn)行監(jiān)控,在服務(wù)恢復(fù)后可自動(dòng)恢復(fù)消費(fèi)。
- 系統(tǒng)失敗重試
消息總線服務(wù)發(fā)生故障時(shí),可對(duì)期間的失敗消息采用重試策略進(jìn)行重試,避免由于基礎(chǔ)服務(wù)問(wèn)題造成的消費(fèi)失敗。
- 業(yè)務(wù)失敗重試
在業(yè)務(wù)應(yīng)用消費(fèi)時(shí)產(chǎn)生業(yè)務(wù)異常,可在訂閱消息時(shí)指定是否進(jìn)行重試。消息總線會(huì)對(duì)需要失敗的消息按照一定的時(shí)間周期進(jìn)行多次重試。
- Graceful 重啟
Deliver 實(shí)現(xiàn)了 Graceful 重啟和退出,保障當(dāng)前正在消費(fèi)的消息都處理完成后才會(huì)進(jìn)程退出。
未來(lái)規(guī)劃
圖7 未來(lái)演進(jìn)方向
1. 產(chǎn)品化
當(dāng)前消息總線在功能上經(jīng)過(guò)近一年的迭代,已經(jīng)基本穩(wěn)定。但在消息管理,監(jiān)控,統(tǒng)計(jì)等環(huán)節(jié)對(duì)開(kāi)發(fā)者來(lái)說(shuō)還不夠友好,接下來(lái)一段時(shí)間會(huì)著重優(yōu)化系統(tǒng)的易用性。
目前已經(jīng)在規(guī)劃以下方向的優(yōu)化改進(jìn):
開(kāi)發(fā)者可以通過(guò)消息管理系統(tǒng)進(jìn)行新增消息,訂閱消息 (加入權(quán)限的審核) 等操作,代替當(dāng)前手工提 issue 的方式。
開(kāi)發(fā)者可以通過(guò)系統(tǒng)關(guān)注到自己消息的消費(fèi)情況,并及時(shí)接收到消息處理異常的報(bào)警。
完善監(jiān)控體系,提供更精細(xì)維度的系統(tǒng)監(jiān)控?cái)?shù)據(jù)。
2. 微服務(wù)
關(guān)于在微服務(wù)架構(gòu)內(nèi)提供消息總線服務(wù),也已經(jīng)在計(jì)劃當(dāng)中。包括在微服務(wù)內(nèi)進(jìn)行消息發(fā)送和使用某個(gè)微服務(wù)進(jìn)行消息的消費(fèi)。未來(lái)整個(gè)消息總線計(jì)劃會(huì)往下圖的架構(gòu)進(jìn)行演進(jìn),增加對(duì)多語(yǔ)言和不同架構(gòu)服務(wù)的支持。適應(yīng)更多的業(yè)務(wù)開(kāi)發(fā)場(chǎng)景,提供更穩(wěn)定,友好的消息總線服務(wù)。
另外對(duì)消息引擎的技術(shù)選型,未來(lái)也會(huì)考慮接入 Kafka,RocketMQ 等其他消息隊(duì)列服務(wù)。根據(jù)不同業(yè)務(wù)場(chǎng)景的消息特性,在發(fā)布時(shí)選擇進(jìn)入不同的消息隊(duì)列服務(wù)。比如對(duì)可靠性,數(shù)據(jù)安全性要求高的消息會(huì)進(jìn)入 RabbitMQ,而對(duì)高吞吐量的消息可以進(jìn)入 Kafka。但對(duì)消息的發(fā)送方和訂閱方來(lái)說(shuō)都可以不用關(guān)心這些細(xì)節(jié),仍然按照統(tǒng)一的方式進(jìn)行接入。
馬蜂窩消息總線服務(wù)當(dāng)前也在不斷迭代中,在很多地方還有不少?zèng)]有考慮到的問(wèn)題。歡迎大家多提寶貴意見(jiàn),您可以?huà)呙柘路蕉S碼訂閱「馬蜂窩技術(shù)」更多內(nèi)容。
本文作者:梁亮,馬蜂窩電商研發(fā)團(tuán)隊(duì)技術(shù)專(zhuān)家。2004 年畢業(yè)于西安郵電大學(xué),曾在新浪,開(kāi)心網(wǎng),阿里巴巴等公司工作。先后從事搜索,社交,視頻,電商等多個(gè)方向的研發(fā)工作。于 2017 年加入馬蜂窩,現(xiàn)負(fù)責(zé)馬蜂窩電商平臺(tái)服務(wù)系統(tǒng)開(kāi)發(fā)。
【本文是51CTO專(zhuān)欄作者馬蜂窩技術(shù)的原創(chuàng)文章,作者微信公眾號(hào)馬蜂窩技術(shù)(ID:mfwtech)】