純異步事件驅(qū)動構(gòu)建的微服務(wù)體系
奇葩的需求
通常我們都只會記錄最后一次變更的狀態(tài),因為最終狀態(tài)是真實時間多數(shù)的需求。例如修改了用戶名,只會記錄最后修改的用戶名,通常還會加上修改時間和創(chuàng)建時間,就是我們多數(shù)常見系統(tǒng)的設(shè)計模式。 如果需求增加了:“用戶名不能和曾經(jīng)使用過的用戶名一致”,問題就來了。
如果不用常見的設(shè)計方案,會怎樣
修改用戶名,這個操作就是一次事件。事件是關(guān)于“發(fā)生了什么”的記錄,類似日志。但是與日志不同的是,事件還是事實的單一來源。因此,它們必須包含準(zhǔn)確描述所發(fā)生事件所需的所有信息。
事件驅(qū)動的解決方案
存儲
那如果我們不進(jìn)行update,直接存儲了原始完整的事件,也就是只記錄了日志,會怎樣? 數(shù)據(jù)庫里會有多條記錄,每次查詢都必須使用max(版本)取得最后記錄后才能拿到最準(zhǔn)確的信息。性能上不夠好,且產(chǎn)生了大量的冗余信息。 那如果不考慮存儲成本,也不考慮事務(wù),且有一種物化視圖,能夠全自動的從所有事件中自動取得最新版本,是不是就是最理想的狀態(tài)? 類似區(qū)塊鏈的運(yùn)行原理,沒有刪除和修改,只有新增,所有發(fā)生的事件都被完整存儲,測試、解耦、伸縮變得so easy
調(diào)用
如果我們向mq發(fā)出一個事件,但等待對此事件的一個回信(可能從另一個topic中),不考慮性能的情況下,本質(zhì)上來說就是一個普通調(diào)用了。 不需要服務(wù)發(fā)現(xiàn)也不需要負(fù)載均衡,每個調(diào)用都被完整記錄,且請求或返回數(shù)據(jù)的共享變得so easy。
同步調(diào)用的缺陷
解耦。無論是常見的服務(wù)發(fā)現(xiàn)還是dubbo式能力發(fā)現(xiàn),必須在知道被調(diào)用方的前提下才能執(zhí)行調(diào)用。至少需要知道被調(diào)用方的名稱和請求對象,才能準(zhǔn)確執(zhí)行調(diào)用。 這樣不夠“聲明式”,不夠解耦。
彈性。今天我們在k8s這樣的平臺上,似乎很少遇到彈性問題。深入細(xì)節(jié),長連接中的高qps依然無法分流,特別是在特殊協(xié)議中這種情況可能雪上加霜。
線上故障。沒打訪問日志?哭死。打了訪問日志?一個月激增10倍的日志成本
接口版本。多個 API 定義和服務(wù)版本通常需要同時存在。強(qiáng)制讓客戶端升級到最新的 API 并不總是可行或可取的。
測試。單元測試改動了數(shù)據(jù)怎么辦?基建層要不要做單元測試?
基于純異步事件驅(qū)動構(gòu)建的微服務(wù)體系
事件應(yīng)該是什么樣
準(zhǔn)確的描述是:包含唯一id的kv結(jié)構(gòu)數(shù)據(jù)
最佳實踐的格式:使用protobuf或apache avro,保持足夠的體積優(yōu)勢、反解析優(yōu)勢,利用現(xiàn)有的基建(例如proto工程化能力)
事件的設(shè)計
錯誤的設(shè)計:用戶名正在更新
正確的設(shè)計:用戶名更新為xxx 事件不能是處于中間狀態(tài),而且已經(jīng)完成,成為事實的事情。
怎樣定義“類型” 錯誤的設(shè)計:
id: 1 name: xxx admin_name: "" type: user
正確的設(shè)計:
id: 1 name: xxx --- id: 2 admin_name: xxx
不要使不同事件“合用”定義,如果是兩個類型,就定義2個事件。事件不需要和數(shù)據(jù)庫設(shè)計對等 3. 最小化事件 拿修改用戶名的事件舉例,事件中當(dāng)然需要包含用戶id和用戶名,但需要不需要包含用戶頭像等其他有關(guān)聯(lián)但并不是“有用的信息”?站在最佳實踐的角度上,不要包含此類信息。不僅會造成數(shù)據(jù)量變大,而且實際上也要校驗每次的“更新”是否對原始數(shù)據(jù)有修改,造成了額外的負(fù)擔(dān)
技術(shù)和中間件
mq。常見的mq都可以完成。但是kafka這種有各種限制(消費(fèi)者數(shù)目不能大于分區(qū)數(shù)等),可能會導(dǎo)致整體服務(wù)性能或彈性能力不足。
落盤。根據(jù)數(shù)據(jù)類型的不同,有的數(shù)據(jù)需要實時落盤+事務(wù)做聚會,這種情況需要開發(fā)。如果不需要實時,比如”用戶名不能和曾經(jīng)使用過的用戶名一致“的場景,完全可以使用某些”奇葩“的方案,包括但不限于:
使用kafka connect把數(shù)據(jù)按照parquet格式寫入對象存儲,使用presto加載parquest進(jìn)行查詢。
使用帶有kafka抽取的數(shù)據(jù)庫(例如singlestore),直接抽取kafka數(shù)據(jù),自動落盤
老系統(tǒng)怎么辦
觸發(fā)器。使用觸發(fā)器拿到最新數(shù)據(jù),并發(fā)布在流中
數(shù)據(jù)庫日志。訂閱binlog并推送在流中,可能需要做相應(yīng)的轉(zhuǎn)化,因為一般數(shù)據(jù)庫的數(shù)據(jù)都有一定冗余,比如上面說的頭像信息
orm攔截器。代碼層面的變更通知
定時掃描update_at。使用定時任務(wù)獲取變更事件并推送
有了事件流,然后呢
困擾多年的問題突然沒有了
mysql如何同步redis
mysql如何同步es
需求變化快?我變的更快。甚至無需修改原有代碼,訂閱事件結(jié)果,并直接使用事件覆蓋上一個事件,可以快速滿足需求,代碼屎山也可以輕松重構(gòu)。
海量數(shù)據(jù)+事務(wù)。打破傳統(tǒng)tp和ap天然的邊界(可能是兩個老死不相往來的部門),研發(fā)的思路可以不用總是局限在加索引這件事上
結(jié)尾
對于“傳統(tǒng)”研發(fā)團(tuán)隊而言,以純事件驅(qū)動架構(gòu)改造微服務(wù)是個過大的調(diào)整,會打破現(xiàn)有的多數(shù)認(rèn)知。我個人猜想敢于嘗試這只螃蟹的肯定不多。
但是有了這種打破常規(guī)的思路,在某些問題上可能就不那么會受到限制。對于某些“惡臭”的傳統(tǒng)設(shè)計,是否能夠在某些程度的突破一下邊界。在某些特定的、非核心的系統(tǒng)中,可以嘗試下使用新模式解決問題。

更多精彩內(nèi)容
研發(fā)團(tuán)隊新鮮事兒,來公眾號「迷路idea」找我一起探討