從事件總線和消息隊(duì)列說起
Jusfr 原創(chuàng),轉(zhuǎn)載請(qǐng)注明來源
系列目錄
- Chuye.Kafka: 從事件總線和消息隊(duì)列說起
- Chuye.Kafka: 清晰精練的 .Net driver for Apache Kafka
- 協(xié)議部分: TopicMetadata
- [協(xié)議部分: Produce/Fetch/MessageSet] planned
- [Chuye.Kafka: BufferWriter/BufferReader] planned
- [協(xié)議部分: Offset/OffsetCommit/OffsetFetch] planned
事件總線(EventBus)及其演進(jìn)過程必須提到內(nèi)存模型、傳統(tǒng)的隊(duì)列模型、發(fā)布-訂閱模型。
- 內(nèi)存模型:進(jìn)程內(nèi)模型,事件總線(EventBus)在內(nèi)部遍歷消費(fèi)者(Consumer)列表傳遞數(shù)據(jù);
- 隊(duì)列模型:消息或事件持久化到傳統(tǒng)消息隊(duì)列(Queue)即返回,以實(shí)時(shí)性降低換取吞吐能力提升;
- 發(fā)布-訂閱模型:事件源(EventSource)得到強(qiáng)化,出現(xiàn)如分布式、持久化、消費(fèi)復(fù)制/分區(qū)等特性;
文中使用了“術(shù)語(單詞)”的形式引入概念,用詞可能有差異,只是力求表義清楚,下文描述將直接使用單詞。
內(nèi)存模型
內(nèi)存模型可以很好地解耦,舉例來說,版本初期我們有 IUserService 負(fù)責(zé)用戶創(chuàng)建,邏輯如下:
interface IUserService {
void CreateNewUser(String name);
}
class UserService1 : IUserService {
public void CreateNewUser(String name) {
Console.WriteLine("User \"{0}\" created", name);
}
}
現(xiàn)在希望在用戶創(chuàng)建后,進(jìn)行一次消息服務(wù)調(diào)用,發(fā)送歡迎辭。為了解決這個(gè)需求,需要添加和實(shí)現(xiàn)新的 MessageService , 并添加依賴,在 CreateNewUser() 方法某入插入調(diào)用邏輯,于是代碼變這樣:
interface IMessageService {
void NotifyWelcome(User user);
}
class UserService2 : IUserService {
private readonly IMessageService _messageService;
public UserService2(IMessageService messageService) {
_messageService = messageService;
}
public void CreateNewUser(String name) {
var user = new User { Name = name };
Console.WriteLine("User \"{0}\" created", user.Name);
_messageService.NotifyWelcome(user); //添加消息服務(wù)調(diào)用
}
}
目前看起來好像沒啥問題,因?yàn)榇a簡單,但是當(dāng)邏輯越來越復(fù)雜時(shí)情況就變得不一樣了,比如我們希望用戶創(chuàng)建后將數(shù)據(jù)寫入索引,需要依賴 ISearchService;比如希望調(diào)用報(bào)表服務(wù) IReportService 添加每日新增用戶數(shù);
public void CreateNewUser(String name) {
var user = new User { Name = name };
Console.WriteLine("User \"{0}\" created", user.Name);
_messageService.NotifyWelcome(user); //添加消息服務(wù)調(diào)用
_searchService.SaveIndex(user) //搜索服務(wù)調(diào)用
_reportService.CounterNewUser(user); //報(bào)表服務(wù)調(diào)用;
}
<center>更多的依賴</center>
如此多的依賴實(shí)在時(shí)重負(fù)難堪,當(dāng)然你可以說這些應(yīng)該異步處理、應(yīng)該放到后端隊(duì)列,沒錯(cuò)。現(xiàn)實(shí)中需要同步處理的邏輯并不少見,而規(guī)模尚小時(shí)引入隊(duì)列將帶來額外的開發(fā)測試、部署監(jiān)控成本。使用 EventBus 的內(nèi)存模型可以比較優(yōu)雅地處理此問題,以下是實(shí)現(xiàn)思路。
場景和實(shí)現(xiàn)思路
引入 EventBus 作為共同依賴,IUserService 視為生產(chǎn)者,IMessageService 視為對(duì)用戶創(chuàng)建事件感興趣的 Consumer ,其消費(fèi)邏輯調(diào)用 NotifyWelcome() 方法。EventBus 內(nèi)部維護(hù)了一份 EventType-Consumer 列表,遍歷列表分發(fā) Event 實(shí)例;ISearchService 、IReportService 等類似,同樣注冊(cè)到 EventBus 內(nèi)即可。
abstract class Event {
}
interface IConsumer {
void Proceed(Event @event);
}
class EventBus {
private readonly HashSet<IConsumer> _consumers = new HashSet<IConsumer>();
//... 更多細(xì)節(jié)
public void Publish(Event @event) {
foreach (var consumer in _consumers) {
consumer.Proceed(@event);
}
}
}
class UserService3 : IUserService {
private readonly EventBus _eventBus;
public UserService3(EventBus eventBus) {
_eventBus = eventBus;
}
public void CreateNewUser(String name) {
var user = new User { Name = name };
Console.WriteLine("User \"{0}\" created", user.Name);
var @event = ... //創(chuàng)建消息
_eventBus.Publish(@event); //交由 EventBus 發(fā)布
}
}
<center>依賴關(guān)系的轉(zhuǎn)變</center>
在此過程中,Consumer 并不知道誰創(chuàng)建了 Event,不同的 Producer 對(duì)各 Consumer 的依賴統(tǒng)一變更為對(duì) EventBus 的依賴,內(nèi)存模型達(dá)到了解耦目的。
隊(duì)列模型
在內(nèi)存模型的場景中,我們確認(rèn)這些業(yè)務(wù)需要由異步進(jìn)程處理。從 MSMQ 到各種第3方實(shí)現(xiàn)方案眾多,但真實(shí)業(yè)務(wù)中 while(true) 循環(huán)有太多問題,比較棘手的像
- 異常處理:消息處理中發(fā)生異常,但短時(shí)間內(nèi)重試可能解決不了問題;
- 多消費(fèi)者:大家都有消費(fèi)程序,可能監(jiān)聽相同隊(duì)列;
對(duì)于異常,常規(guī)做法是使用監(jiān)聽時(shí)間依次延長的多個(gè)異常隊(duì)列,定時(shí)檢查并出隊(duì)處理;
多消費(fèi)者麻煩一點(diǎn),由于傳統(tǒng)隊(duì)列出隊(duì)即消息的特性,這意味著要么數(shù)據(jù)寫多份大家各自消費(fèi),要么消費(fèi)者集中管理遍歷調(diào)用。
<center>Queue 與 EventBus 協(xié)同工作</center>
- 異常隊(duì)列誰來監(jiān)聽和分發(fā)?
- 如果數(shù)據(jù)寫多份,生產(chǎn)者如何得知消費(fèi)者數(shù)量?寫入性能損失怎樣?動(dòng)態(tài)添加消費(fèi)者時(shí)怎么辦?消費(fèi)者又如何路由到自己的隊(duì)列上?
- 果數(shù)據(jù)寫一份,消費(fèi)者同步調(diào)用還是異步調(diào)用?等待所有的消費(fèi)邏輯完成既可能存在短板,某消費(fèi)者出現(xiàn)異常時(shí)又如何進(jìn)行進(jìn)度區(qū)分?
發(fā)布-訂閱模型及各 EventSource 的諸多特性提供了解決思路。
發(fā)布-訂閱模型
本文是 Kafka 系列文章之一,故使用 Kafka 作為 EventSource 描述和參考,其他隊(duì)列并未過多涉及請(qǐng)有限參考。
隊(duì)列模型雖然存在許多問題,但應(yīng)用與業(yè)務(wù)規(guī)模并不龐大時(shí)仍可一用。我們可以使用宿主代為監(jiān)聽列隊(duì)和消息分發(fā)、插件式寄宿消費(fèi)程序,使消費(fèi)者可以專注于業(yè)務(wù);由于消費(fèi)者短板效應(yīng)無法避免,可以在業(yè)務(wù)層面妥協(xié),盡量聚合高效、有限的消費(fèi)者等等。
在應(yīng)用與業(yè)務(wù)繼續(xù)擴(kuò)展時(shí),發(fā)布訂閱模型的事件總線變得不可或缺,甚至流式處理框架也不可避免地提上日程,使用 Kafka 對(duì)前文問題作出解答。
- Kafka 基于文件系統(tǒng),消息移除是基于時(shí)間和磁盤的策略,并不會(huì)輕易丟失數(shù)據(jù),消費(fèi)者出現(xiàn)異常也不用擔(dān)心;
- Kafka 將 Consumer 的當(dāng)前位置的管理職責(zé)交由消費(fèi)者負(fù)責(zé),只是提供了可選的 OffsetCommit 和 OffsetFetch API,這帶來了極大的便利性和一定的復(fù)雜度;你可以從任何位置開始消費(fèi),也沒有重復(fù)消費(fèi)限制,附加的是需要合適的 Offset 策略;
- Kafka 提供了 Topic Partition + Consumer Group 并定義了發(fā)布-訂閱語義,可以配合堵塞式 API 保障消息處理的低延遲。
關(guān)于推與拉
Kafka 遵循傳統(tǒng)的 Pull 模式,由消費(fèi)者決定數(shù)據(jù)流速,畢竟寫入速率遠(yuǎn)高于消費(fèi)的情況下,消費(fèi)者實(shí)際是處于過載狀態(tài)。個(gè)人的理解的推拉(Push/Pull 或 Publish/Subscribe)并不是主要差異而只是受制于事件源(EventSource)的實(shí)現(xiàn)細(xì)節(jié)。
關(guān)于 Chuye.Kafka
Chuye.Kafka 是 Kafka 0.9版本 API 的 .NET 實(shí)現(xiàn),其 Consumer、Producer 是 low levl API 的輕度封裝,使用它實(shí)現(xiàn) EventBus 并沒有過多障礙,消費(fèi)者分組管理、狀態(tài)監(jiān)控和異常策略才是重點(diǎn)。
Jusfr 原創(chuàng),轉(zhuǎn)載請(qǐng)注明來源