作者 | 林暉
一、業務背景
電商平臺供應鏈的業務場景非常復雜,技術中臺需要支持非常復雜且不斷變化的業務需求,構建了數量繁多且緊密耦合的業務鏈路,為技術架構的維護帶來了壓力。
1. 問題描述
上圖是一個典型的業務架構,A域是上游域,B域和C域是下游域。A域在收到外部調用請求時,首先同步調用B域的服務接口完成同步業務邏輯,然后發送消息通知到MQ。C域異步消費消息后,反向調用A域的接口查詢詳細信息,完成異步業務邏輯。
這種架構的問題包括:
(1) A域強依賴B域的接口,B域接口變動會導致A域調用失敗,而A域無法管控B域的接口變動;
(2). C域收到消息后需要反查A域的接口,對A域形成了雙重依賴,A域接口和消息格式的任何變動及不穩定性都會影響C域;
(3) A域的消息和接口都是瞬時數據,兩者由于時間差可能不一致,增加了C域處理的復雜度(例如:C域收到的消息是單據已創建,調用接口時查到該單據已完結);
(4) A域需要保證同步調用和消息通知的一致性,包括MQ不可用等情況發生時的容災處理面對這些問題,我們希望應用事件驅動架構的特性來解耦子域,降低業務鏈路復雜度,構建穩定并向前兼容的事件契約,從而提升全域的穩定性。
2. 事件驅動架構的應用過程
(1)重新梳理全鏈路業務流和業務活動,建立統一的標準語言;
(2)定義標準的事件格式和通用基礎字段;
(3) 各域定義包含完整業務語義、自閉包、多租戶的領域事件;
(4) 開發并接入一套適應供應鏈業務特點的事件系統(NBF事件中心);
3. 關于NBF
NBF[1] 是阿里巴巴供應鏈中臺的基礎技術團隊打造的一個技術PaaS平臺,全稱是New-Retail Business Factory,她提供了微服務FaaS框架,低代碼平臺和中臺基礎設施等一系列的PaaS產品,旨在幫助業務伙伴快速復用和擴展中臺能力,提升研發效能和對外的商業化輸出。事件中心就是NBF系列技術產品中的一員。
本文首先介紹事件驅動架構的概念及適用場景,然后會介紹事件中心產品的設計和實現。
二、什么是事件驅動架構(EDA)
1. 領域事件
很多同學會將事件和消息混淆。在業務系統中,事件指的是領域事件,而消息可以是任意數據或數據片段。領域事件的特點包括:
(1)與服務接口一樣有完整的schema,并保證schema向前兼容;
(2)是業務流程的一部分,由業務動作觸發,包含了完整(或部分但有獨立語義)的業務狀態變化;
(3)事件消費者接收到事件后,相應修改自身的業務狀態,并按需發出新的事件;消費者需要保證所有事件最終消費成功,否則會導致業務流程不完整;
(4)事件需要持久化保存并長期歸檔,方便業務同學查詢、恢復中斷的業務流程、重新發起業務流程等,也方便風控及財務分析同學做離線分析。
2. 事件驅動架構的概念
和很多架構名詞類似,事件驅動架構并沒有一個明確的定義和能力范圍。Martin Fowler在2017年的文章[2] 中描述了與事件驅動架構相關的一些主要模式。在本文中,事件驅動架構的概念具象為由領域事件驅動的業務流技術架構。每一個領域事件都對應一個業務流中的具體活動(如采購單建單),而事件就是活動發生導致的結果(如采購單建單完成事件),事件內容就是活動導致的完整狀態變化(如采購單+子單列表)。
3. 事件驅動架構的優點
在Fundamentals of Software Architecture[3] 以及Microservices Patterns[4]等書中描述了事件驅動架構的一些明顯特點,我們總結為以下幾項:
- 高度解耦
- 廣播能力
- 純異步調用(Fire and Forget)
- 靈活擴展
- 高處理性能
4. 事件驅動架構能解決什么實際問題
下面我們舉幾個例子來描述事件驅動架構的解耦和廣播能力如何幫助解決現實工作中的問題:
解耦能力
在基于請求/響應方式的服務化架構中,上游服務按照約定的RPC接口調用下游服務,這樣有一個比較嚴重的問題:上游服務作為數據(例如業務單據)的生產者,強依賴了作為數據消費方的下游服務所定義的接口,導致上游服務自身無法沉淀接口和數據標準。
一種更合理的方案是依賴倒置:由上游服務定義SPI,下游服務實現SPI,這樣,上游服務終于有機會沉淀出自身的接口和數據標準,不再需要適配各個下游服務的接口,而是由下游服務的開發者按照接口文檔來做實現。但這種設計仍然無法解決運行時上游服務仍然依賴下游服務的問題,下游服務的可用性、一致性、冪等性能力會直接影響上游服務的相關指標及實現方式,需要上下游服務開發者一起對齊方案,在出問題時一起解決。
使用事件驅動設計可以實現契約定義和運行時的全面解耦:上游服務可以沉淀自己的事件契約,在運行時無論是上游服務還是下游服務都只依賴事件Broker,下游服務的可用性和一致性等問題由事件Broker來保障。
廣播能力
在供應鏈中臺這樣復雜的微服務架構中,關鍵的上游服務往往有多個下游服務,上游服務一般需要順序或并發調用所有的下游服務來完成一次完整的調用。
上游服務的開發者會面臨多個難題:
- 服務的可用性會被下游服務影響;
- 服務的RT自己無法控制;
- 下游服務之間的一致性如何保障;
- 如何實現一套可靠的重試機制;
而下游服務的開發者也有自己的問題:
- 每接入一個上游服務都需要跟服務開發者排期:誰來答疑,什么時候聯調,什么時候上線;
- 上游流量如何做過濾,高峰流量是否能抗得住;
- 如何滿足上游服務的可用性及RT要求;
使用事件驅動架構天然可以避免上述問題:
- 上下游完全解耦,上游服務只要保證將事件成功發送到Broker,無論有幾個下游消費者,都不會影響自身的RT,也不需要考慮下游服務之間的一致性;
- 下游服務在接入新的事件時,只需要在事件管理服務中走完訂閱審批流,不需要等待事件發布者排期和聯調;
- 通過事件Broker提供的事件過濾能力,下游服務只需要消費與自身相關的事件流量(例如:天貓超市的計費服務只需要消費tenantId為天貓超市的采購單創建事件,而不需要消費銀泰租戶的采購單創建事件);
- 通過事件Broker提供的事件存儲能力和重投能力,即使上游服務發送的事件流量超過了下游服務的處理能力,也只會影響下游服務的消費延遲,不會導致大量請求失敗的情況。
5. 事件驅動架構不適合什么場景
- 強依賴Response的場景,例如單據查詢、商品查詢;
- 對全局處理延遲敏感的場景,例如游戲、搜索;
- 要求服務之間保持強一致性的場景;
三、事件中心的功能設計
作為面向中臺的事件中間件,事件中心集成了消息中間件MetaQ(RocketMQ),初始使用體感也與MQ很像,但事件中心有很多不同的功能設計:
(1)完善的權限控制;
(2) 支持事件契約定義以及運行時合法性校驗;
(3) 支持大事件發送和消費(10MB或更高);
(4)支持長期的事件歷史查詢、事件索引查詢(如單據編號、sku)、事件重投;
(5) 支持消費周期很長的事件(如需要幾個月才能完結的入庫單);
(6)所有事件及消費記錄的完整歸檔;
(7)以OpenAPI的形式開放了事件查詢、事件重投等運維態的功能,方便被其他系統集成。
四、事件中心的運行時架構
事件中心運行態主要由以下部分組成:
- 事件中心服務/SDK
a) SDK:包含事件收發的主要邏輯,支持事務發送和普通發送,支持事件校驗、壓縮、本地備份;
b) Tunnel Service:一層很薄的數據庫代理服務,支持按應用、事件、場景、IO維度的限流,支持數據庫快速靈活擴容;
c) Index Service:事件索引服務,通過精衛(DataX)獲取Binlog,解析為索引后寫入索引表(Lindorm)。
- 阿里中間件
a) Diamond(Nacos):包含應用相關的全部配置信息,如發送、訂閱關系、事件定義、中間件配置等;
b) SchedulerX:調度SDK執行事件重新發送、重新消費、事務異常狀態問詢;
c) MetaQ:主要的事件收發管道;
d) TDDL(RDS):事件內容及消費記錄存儲;
e) 精衛:用于生成索引、計算延遲等異步處理邏輯;
f) Lindrom(serverless):用于存放事件外部索引,serverless模式支持按量付費和彈性擴容,性能比較穩定。
下圖為簡化的運行時架構圖,圖中藍色線條表示事件的正常收發鏈路(事務發送),紅色線條表示事件的異常處理鏈路。
1. 事件發送與消費流程
事件結構
運行時的一條事件實例由三部分組成:
(1)事件ID:全局唯一,格式為“邏輯庫編號_月內發送日期_uuid”,例如01_11_f75ec4fb347c49c4bc3e93xxxxxxxx,其中邏輯庫編號用于邏輯庫路由,日期用于事件清理;
(2) 事件Head:包含事件元信息,如trace信息、發送者信息、事件大小、MetaQ信息等,參考示例:
(3)事件Body:JSON格式,包含由用戶已定義的事件內容,事件內容要符合事件定義契約,否則會被拒絕發送。
運行時的事件可能有多個消費方,每個消費方會產生一條消費記錄,消費記錄包含:
- 事件ID
- 消費信息:消費狀態、消費次數、下次消費時間等
事件發送流程
事件中心支持事務發送和非事務發送兩種模式,使用狀態機驅動,API設計與MetaQ的API基本一致。以下以事務發送為例介紹發送流程,由于非事務發送的流程更簡單,所以不再詳細介紹。
1)事務發送狀態機
2)事務發送時序圖
3)異常狀態事務問詢
事件消費流程
事件消費流程也使用狀態機驅動,API相比MetaQ有一些不同:
(1)不需要再調用subscribe topic;
(2)新增消費過濾器EventFilter,支持按照租戶、業務流、事件維度做過濾;
(3)支持不同的事件使用不同的Listener消費;
1)事件消費狀態機
2)重試周期
事件進入消費失敗狀態后,事件中心會周期調用用戶Listener重新消費,消費周期以5s起始指數增加,最多重試15次,最大為5 * 214 = 81920秒(約22小時)。
3)事件消費時序圖
2 事件存儲
數據表
事件中心使用了32分庫的TDDL,按照HASH(事件ID)做分庫,每個庫上有以下幾張表:
(1)事件主表,包含發送者信息、事件信息以及普通事件的事件體;
(2) 事件消費記錄主表,包含消費者信息、消費狀態以及重新消費信息,與事件主表通過事件ID關聯;
(3)大事件主表,包含大事件體,與事件主表通過事件ID關聯;
(4) 事件天表,表結構與事件主表相同,存放消費完畢的事件;
(5)消費記錄天表;
(6)大事件天表;
事件生命周期
(1)新寫入的事件和消費記錄會進入主表;
(2)當事件寫入超過1天,且事件的所有消費方都消費成功后,事件及所有消費記錄會從主表移動到天表中;
(3)當事件某個消費方需要重新消費之前消費成功的事件時,事件及所有消費記錄會從天表移回到主表中;
(4) 每天的某個時間,事件清理服務會將7天前的那張天表清空,例如今天是2月11號,那么就會清空2月4號的所有天表。
3. 外部索引
事件發送歷史列表、事件索引查詢和事件重投是事件中心運維平臺的主要功能。其中索引查詢功能的查詢速度快、查詢結果準確,用戶反饋一直比較好。
索引配置
用戶在修改事件定義時,可以為其中任意基礎類型字段配置為“查詢字段”,事件中心會在運行時解析該字段的值,并創建索引;一個事件中的每個查詢字段都會對應一條索引;即使沒有配置查詢字段,也會生成一條包含時間戳的索引,用于已發送事件的排序和分頁。
索引結構
事件中心的索引為KV結構,使用Lindorm的寬表存儲,按使用場景分為兩種類型:
(1)不包含查詢字段的索引;
(2)Key格式為 HASH(租戶id_事件code)_env_發送時間差值_事件ID;
(3)Value為事件ID、事件頭;
(4)包含查詢字段的索引;
(5)Key格式為 HASH(租戶id_事件Code_字段路徑_索引值)_env_發送時間差值_事件ID;
(6) Value為事件ID、事件頭;
其中
(1)發送時間差值 = Long.MAX_VALUE - 發送時間毫秒數,用于按發送時間倒序展示;
(2)字段路徑是json path格式,例如 $.bizNo;
查詢性能
通過目前事件中心運維平臺99%的查詢都可以在毫秒級別返回結果,Lindorm索引行數在十億級別。
五、總結
本文介紹了事件驅動架構在供應鏈執行鏈路的應用背景和實踐過程,并介紹了NBF事件中心產品的設計和部分實現。目前事件中心每日事件發送量峰值在千萬級別,平穩度過了雙11、雙12、年貨節等流量高峰。
參考鏈接:
[1]https://www.infoq.cn/video/xXxlmqhTH5owSDRSx52p
[2]https://martinfowler.com/articles/201701-event-driven.html
[3]https://book.douban.com/subject/34464806/
[4]https://book.douban.com/subject/26989027/