在軟件架構(gòu)中,有一種模式雖鮮為人知的,但值得引起更多的關(guān)注。面向數(shù)據(jù)的架構(gòu)(Data-Oriented Architecture)由 Rajive Joshi在RTI 的2007 年白皮書中首次提出,而維也納大學(xué)(University of Vienna)的Christian Vorhemus 和Erich Schikuta 在2017 年的 iiWAS 論文中又再次對其進(jìn)行了描述。 DOA 是對傳統(tǒng)二分法的顛覆,它介于單體架構(gòu)和微服務(wù)(Microservices)、面向服務(wù)的架構(gòu)(Service-Oriented Architecture)之間。單體架構(gòu)由一個單體二進(jìn)制文件(binary)和數(shù)據(jù)存儲組成;微服務(wù)、面向服務(wù)的架構(gòu)由許多小型的、分布式的、獨(dú)立的二進(jìn)制文件組成,并且每個二進(jìn)制文件都有自己的數(shù)據(jù)存儲。在面向數(shù)據(jù)的架構(gòu)中,單體數(shù)據(jù)存儲是系統(tǒng)中狀態(tài)的唯一來源,并由松耦合無狀態(tài)的微服務(wù)對其進(jìn)行操作。

我很幸運(yùn),我的前雇主也采用了這種非同尋常的架構(gòu)選擇。它提醒我們,事情可以用不同的方式來做。無論如何,面向數(shù)據(jù)的架構(gòu)都不是銀彈。它有自己獨(dú)特的成本和收益。不過,我確實(shí)發(fā)現(xiàn),許多大型公司和生態(tài)系統(tǒng)都陷入了某種種類型的瓶頸,而這種類型的瓶頸正是面向數(shù)據(jù)的架構(gòu)能解決的。
單體架構(gòu)簡介
由于許多架構(gòu)通常都是在與單體架構(gòu)(Monolithic Architecture)進(jìn)行對比的情況下定義的,因此,花一些時間來介紹單體架構(gòu)是值得的。畢竟,它是服務(wù)端軟件開發(fā)傳說中的自然狀態(tài)。

在單體(monolithic)服務(wù)中,大部分服務(wù)端代碼都在一個程序中,該程序與一個或多個數(shù)據(jù)庫通信,并處理功能計算的各個方面。假設(shè)有一個交易系統(tǒng),它接收客戶購買或出售某種證券的請求,為它們定價,并完成訂單。
在單體服務(wù)中,仍然可以將代碼組件化并分離到各個模塊中,但是程序中不同組件之間的 API 邊界不是強(qiáng)制的。程序中唯一經(jīng)過嚴(yán)格定義的 API 通常是:**(a)UI 和服務(wù)端之間的 API(可以使用任何它們約定好的 REST/HTTP 協(xié)議);(b)服務(wù)端和數(shù)據(jù)存儲之間的 API(可以使用任何它們約定好的查詢語言);或(c)** 服務(wù)端與其外部依賴之間的 API。
面向服務(wù)的架構(gòu)和微服務(wù)
另一方面,面向服務(wù)的架構(gòu)(Service-oriented architectures,SOA)將單體程序分解成各個相互獨(dú)立的、組件化的功能服務(wù)。在我們的交易應(yīng)用程序中,我們可能需要一個單獨(dú)的服務(wù)來作為外部 API 接收請求并處理客戶響應(yīng);第二個單獨(dú)的系統(tǒng)來接收報價和其他市場相關(guān)的信息;第三個系統(tǒng)來跟蹤訂單和風(fēng)險等。這些服務(wù)之間的接口都是一個個形式化定義的 API 層。服務(wù)之間通常通過 RPC 進(jìn)行點(diǎn)對點(diǎn)的通信,此外,通過其他通信技術(shù)(如,消息傳遞和發(fā)布訂閱模式)進(jìn)行通信也是很常見的。

面向服務(wù)的架構(gòu)允許根據(jù)需要對不同的服務(wù)進(jìn)行獨(dú)立(并行)開發(fā)和推理。這些服務(wù)是松耦合的,這就意味著一個全新的服務(wù)現(xiàn)在可以重用其他服務(wù)了。
由于 SOA 中的每個服務(wù)都定義了自己的 API,因此可以獨(dú)立訪問每個服務(wù)并與之交互。開發(fā)人員如果要調(diào)試或模擬各個功能部分,可以分別調(diào)用各個組件,并且新流程可以重新組合這些單獨(dú)的服務(wù)以啟用新的行為。
微服務(wù)是面向服務(wù)的架構(gòu)的一種形式。根據(jù)服務(wù)對象的不同,它們可能與 SOA 不同,因?yàn)檫@些服務(wù)本應(yīng)特別小巧輕量,或者它們只是 SOA 的同義詞。
規(guī)模問題
在 SOA 中,各個組件通過每個組件各自定義的特定 API 直接相互通信。為了通信,每個組件都可以單獨(dú)尋址(即,使用 IP 地址、服務(wù)地址或其他內(nèi)部標(biāo)識符來相互發(fā)送請求 / 消息)。這意味著架構(gòu)中的每個組件都需要了解它們的依賴關(guān)系,并且需要專門與它們的依賴進(jìn)行集成。
依賴于架構(gòu)的拓?fù)浣Y(jié)構(gòu),這可能意味著需要一個額外的組件來跟蹤了解所有之前的組件。此外,這可能還意味著要替換一個已經(jīng)與其他 N 個組件通信的單個服務(wù)也是一種挑戰(zhàn):我們需要注意保留我們定義的任何點(diǎn)對點(diǎn)的 API,并確保有一個遷移計劃,用于將每個組件從老的尋址服務(wù)移動到新的尋址服務(wù)上。由于服務(wù)到服務(wù)的 API 是點(diǎn)對點(diǎn)的(ad-hoc)(1),這通常意味著組件之間的 RPC 可以是任意復(fù)雜的,這可能會增加將來 API 變更的影響面。因?yàn)槿绻獙Ψ?wù)中被其他服務(wù)依賴的每個 API 進(jìn)行變更都將是一項艱巨的任務(wù)。
我要說的是,隨著微服務(wù)生態(tài)系統(tǒng)的發(fā)展,在規(guī)模上,它變得很容易受到如下問題的影響:
- 隨著組件數(shù)量的增長(2),集成的復(fù)雜度也以 N^2 的級別增加。
- 網(wǎng)絡(luò)的形狀變得很難用先驗(yàn)來推理;即,創(chuàng)建或維護(hù)測試環(huán)境或沙箱將需要進(jìn)行大量的推理才能確保圖中的任何組件都不具有外部依賴性
我的一些朋友也提出了一些他們在使用大規(guī)模面向服務(wù)的架構(gòu)時遇到的問題:
隨著 SOA 規(guī)模的增長,我發(fā)現(xiàn)的另一個問題是服務(wù)之間的循環(huán)依賴。由于我們是單獨(dú)發(fā)布各個服務(wù)的,很少從頭開始構(gòu)建整個系統(tǒng),因此很容易引入循環(huán)并破壞 DAG。
大規(guī)模 SOA 另一個值得注意的問題是:它們要求我們提前了解所有未來的客戶工作流。假設(shè)我們需要跨多個垂直領(lǐng)域來隔離單個工作流的數(shù)據(jù),如果沒有做到提前了解,那么我們要么會遇到性能問題,因?yàn)樗鼘⒃噲D保證跨多個持久化存儲的事務(wù)性;要么需要重新定義要用哪些垂直主服務(wù)器來復(fù)制(緩存,但實(shí)際上是持久化到數(shù)據(jù)庫中的)數(shù)據(jù)。
面向數(shù)據(jù)的架構(gòu)
在面向數(shù)據(jù)的架構(gòu)( Data-Oriented Architecture,DOA)中,系統(tǒng)仍然圍繞小型的、松耦合的標(biāo)準(zhǔn)來組織組件,就像在 SOA、微服務(wù)中一樣。但是 DOA 與微服務(wù)的區(qū)別主要體現(xiàn)在兩個方面:

- 組件通常是無狀態(tài)的
DOA 沒有對每個相關(guān)組件的數(shù)據(jù)存儲進(jìn)行組件化和聯(lián)合,而是要求按照集中管理的全局模式來描述數(shù)據(jù)或狀態(tài)層。
- 最小化了組件之間的交互,并通過數(shù)據(jù)層的交互來替代
在我們的交易系統(tǒng)中,接收不同證券報價的組件在我們的數(shù)據(jù)存儲中只是以一種規(guī)范的形式來發(fā)布價格。系統(tǒng)可以通過查詢數(shù)據(jù)層的價格來使用這些報價,而無需通過特定的 API 向某個特定的服務(wù)(或一組服務(wù))請求價格。
這里,集成的代價是線性的。變更 DOA 模式意味著最多只需要更新 N 個組件,而不是它們之間互聯(lián)的最大值 N^2。
真正令人矚目的地方在于不同的提供者可以填充獨(dú)立的高級數(shù)據(jù)類型。如果我們用一張表來替換一個服務(wù),這并不會帶來太大的簡化。但是當(dāng)同一個通用數(shù)據(jù)類型有多個源時,這樣做就會有很大的幫忙。假設(shè)交易系統(tǒng)需要連接到多個市場,每個市場都會將客戶的請求發(fā)布到詢價(RFQ)表中,那么下游系統(tǒng)就可以查詢這個表,而無需關(guān)心客戶請求到底來自何處。
組件通信類型
由于在 DOA 中最小化了組件之間的交互,那么如何通過數(shù)據(jù)層的交互來代替當(dāng)今 SOA 中組件之間的通信呢?
1. 數(shù)據(jù)生產(chǎn)和消費(fèi)
設(shè)計 DOA 系統(tǒng)的主要方法是將組件組織成數(shù)據(jù)的生產(chǎn)者和消費(fèi)者。
如果我們能夠在較高層次上將業(yè)務(wù)邏輯編寫為一系列的 map、filter、reduce、flatMap 和其他一元(monadic)操作,那么我們就可以將 DOA 系統(tǒng)編寫成一系列的組件,每個組件都查詢或訂閱其輸入并產(chǎn)生其輸出。 DOA 面臨的挑戰(zhàn)在于這些中間步驟是可見的、可查詢的數(shù)據(jù),這意味著需要對其進(jìn)行良好的封裝和表示,并且需要將其與特定的業(yè)務(wù)邏輯概念對應(yīng)。不過,它的優(yōu)勢在于系統(tǒng)的行為是可從外部觀察、跟蹤和審核的。
在 SOA 交易系統(tǒng)中,從市場上接收訂單的組件可能會使用 RPC 調(diào)用來確定如何對訂單進(jìn)行定價、報價或交易。在 DOA 中,微服務(wù)接收來自市場的請求(通常是通過 SOA 的方式)并生成詢價(RFQ),而其他生產(chǎn)者則生產(chǎn)定價數(shù)據(jù),等等。另一個微服務(wù)通過請求來查詢 RFQ,該 RFQ 會結(jié)合它們的所有定價以輸出報價、訂單或任何其他需要響應(yīng)的自定義數(shù)據(jù)。
2. 觸發(fā)動作和行為
有時,RPC 是組件之間通信的最簡單方式。雖然在設(shè)計良好的 DOA 系統(tǒng)中(3),其大部分組件間的通信采用的是生產(chǎn)者 / 消費(fèi)者模式,但是我們可能仍需要采用直接的方式來讓組件 X 告訴 Y 去做 Z。
首先,必須考慮是否可以將 RPC 重組為事件(event)及其影響(effect)。即,不是讓組件 X 向發(fā)生事件 E 的組件 Y 發(fā)送 RPC 請求,而是詢問 X 是否可以生成事件 E,并讓組件 Y 通過消費(fèi)這些事件來驅(qū)動響應(yīng)?
這種方法,我稱之為基于數(shù)據(jù)的事件(data-based events),它可以很好地逆轉(zhuǎn)我們通常使用的組件通信方式。它之所以如此強(qiáng)大是因?yàn)樗刮覀兛梢詫?ldquo;松耦合”這個術(shù)語提升到一個全新的層次。系統(tǒng)不需要知道誰在消費(fèi)它的事件(即,系統(tǒng)不是一個絕對需要知道他們在調(diào)用誰的 RPC 調(diào)用方),生產(chǎn)者也無需擔(dān)心事件的來源,只需知道這些事件的業(yè)務(wù)邏輯語義即可。
當(dāng)然,存在一種簡單的方法可以實(shí)現(xiàn)基于數(shù)據(jù)的事件,在這種方法中,每個事件都是以與 RPC 請求序列化版本 1:1 的對應(yīng)關(guān)系持久化到自身表的數(shù)據(jù)庫中。在這種情況下,基于數(shù)據(jù)的事件根本不會使系統(tǒng)解耦合。為了使基于數(shù)據(jù)的事件能正常運(yùn)行,則要求將請求 / 響應(yīng)轉(zhuǎn)換成的持久化事件必須是有意義的業(yè)務(wù)邏輯結(jié)構(gòu)。
基于數(shù)據(jù)的事件有時可能不太合適。例如,我們實(shí)際上要觸發(fā)某個特定組件中的行為。在這些情況下,可能仍然需要保留少量的實(shí)際組件到組件的 RPC。
面向數(shù)據(jù)的架構(gòu)的成功案例研究
高集成問題空間
我之所以一直以交易 / 財務(wù)軟件為例,部分原因在于財務(wù)通常需要較大的集成表面積。一個典型的允許較小客戶進(jìn)行交易的賣方公司,通常會與許多市場進(jìn)行整合,以與客戶進(jìn)行互動,而許多流動資金提供者則會通過其獲取價格并下訂單。在請求進(jìn)入市場到對客戶做出響應(yīng)之間需要處理的業(yè)務(wù)邏輯是一個復(fù)雜的、多階段的流程。
在高集成問題空間中,單個服務(wù)可能需要了解許多其他服務(wù)。為了避免 O(N^2) 的集成成本及具有高扇出比率的復(fù)雜獨(dú)立服務(wù)的出現(xiàn),圍繞數(shù)據(jù)生產(chǎn)者和消費(fèi)者的重新配置系統(tǒng)可以使集成更加簡單。假設(shè)要進(jìn)行一個新的集成,不能編寫 N 個新系統(tǒng),也不能編寫一個具有向 N 個其他系統(tǒng)進(jìn)行復(fù)雜扇出的系統(tǒng),那么集成過程可能需要編寫一個適配器,該適配器以通用的 DOA 模式生產(chǎn)數(shù)據(jù)、消費(fèi)最終的輸出并以正確的線性格式來呈現(xiàn)。
隱含地是,集成中出現(xiàn)了一種新的復(fù)雜性:需要考慮模式。任何新的集成對我們的系統(tǒng)而言都應(yīng)該是原生的,并且我們的模式應(yīng)該能在不添加補(bǔ)充、修改和特殊用例的情況下擴(kuò)展。這本身就是一項艱巨的任務(wù)。但是,當(dāng)集成的數(shù)量足夠多時,難度就會降低,而且往往是值得的。
沙箱數(shù)據(jù)以及數(shù)據(jù)隔離的推理

如果我們要手動建模或測試,則希望最好能在生產(chǎn)之外進(jìn)行。但是,某些 SOA 生態(tài)系統(tǒng)的架構(gòu)方式通常意味著,想要知道某個服務(wù)所處的環(huán)境或特定環(huán)境是否完全獨(dú)立并不那么容易。
環(huán)境是指內(nèi)部一致、連接一致的服務(wù)集合,通常或理想情況下,它應(yīng)與生產(chǎn)的拓?fù)浣Y(jié)構(gòu)相同。由于 SOA 服務(wù)通常是可獨(dú)立尋址的,因此,環(huán)境一致性斷言要求環(huán)境中的每個服務(wù)必須與環(huán)境中的其他服務(wù)就調(diào)用哪個地址能達(dá)成共識。RPC、訂閱模式(pubsub)和數(shù)據(jù)流不能從一個環(huán)境泄漏到另一個環(huán)境中。
很明顯,在 SOA 中有很多方法可以解決這個問題,比如轉(zhuǎn)換到能為服務(wù)生成正確配置的服務(wù)注冊中心 (4),或者,如果是通過 URI 訪問服務(wù),則隱藏直接的服務(wù)地址,以支持某個環(huán)境前綴下的不同路徑(5)。
然而,在 DOA 中,環(huán)境的概念要簡單得多。知道組件連接到哪個數(shù)據(jù)存儲層就足以描述它所處的環(huán)境了。由于所有組件都不在內(nèi)部存儲任何狀態(tài),因此數(shù)據(jù)是根據(jù)定義來隔離的。組件僅通過數(shù)據(jù)存儲進(jìn)行通信,因此不存在將數(shù)據(jù)從一種環(huán)境泄漏到另一種環(huán)境的危險。
面向數(shù)據(jù)架構(gòu)比你想象的更接近現(xiàn)實(shí)
如今,有很多類似于面向數(shù)據(jù)的架構(gòu)的通用案例。將所有(或大部分)數(shù)據(jù)保存在一個大型數(shù)據(jù)存儲中的數(shù)據(jù)單體,在系統(tǒng)架構(gòu)上就非常接近于 DOA。
例如,知識圖譜(Knowledge Graphs)就是一個廣義的數(shù)據(jù)單體。也就是說,它們通常不是很通用;許多與業(yè)務(wù)邏輯相關(guān)的狀態(tài)可能會丟失。
GraphQL 通常被用作標(biāo)準(zhǔn)化的數(shù)據(jù)存儲層,就像數(shù)據(jù)單體一樣。 GraphQL 是否能成功地成為 DOA 系統(tǒng)的后端,在很大程度上取決于系統(tǒng)對模式設(shè)計的選擇:選擇與業(yè)務(wù)邏輯概念相關(guān)的通用模式和表,而不是選擇特定于該數(shù)據(jù)特定源的模式和表。
權(quán)衡取舍
這種架構(gòu)也不是萬能的。當(dāng)面向數(shù)據(jù)的架構(gòu)消除了某些類型的問題時,就會出現(xiàn)新的問題:它要求設(shè)計人員需要認(rèn)真考慮數(shù)據(jù)的所有權(quán)。當(dāng)多個寫程序修改同一記錄時,可能會很麻煩,它通常會鼓勵系統(tǒng)仔細(xì)劃分記錄的寫入所有權(quán)。而且,由于組件間的 API 是在數(shù)據(jù)中編碼的,因此必須采用需要謹(jǐn)慎考慮的共享全局模式。
我記得 google 的 Protocol Buffers 文檔,在討論如何根據(jù)需要將模式中的字段標(biāo)記為 required 時,它會警告說:“ Required Is Forever ”。在 Broadway Technology,首席技術(shù)官(CTO)Joshua Walsky 曾對 DOA 模式說過類似的話:數(shù)據(jù)是永遠(yuǎn)存在(Data Is Forever)。事實(shí)證明,出于與 Protobuf 警告類似的原因,在松耦合的分布式系統(tǒng)中,從表中刪除列確實(shí)非常困難。
我的建議是:如果您擔(dān)心自己的架構(gòu)存在水平擴(kuò)展問題,那么就可以考慮以數(shù)據(jù)單體為中心來進(jìn)行設(shè)計了。
備注
(1)服務(wù)到服務(wù)的 API 不一定是點(diǎn)對點(diǎn)的,但是組件到組件的直接通信通常意味著,為了達(dá)到某個給定的目的,需在兩者之間傳遞參數(shù)。
(2)一個架構(gòu)的集成復(fù)雜度增長是否真能達(dá)到 N^2,實(shí)際上取決于架構(gòu)的拓?fù)浣Y(jié)構(gòu)。如果在我們使用的系統(tǒng)中集成是主要的瓶頸之一,則可能會遇到這個問題。
例如,集成了各種流動資金提供者和場外交易(OTC)市場的交易系統(tǒng),在理想情況下不應(yīng)處于這樣的場景中:每個管理市場訂單的組件都需要了解每個提供流動資金的組件。
(3)非常適合的 DOA 就是精心設(shè)計的。
(4)假設(shè)服務(wù)調(diào)用對方是基于直接地址的(例如,IP 或正在運(yùn)行的進(jìn)程的某些內(nèi)部地址模式),并且服務(wù)基于命令行參數(shù)能知道在何處訪問特定的服務(wù),那么就可能需要使用更適合的邏輯來包裝這些服務(wù)了,對應(yīng)的邏輯需要根據(jù)環(huán)境來構(gòu)造正確的標(biāo)志。
(5) 例如,與其通過 IP 地址或特定于某個服務(wù)的內(nèi)部 URI 來訪問該特定服務(wù),不如將每個服務(wù)構(gòu)造在一個服務(wù)端路由的“路徑”下。例如,使用 ://env.namespace.company.com/Employees/* 而不是 ://process1.namespace.company.com/*