介紹 系統(tǒng)架構(gòu)微服務(wù)化以后,根據(jù)微服務(wù)獨(dú)立數(shù)據(jù)源的思想,每個(gè)微服務(wù)一般具有各自獨(dú)立的數(shù)據(jù)源,但是不同微服務(wù)之間難免需要通過(guò)數(shù)據(jù)分發(fā)來(lái)共享一些數(shù)據(jù),這個(gè)就是微服務(wù)的數(shù)據(jù)分發(fā)問(wèn)題。Netflix/Airbnb等一線互聯(lián)網(wǎng)公司的實(shí)踐[參考附錄1/2/3]表明,數(shù)據(jù)一致性分發(fā)能力,是構(gòu)建松散耦合、可擴(kuò)展和高性能的微服務(wù)架構(gòu)的基礎(chǔ)。 本文解釋分布式微服務(wù)中的數(shù)據(jù)一致性分發(fā)問(wèn)題,應(yīng)用場(chǎng)景,并給出常見(jiàn)的解決方法。本文主要面向互聯(lián)網(wǎng)分布式系統(tǒng)架構(gòu)師和研發(fā)經(jīng)理。 為啥要分發(fā)數(shù)據(jù)?場(chǎng)景?我們還是要從具體業(yè)務(wù)場(chǎng)景出發(fā),為啥要分發(fā)數(shù)據(jù)?有哪些場(chǎng)景?在實(shí)際企業(yè)中,數(shù)據(jù)分發(fā)的場(chǎng)景其實(shí)是非常多的。假設(shè)某電商企業(yè)有這樣一個(gè)訂單服務(wù)Order Service,它有一個(gè)獨(dú)立的數(shù)據(jù)庫(kù)。同時(shí),周邊還有不少系統(tǒng)需要訂單的數(shù)據(jù),上圖給出了一些例子:
當(dāng)然,為了獲得一份訂單數(shù)據(jù),這些系統(tǒng)可以定期去訂單服務(wù)查詢(xún)最新的數(shù)據(jù),也就是拉模式,但是拉模式有兩大問(wèn)題:
所以,當(dāng)企業(yè)規(guī)模到了一定階段,還是需要考慮數(shù)據(jù)分發(fā)技術(shù),將業(yè)務(wù)數(shù)據(jù)同步分發(fā)到對(duì)數(shù)據(jù)感興趣的其它服務(wù)。除了上面提到的一些數(shù)據(jù)分發(fā)場(chǎng)景,其實(shí)還有很多其它場(chǎng)景,例如:
總之,波波認(rèn)為,數(shù)據(jù)分發(fā),是構(gòu)建現(xiàn)代大規(guī)模分布式系統(tǒng)、微服務(wù)架構(gòu)和異步事件驅(qū)動(dòng)架構(gòu)的底層基礎(chǔ)技術(shù)。 雙寫(xiě)?對(duì)于數(shù)據(jù)分發(fā)這個(gè)問(wèn)題,乍一看,好像并不復(fù)雜,稍有開(kāi)發(fā)經(jīng)驗(yàn)的同學(xué)會(huì)說(shuō),我在應(yīng)用層做一個(gè)雙寫(xiě)不就可以了嗎?比方說(shuō),請(qǐng)看上圖右邊,這里有一個(gè)微服務(wù)A,它需要把數(shù)據(jù)寫(xiě)入DB,同時(shí)還要把數(shù)據(jù)寫(xiě)到MQ,對(duì)于這個(gè)需求,我在A服務(wù)中弄一個(gè)雙寫(xiě),不就搞定了嗎?其實(shí)這個(gè)問(wèn)題并沒(méi)有那么簡(jiǎn)單,關(guān)鍵是你如何才能保證雙寫(xiě)的事務(wù)性? 請(qǐng)看上圖左邊的代碼,這里有一個(gè)方法updateDbThenSendMsgInTransaction,這個(gè)方法上加了事務(wù)性標(biāo)注,也就是說(shuō),如果拋異常的話,數(shù)據(jù)庫(kù)操作會(huì)回滾。我們來(lái)看這個(gè)方法的執(zhí)行步驟: 第一步先更新數(shù)據(jù)庫(kù),如果更新成功,那么result設(shè)為true,如果更新失敗,那么result設(shè)為false; 第二步,如果result為true,也就是說(shuō)DB更新成功,那么我們就繼續(xù)做第三步,向mq發(fā)送消息 如果發(fā)消息也成功,那么我們的流程就走到第四步,整個(gè)雙寫(xiě)事務(wù)就成功了。 如果發(fā)消息拋異常,也就是發(fā)消息失敗,那么容器會(huì)執(zhí)行該方法的事務(wù)性回滾,上面的數(shù)據(jù)庫(kù)更新操作也會(huì)回滾。 初看這個(gè)雙寫(xiě)流程沒(méi)有問(wèn)題,可以保證事務(wù)性。但是深入研究會(huì)發(fā)現(xiàn)它其實(shí)是有問(wèn)題的。比方說(shuō)在第三步,如果發(fā)消息拋異常了,并不保證說(shuō)發(fā)消息失敗了,可能只是由于網(wǎng)絡(luò)異常抖動(dòng)而造成的拋異常,實(shí)際消息可能是已經(jīng)發(fā)到MQ中,但是拋異常會(huì)造成上面數(shù)據(jù)庫(kù)更新操作的回滾,結(jié)果造成兩邊數(shù)據(jù)不一致。 模式一:事務(wù)性發(fā)件箱(Transactional Outbox)對(duì)于事務(wù)性雙寫(xiě)這個(gè)問(wèn)題,業(yè)界沉淀下來(lái)比較實(shí)踐的做法,其中一種,就是采用所謂事務(wù)性發(fā)件箱模式,英文叫Transactional Outbox。據(jù)說(shuō)這個(gè)模式是eBay最早發(fā)明和使用的。事務(wù)性發(fā)件箱模式不難理解,請(qǐng)看上圖。 我們?nèi)匀灰杂唵蜲rder服務(wù)為例。在數(shù)據(jù)庫(kù)中,除了訂單Order表,為了實(shí)現(xiàn)事務(wù)性雙寫(xiě),我們還需增加了一個(gè)發(fā)件箱Outbox表。Order表和Outbox表都在同一個(gè)數(shù)據(jù)庫(kù)中,對(duì)它們進(jìn)行同時(shí)更新的話,通過(guò)數(shù)據(jù)庫(kù)的事務(wù)機(jī)制,是可以實(shí)現(xiàn)事務(wù)性更新的。 下面我們通過(guò)例子來(lái)展示這個(gè)流程,我們這里假定Order Service要添加一個(gè)新訂單。 首先第一步,Order Service先將新訂單數(shù)據(jù)寫(xiě)入Order表,然后它再向Outbox表中寫(xiě)入一條訂單新增記錄,這兩個(gè)DB操作可以包在一個(gè)DB事務(wù)里頭,也就是可以實(shí)現(xiàn)事務(wù)性寫(xiě)入。 然后第二步,我們?cè)僖胍粋€(gè)稱(chēng)為消息中繼Message Relay的角色,它負(fù)責(zé)定期Poll拉取Outbox中的新數(shù)據(jù),然后第三步再Publish發(fā)送到MQ。如果寫(xiě)入MQ確認(rèn)成功,Message Relay就可以將Outbox中的對(duì)應(yīng)記錄標(biāo)記為已消費(fèi)。這里可能會(huì)出現(xiàn)一種異常情況,就是Message Relay在將消息發(fā)送到MQ時(shí),發(fā)生了網(wǎng)絡(luò)抖動(dòng),實(shí)際消息可能已經(jīng)寫(xiě)入MQ,但是Message Relay并沒(méi)有得到確認(rèn),這時(shí)候它會(huì)重發(fā),直到明確成功為止。所以,這里也是一個(gè)At Least Once,也就是至少交付一次的消費(fèi)語(yǔ)義,消息可能被重復(fù)投遞。因此,MQ之后的消費(fèi)方要做消息去重或冪等處理。 總之,事務(wù)性發(fā)件箱模式可以保證,對(duì)Order表的修改,然后將對(duì)應(yīng)事件發(fā)送到MQ,這兩個(gè)動(dòng)作可以實(shí)現(xiàn)事務(wù)性,也就是實(shí)現(xiàn)數(shù)據(jù)分發(fā)的事務(wù)性。 注意,這里的Message Relay角色既可以是一個(gè)獨(dú)立部署的服務(wù),也可以和Order Service住在一起。生產(chǎn)實(shí)踐中,需要考慮Message Relay的高可用部署,還有監(jiān)控和告警,否則如果Message Relay掛了,消息就發(fā)不出來(lái),然后,依賴(lài)于消息的各種消費(fèi)方也將無(wú)法正常工作。 Transactional Outbox參考實(shí)現(xiàn) ~ Killbill Common Queue事務(wù)性發(fā)件箱的原理簡(jiǎn)單,實(shí)現(xiàn)起來(lái)也不復(fù)雜,波波這邊推薦一個(gè)生產(chǎn)級(jí)的參考實(shí)現(xiàn)。這個(gè)實(shí)現(xiàn)源于一個(gè)叫killbill的項(xiàng)目,killbill是美國(guó)高朋(GroupOn)公司開(kāi)源的訂閱計(jì)費(fèi)和支付平臺(tái),這個(gè)項(xiàng)目已經(jīng)有超過(guò)8~9年的歷史,在高朋等公司已經(jīng)有不少落地案例,是一個(gè)比較成熟的產(chǎn)品。killbill項(xiàng)目里頭有一些公共庫(kù),單獨(dú)放在一個(gè)叫killbill-commons的子項(xiàng)目里頭,其中有一個(gè)叫killbill common queue,它其實(shí)是事務(wù)性發(fā)件箱的一個(gè)生產(chǎn)級(jí)實(shí)現(xiàn)。上圖有給出這個(gè)queue的github鏈接。 Killbill common queue也是一個(gè)基于DB實(shí)現(xiàn)的分布式的隊(duì)列,它上層還包裝了EventBus事件總線機(jī)制。killbill common queue的總體設(shè)計(jì)思路不難理解,請(qǐng)看上圖: 在上圖的左邊,killbill common queue提供發(fā)送消息API,并且是支持事務(wù)的。比方說(shuō)圖上的postFromTransaction方法,它可以發(fā)送一個(gè)BusEvent事件到DB Queue當(dāng)中,這個(gè)方法還接受一個(gè)數(shù)據(jù)庫(kù)連接Connection參數(shù),killbill common queue可以保證對(duì)事件event的數(shù)據(jù)庫(kù)寫(xiě)入,和使用同一個(gè)Connection的其它數(shù)據(jù)庫(kù)寫(xiě)入操作,發(fā)生在同一個(gè)事務(wù)中。這個(gè)做法其實(shí)就是一種事務(wù)性發(fā)件箱的實(shí)現(xiàn),這里的發(fā)件箱存的就是事件event。 除了POST寫(xiě)入API,killbill common queue還支持類(lèi)似前面提到的Message Relay的功能,并且是包裝成EeventBus + Handler方式來(lái)實(shí)現(xiàn)的。開(kāi)發(fā)者只需要實(shí)現(xiàn)事件處理器,并且注冊(cè)訂閱在EventBus上,就可以接收到DB Queue,也就是發(fā)件箱當(dāng)中的新事件,并進(jìn)行消費(fèi)處理。如果事件處理成功,那么EvenbBus會(huì)將對(duì)應(yīng)的事件從發(fā)件箱中移走;如果事件處理不成功,那么EventBus會(huì)負(fù)責(zé)重試,直到處理成功,或者超過(guò)最大重試次數(shù),那么它會(huì)將該事件標(biāo)記為處理失敗,并移到歷史歸檔表中,等待后續(xù)人工檢查和干預(yù)。這個(gè)EventBus的底層,其實(shí)有一個(gè)Dispatcher派遣線程,它負(fù)責(zé)定期掃描DB Queue(也就是發(fā)件箱)中的新事件,有的話就批量拉取出來(lái),并發(fā)送到內(nèi)部EventBus的隊(duì)列中,如果內(nèi)部隊(duì)列滿了,那么Dispather Thread也會(huì)暫停拉取新事件。 在killbill common queue的設(shè)計(jì)中,每個(gè)節(jié)點(diǎn)上的Dispather線程只負(fù)責(zé)通過(guò)自己這個(gè)節(jié)點(diǎn)寫(xiě)入的事件,并且在一個(gè)節(jié)點(diǎn)上,Dispather線程也只有一個(gè),這樣才能保證消息消費(fèi)的順序性,并且也不會(huì)重復(fù)消費(fèi)。 Reaper機(jī)制killbill common queue,其實(shí)是一個(gè)基于集中式數(shù)據(jù)庫(kù)實(shí)現(xiàn)的分布式隊(duì)列,為什么說(shuō)它是分布式隊(duì)列呢?請(qǐng)看上圖,killbill common queue的設(shè)計(jì)是這樣的,它的每個(gè)節(jié)點(diǎn),只負(fù)責(zé)消費(fèi)處理從自己這個(gè)節(jié)點(diǎn)寫(xiě)入的事件。比方說(shuō)上圖中有藍(lán)色/黃色和綠色3個(gè)節(jié)點(diǎn),那么藍(lán)色節(jié)點(diǎn),只負(fù)責(zé)從藍(lán)色節(jié)點(diǎn)寫(xiě)入,在數(shù)據(jù)庫(kù)中標(biāo)記為藍(lán)色的事件。同樣,黃色節(jié)點(diǎn),只負(fù)責(zé)從黃色節(jié)點(diǎn)寫(xiě)入,在數(shù)據(jù)庫(kù)中標(biāo)記為黃色的事件。綠色節(jié)點(diǎn)也是類(lèi)似。這是一種分布式的設(shè)計(jì),如果處理容量不夠,只需按需添加更多節(jié)點(diǎn),就可以實(shí)現(xiàn)負(fù)載分?jǐn)偂?/span> 這里有個(gè)問(wèn)題,如果其中某個(gè)節(jié)點(diǎn)掛了,比方說(shuō)上圖的藍(lán)色節(jié)點(diǎn)掛了,那么誰(shuí)來(lái)繼續(xù)消費(fèi)數(shù)據(jù)庫(kù)中藍(lán)色的,還沒(méi)有來(lái)得及處理的事件呢?為了解決這個(gè)問(wèn)題,killbill common queue設(shè)計(jì)了一種稱(chēng)為reaper收割機(jī)的機(jī)制。每個(gè)節(jié)點(diǎn)上都還住了一個(gè)收割機(jī)線程,它們會(huì)定期檢查數(shù)據(jù)庫(kù),看有沒(méi)有長(zhǎng)時(shí)間無(wú)人處理的事件,如果有,就搶占標(biāo)記為由自己負(fù)責(zé)。比方說(shuō)上圖的右邊,最終黃色節(jié)點(diǎn)上的收割機(jī)線程搶到了原來(lái)由藍(lán)色節(jié)點(diǎn)負(fù)責(zé)的事件,那么它會(huì)把這些事件標(biāo)記為黃色,也就是由自己來(lái)負(fù)責(zé)。 收割機(jī)機(jī)制,保證了killbill common queue的高可用性,相當(dāng)于保證了事務(wù)性發(fā)件箱中的Message Relay的高可用性。 Killbill PersistentBus表結(jié)構(gòu)基于killbill common queue的EventBus,也被稱(chēng)為killbill PersistentBus。上圖給出了它的數(shù)據(jù)庫(kù)表結(jié)構(gòu),其中bus_events就是用來(lái)存放待處理事件的,相當(dāng)于發(fā)件箱,主要的字段包括:
當(dāng)前處理狀態(tài)主要包括6種:
除了bus_events待處理事件表,還有一個(gè)對(duì)應(yīng)的bus-events-history事件歷史記錄表。不管成功還是失敗,最終,事件會(huì)被寫(xiě)入歷史記錄表進(jìn)行歸檔,作為事后審計(jì)或者人工干預(yù)的依據(jù)。 上圖下方給出了數(shù)據(jù)庫(kù)表的github鏈接,你可以進(jìn)一步參考學(xué)習(xí)。 Killbill PersistentBus處理狀態(tài)遷移上圖給出了killbill PersistentBus的事件處理狀態(tài)遷移圖。
上圖有一個(gè)通過(guò)API觸發(fā)進(jìn)入的REMOVED移除狀態(tài),這個(gè)是給通知隊(duì)列用的,用戶(hù)可以通過(guò)API移除對(duì)應(yīng)的通知消息。順便提一下,除了事件/消息隊(duì)列,Killbill queue也是支持通知隊(duì)列(或者說(shuō)延遲消息隊(duì)列)的。 模式二:變更數(shù)據(jù)捕獲(Change Data Capture, CDC)對(duì)于事務(wù)性雙寫(xiě)這個(gè)問(wèn)題,業(yè)界沉淀下來(lái)比較實(shí)踐的做法,其中第二種,就是所謂的變更數(shù)據(jù)捕獲,英文稱(chēng)為Change Data Capture,簡(jiǎn)稱(chēng)CDC。 變更數(shù)據(jù)捕獲的原理也不復(fù)雜,它利用了數(shù)據(jù)庫(kù)的事務(wù)日志記錄。一般數(shù)據(jù)庫(kù),對(duì)于變更提交操作,都記錄所謂事務(wù)日志Transaction Log,也稱(chēng)為提交日志Commit Log,比方說(shuō)MySQL支持binlog,Postgres支持Write Ahead log。事務(wù)日志可以簡(jiǎn)單理解為數(shù)據(jù)庫(kù)本地的一個(gè)文件隊(duì)列,它記錄了按時(shí)間順序發(fā)生的對(duì)數(shù)據(jù)庫(kù)表的變更提交記錄。 下面我們通過(guò)例子來(lái)展示這個(gè)變更數(shù)據(jù)捕獲的流程,我們這里假定Order Service要添加一個(gè)新訂單。 第一步,Order Service將新訂單記錄寫(xiě)入Order表,并且提交。因?yàn)檫@是一次表變更操作,所以這次變更會(huì)被記錄到數(shù)據(jù)庫(kù)的事務(wù)日志當(dāng)中,其中內(nèi)容包括發(fā)生的變更數(shù)據(jù)。 第二步,我們還需要引入一個(gè)稱(chēng)為T(mén)ransaction Log Miner這樣的角色,這個(gè)Miner負(fù)責(zé)訂閱在事務(wù)日志隊(duì)列上,如果有新的變更記錄,Miner就會(huì)捕獲到變更記錄。 然后第三步,Miner會(huì)將變更記錄發(fā)送到MQ消息隊(duì)列。同之前的Message Relay一樣,這里的發(fā)送到MQ也是At Least Once語(yǔ)義,消息可能會(huì)被重復(fù)發(fā)送,所以MQ之后的消費(fèi)者需要做去重或者冪等處理。 總之,CDC技術(shù)同樣可以保證,對(duì)Order表的修改,然后將對(duì)應(yīng)事件發(fā)送到MQ,這兩個(gè)動(dòng)作可以實(shí)現(xiàn)事務(wù)性,也就是實(shí)現(xiàn)數(shù)據(jù)分發(fā)的事務(wù)性。 注意,這里的CDC一般是一個(gè)獨(dú)立部署的服務(wù),生產(chǎn)中需要做好高可用部署,并且做好監(jiān)控告警。否則如果CDC掛了,消息也就發(fā)不出來(lái),然后,依賴(lài)于消息的各種消費(fèi)方也將無(wú)法正常工作。 CDC開(kāi)源項(xiàng)目(企業(yè)級(jí))當(dāng)前,有幾個(gè)比較成熟的企業(yè)級(jí)的CDC開(kāi)源項(xiàng)目,我這邊收集了一些,供大家學(xué)習(xí)參考:
對(duì)于上面的這些項(xiàng)目,如果你想生產(chǎn)使用的話,波波推薦的是阿里的Canal,因?yàn)檫@個(gè)項(xiàng)目畢竟是國(guó)內(nèi)大廠阿里落地出來(lái),而且在國(guó)內(nèi)已經(jīng)有不少企業(yè)落地案例。其它幾個(gè)項(xiàng)目,你也可以參考研究。 學(xué)習(xí)參考 ~ Eventuate-Tram既然談到這個(gè)CDC,這里有必要提到一個(gè)人和一本書(shū),這個(gè)人叫Chris Chardson,他是美國(guó)的老一輩的技術(shù)大牛,曾今是第一代的Cloud Foundry項(xiàng)目的創(chuàng)始人(后來(lái)Cloud Foundry被Pivotal所收購(gòu))。近幾年,Chris Chardson開(kāi)始轉(zhuǎn)戰(zhàn)微服務(wù)領(lǐng)域,這兩年,他還專(zhuān)門(mén)寫(xiě)了一本書(shū),叫《微服務(wù)設(shè)計(jì)模式》,英文名是《Microservices Patterns》。這本書(shū)主要是講微服務(wù)架構(gòu)和設(shè)計(jì)模式的,內(nèi)容還不錯(cuò),是我推薦大家閱讀的。 Charis Chardson還專(zhuān)門(mén)開(kāi)發(fā)了一個(gè)叫Eventuate-Tram的開(kāi)源項(xiàng)目(這個(gè)項(xiàng)目也有商業(yè)版),另外他的微服務(wù)書(shū)里頭也詳細(xì)介紹了這個(gè)項(xiàng)目。這個(gè)項(xiàng)目可以說(shuō)是一個(gè)大集成框架,它不僅實(shí)現(xiàn)了DDD領(lǐng)域驅(qū)動(dòng)開(kāi)發(fā)模式,CQRS命令查詢(xún)職責(zé)分離模式,事件溯源模式,還實(shí)現(xiàn)了Saga事務(wù)狀態(tài)機(jī)模式。當(dāng)然,這個(gè)項(xiàng)目的底層也實(shí)現(xiàn)了CDC變更數(shù)據(jù)捕獲模式。 波波認(rèn)為,Charis的項(xiàng)目,作為學(xué)習(xí)研究還是有價(jià)值的,但是暫不建議生產(chǎn)級(jí)使用,因?yàn)樗臇|西不是一線企業(yè)落地出來(lái)的,主要是他個(gè)人開(kāi)發(fā)的。至于說(shuō)Charis的項(xiàng)目能否在一線企業(yè)落地,還有待時(shí)間的進(jìn)一步檢驗(yàn)。 Transactional Outbox vs CDC好的,前面我介紹了解決數(shù)據(jù)的事務(wù)性分發(fā)的兩種落地模式,一種是事務(wù)性發(fā)件箱模式,另外一種是變更數(shù)據(jù)捕獲模式,這兩種模式其實(shí)各有優(yōu)劣,為了幫助大家做選型決策,我這邊對(duì)這兩種模式進(jìn)行一個(gè)比較,請(qǐng)看上面的比較表格:
Single Source of Truth前面我解答了如何解決微服務(wù)的數(shù)據(jù)一致性分發(fā)問(wèn)題,也給出了可落地的方案。最后,我特別說(shuō)明在實(shí)踐中進(jìn)行數(shù)據(jù)分發(fā)的一個(gè)原則,叫Single Source of Truth,翻成中文就是單一真實(shí)數(shù)據(jù)源。它的意思是說(shuō),你要實(shí)現(xiàn)數(shù)據(jù)分發(fā),目標(biāo)服務(wù)可以有很多,但是一定要注意,數(shù)據(jù)的主人只能有一個(gè),它是數(shù)據(jù)的權(quán)威記錄系統(tǒng)(canonical system of record),其它的數(shù)據(jù)都是只讀的,非權(quán)威的拷貝(read-only, non-authoritative copy)。 換句話說(shuō),任何時(shí)候,對(duì)于某類(lèi)數(shù)據(jù),它主人應(yīng)該是唯一的,它是Single Source of Truth,只有它可以修改數(shù)據(jù),其它的服務(wù)可以獲得數(shù)據(jù)拷貝,做本地緩存也沒(méi)問(wèn)題,但是這些數(shù)據(jù)都是只讀的,不能修改。 只有遵循這條原則,數(shù)據(jù)分發(fā)才能正常工作,不會(huì)產(chǎn)生不一致的情況。 結(jié)論
|
|
來(lái)自: 黃爸爸好 > 《架構(gòu)設(shè)計(jì)》