正如我在之前的文章中提到那樣,微服務(wù)是怎樣失敗的,調(diào)試一個分布式系統(tǒng)是一項(xiàng)具有挑戰(zhàn)的任務(wù)。 許多東西可能是錯的并且是不可控的, 例如網(wǎng)絡(luò)的不穩(wěn)定性,臨時(shí)不可用或者是一些外部的BUG。
用一些工具監(jiān)控網(wǎng)絡(luò)能被快速解決(像Service Mesh ),你也可以使用一些額外的工具像OpenTracing 來做分布式的日記記錄. 但是當(dāng)我們談到理解我們的實(shí)體狀態(tài)時(shí),并沒有快速的即插即用的框架。
你的數(shù)據(jù)可能比你代碼存活的時(shí)間還要長, 然而我們卻忽略了我們的數(shù)據(jù)隨著時(shí)間推移而產(chǎn)生的變化。 在大多數(shù)系統(tǒng)中,即使是簡單的問題, 例如“該實(shí)體如何達(dá)到這種狀態(tài)?” 或 “一個月前我的狀態(tài)是怎樣的?” 都無法回答,因?yàn)闆]有保存任何變更的歷史記錄。 持續(xù)追蹤這些變更狀態(tài)對一個系統(tǒng)的健康是極其重要的, 不僅是為了安全或者是調(diào)試目的, 而是出于巨大的商業(yè)價(jià)值(你的產(chǎn)品負(fù)責(zé)人會很高興)
解決方案
通過事件溯源|事件記錄為服務(wù)行為增加可見性是一種很好的方式。
一個比較好的辦法是。這個有10年之久的基本概念是讓一個應(yīng)用的每一次變更都應(yīng)該被記錄在一個事件對象并且被有序存儲。
如果這聽起來很熟悉,那可能是因?yàn)槿魏伟姹镜目刂葡到y(tǒng)或數(shù)據(jù)庫事務(wù)日志都是這種模式的重度用戶。
讓我們來深入理解它是怎么工作的。 假設(shè)我們正在為一個電商網(wǎng)站構(gòu)建一個訂單服務(wù)(Order Service) , 讓我們看一下我們的應(yīng)用狀態(tài)和事件看起來是怎樣的:
?
許多作者對于事件的溯源和記錄都定義了三個主要的規(guī)則:
事件總是不可變的;
事件總是那些過去已經(jīng)發(fā)生過的事。 一些開發(fā)者錯誤指令(例如: PlaceOrder) 事件(ex: OrderPlaced)
理論上, 在任意時(shí)間點(diǎn),你可以刪除你當(dāng)前的狀態(tài)并且通過重新處理所接收到全部消息來重新構(gòu)建你的整個系統(tǒng)。
事件溯源| 事件記錄流
消息接器: 負(fù)責(zé)將傳入的請求轉(zhuǎn)換為事件并且校驗(yàn)他們;
事件存儲: 負(fù)責(zé)有序存儲這些事件并且通知監(jiān)聽器;
事件監(jiān)聽器:正如你可能猜到的, 它負(fù)責(zé)根據(jù)每一個事件類型來執(zhí)行對應(yīng)的業(yè)務(wù)邏輯
這種模式有很多種實(shí)現(xiàn)方式, 在Couchbase5.5中使用的“事件服務(wù)”就是這種模式的實(shí)現(xiàn)之一。 總而言之, 它允許你編寫函數(shù), 在一個文檔被插入/更新/刪除時(shí)來觸發(fā)這些函數(shù)。 這個事件機(jī)制也能讓你生成curl 請求,因此無論何時(shí)將給定的文檔存儲在數(shù)據(jù)庫中,都可以在應(yīng)用程序中觸發(fā)一個endpoint來處理它。 讓我們看一下它是如何使用事件的:
?
Couchbase Eventing 是異步的, 所以上述套件實(shí)現(xiàn)僅適用于你的應(yīng)用只接收異步調(diào)用的情況。 它也可以用來充當(dāng)額外的安全層以觸發(fā)通知, 例如,如果有人試圖手動更新一個事件。
在某些系統(tǒng)中, 事件的字段和結(jié)構(gòu)可能有很大的不同, 將這些事件存儲在一個固定結(jié)構(gòu)的RDBM 中是很難建模的, 出于這個原因, 開發(fā)人員通常將它他們用一個varchar類型的字段將這些事件存儲為一個json字符串。這種辦法存在一個主要的問題: 它使得事件查找變得困難,使你的大部分查詢變慢, 變復(fù)雜并且充拆著大量的類似'likes'操作。 其中一種可有的解決方案是使用文檔數(shù)據(jù)庫, 因?yàn)樗鼈兇蠖鄶?shù)將文檔存儲為json并且具有用于查詢它的類似SQL的語言, 如N1QL[1]。
快照-對你的狀態(tài)進(jìn)行版本控制
事件溯源世界中添加版本控制/歷史記錄被稱為快照。 當(dāng)你想要想要知道N天以前的狀態(tài)是什么樣的, 可以避免你重新處理所有的事件。 當(dāng)你需要快速識別在某時(shí)間點(diǎn)時(shí)應(yīng)用的狀態(tài)與處理一個事件之后的所預(yù)期的狀態(tài)之間的差異。
快照具功能十分好用,成本低,易實(shí)現(xiàn)并且非常適合實(shí)時(shí)的上報(bào)。 如果你決定實(shí)現(xiàn)一個Event Sourcing, 可以在實(shí)現(xiàn)快照中多投入一點(diǎn)努力。
?
這部份是你的所有的努力得到回報(bào)的地方。 一旦你在這個地方有了事件溯源/記錄并且具有快照功能, 你就可以使用Retroactive Event 模式的來修復(fù)不一致的情況。
我總結(jié)一下, 如果你修復(fù)了一個BUG并且現(xiàn)在需要調(diào)整被影響實(shí)體的狀態(tài), 而不是手動更新他, 你可以將你的實(shí)體的狀態(tài)設(shè)置為BUG之前的狀態(tài),并且從那個時(shí)刻重放所以與之關(guān)聯(lián)的事件。 無須手動就會自動修改你的狀態(tài)。
?
回滾狀態(tài): 回滾一個實(shí)體的到這個BUG之前的狀態(tài)。 你能避免第一步和第二步重放所有的事件。 然而在這種情況下,我們正恢復(fù)以前的狀態(tài),因?yàn)槲覀兿M苊庵匦绿幚碚麄€過程。
忽略快照: 所有恢復(fù)后的快照都應(yīng)該標(biāo)記為忽略,以避免將來恢復(fù)不一致的快照。
重新構(gòu)建事件: 從目標(biāo)之后重建所有事件。
但是,如果事件中有錯誤數(shù)據(jù)或者從來沒有被觸發(fā)過,該怎么辦?我們可以更新或刪除事件并重新處理整個事件嗎?
如果你還記得,事件溯源的第一條規(guī)則是“事件永遠(yuǎn)是不變的”,這是一個很好的理由;你需要相信你所看到的日志。但它不能回答我們的問題;只需略微修改一下:我們?nèi)绾卧诓桓氖录那闆r下更改事件日志?
那么,解決這個問題的一個簡單方法就是將事件標(biāo)記為可忽略的,以便在重建過程中我們可以忽略它們:
?
如果事件是由錯誤數(shù)據(jù)或錯誤順序觸發(fā)的呢?使用這種方法,我們不得不做的就是將所有事件標(biāo)記為可忽略的,并添加一個具有正確值或位置正確的新事件,如下所示:
?
一個笨辦法是為每個實(shí)體添加一個浮點(diǎn)計(jì)數(shù)器。它會讓你根據(jù)超任務(wù)(supertask)的理論在中間無限增加項(xiàng)(實(shí)際上,你受到float/double 最大的長度限制),這通常足以容納所有必要的事件來修復(fù)你的狀態(tài):
?
關(guān)于外部系統(tǒng)|其他微服務(wù)?
微服務(wù)不是孤島,重放事件的副作用之一是你的服務(wù)向外部發(fā)送消息是合理的。這些消息可能會在其他系統(tǒng)中引發(fā)不一致或傳播錯誤,這可能會使情況比以前更糟糕。
不幸的事,由于可能的情況多種多樣, 這里沒有銀彈來解決這個問題, 并且每一個案例不得不單獨(dú)處理。 下面給出一些普遍的解決方案:
臨時(shí)修改配置禁止發(fā)送任務(wù)外部消息或者添加一個攔截器允許你配置哪些消息需要發(fā)送;
重新路由指定的請求到一個假的服務(wù)(如果你正在使用的服務(wù)網(wǎng)絡(luò)模式就是一個典型的場景)
使其他服務(wù)能夠識別出一個給定的操作已經(jīng)在過去以相同的參數(shù)執(zhí)行了,而不是拋出一個錯誤,服務(wù)需要像以前一樣返回相同的成功消息。
當(dāng)然,有相當(dāng)多的情況下,您無法自動修復(fù)外部不一致情況,在這種情況下,預(yù)計(jì)其他系統(tǒng)會輸出人為可讀的錯誤和/或觸發(fā)人工干預(yù)的通知。
事件溯源的優(yōu)點(diǎn)
盡管它是一個簡單的模式,但使用它有很多優(yōu)點(diǎn):
事件日志具有很高的商業(yè)價(jià)值;
它在DDD和事件驅(qū)動架構(gòu)下運(yùn)行得非常好。
調(diào)試用應(yīng)用程序狀態(tài)中所有變更的來源;
它允許您重放失敗的事件;
易于調(diào)試,您可以將目標(biāo)實(shí)體的所有事件復(fù)制到您的機(jī)器并調(diào)試每個事件,以了解應(yīng)用程序如何達(dá)到特定狀態(tài)(忽略從生產(chǎn)環(huán)境復(fù)制數(shù)據(jù)的安全隱患);
允許您使用追溯事件模式重建/修復(fù)您的狀態(tài)。
許多作者還將優(yōu)先級作為時(shí)間查詢的能力,但我認(rèn)為查詢多個后續(xù)事件不是一項(xiàng)簡單的任務(wù)。因此,我通常認(rèn)為時(shí)間查詢是快照模式的一個優(yōu)點(diǎn)。
事件溯源的缺點(diǎn)
在同步調(diào)用中不太直觀,因?yàn)樾枰紫葘⒄埱筠D(zhuǎn)換為事件。
無論何時(shí)部署重大更新,如果您想要向后兼容(也稱為“事件升級”),你將被迫遷移事件歷史記錄。
某些實(shí)現(xiàn)可能需要額外的工作來檢查最新事件的狀態(tài),以確保所有事件都已被處理。
事件可能包含私有數(shù)據(jù),所以不要忘記確保事件日志得到適當(dāng)保護(hù)。
結(jié)論
我已經(jīng)展示了稍微修改過的事件溯源/事件記錄模式,這在過去幾年一直很適合我。我第一次聽說這種方法是近10年前在Marting Fowler博客文章(必讀)中。從那以后,它為我提供了很多幫助,使我的微服務(wù)的狀態(tài)幾乎牢不可破。
然而,這種方法也不該在你的所有服務(wù)中不分青紅皂白地使用。我個人認(rèn)為只有核心價(jià)值才是真正值得的。例如,您可能不需要保留用戶在系統(tǒng)中更改自己的姓名的所有時(shí)間的歷史記錄。
如果您有任何問題,請隨時(shí)在@deniswsrosa推特給我
參考鏈接
[1] https://query-tutorial.couchbase.com/tutorial/#1