Categories
程式開發

面向數據的架構


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

面向數據的架構 1

我很幸運,我的前雇主也採用了這種非同尋常的架構選擇。它提醒我們,事情可以用不同的方式來做。無論如何,面向數據的架構都不是銀彈。它有自己獨特的成本和收益。不過,我確實發現,許多大型公司和生態系統都陷入了某種種類型的瓶頸,而這種類型的瓶頸正是面向數據的架構能解決的。

單體架構簡介

由於許多架構通常都是在與單體架構(Monolithic Architecture)進行對比的情況下定義的,因此,花一些時間來介紹單體架構是值得的。畢竟,它是服務端軟件開發傳說中的自然狀態

面向數據的架構 2

單體(monolithic)服務中,大部分服務端代碼都在一個程序中,該程序與一個或多個數據庫通信,並處理功能計算的各個方面。假設有一個交易系統,它接收客戶購買或出售某種證券的請求,為它們定價,並完成訂單。

在單體服務中,仍然可以將代碼組件化並分離到各個模塊中,但是程序中不同組件之間的API邊界不是強制的。程序中唯一經過嚴格定義的API通常是:**(a)UI和服務端之間的API(可以使用任何它們約定好的REST/HTTP協議);(b)服務端和數據存儲之間的API(可以使用任何它們約定好的查詢語言);或(c)**服務端與其外部依賴之間的API。

面向服務的架構和微服務

另一方面,面向服務的架構(Service-oriented architectures,SOA)將單體程序分解成各個相互獨立的、組件化的功能服務。在我們的交易應用程序中,我們可能需要一個單獨的服務來作為外部API接收請求並處理客戶響應;第二個單獨的系統來接收報價和其他市場相關的信息;第三個系統來跟踪訂單和風險等。這些服務之間的接口都是一個個形式化定義的API層。服務之間通常通過RPC進行點對點的通信,此外,通過其他通信技術(如,消息傳遞和發布訂閱模式)進行通信也是很常見的。

面向數據的架構 3

面向服務的架構允許根據需要對不同的服務進行獨立(並行)開發和推理。這些服務是松耦合的,這就意味著一個全新的服務現在可以重用其他服務了。

由於SOA中的每個服務都定義了自己的API,因此可以獨立訪問每個服務並與之交互。開發人員如果要調試或模擬各個功能部分,可以分別調用各個組件,並且新流程可以重新組合這些單獨的服務以啟用新的行為。

微服務是面向服務的架構的一種形式。根據服務對象的不同,它們可能與SOA不同,因為這些服務本應特別小巧輕量,或者它們只是SOA的同義詞。

規模問題

在SOA中,各個組件通過每個組件各自定義的特定API直接相互通信。為了通信,每個組件都可以單獨尋址(即,使用IP地址、服務地址或其他內部標識符來相互發送請求/消息)。這意味著架構中的每個組件都需要了解它們的依賴關係,並且需要專門與它們的依賴進行集成。

依賴於架構的拓撲結構,這可能意味著需要一個額外的組件來跟踪了解所有之前的組件。此外,這可能還意味著要替換一個已經與其他N個組件通信的單個服務也是一種挑戰:我們需要注意保留我們定義的任何點對點的API,並確保有一個遷移計劃,用於將每個組件從老的尋址服務移動到新的尋址服務上。由於服務到服務的API是點對點的(ad-hoc)(1),這通常意味著組件之間的RPC可以是任意複雜的,這可能會增加將來API變更的影響面。因為如果要對服務中被其他服務依賴的每個API進行變更都將是一項艱鉅的任務。

我要說的是,隨著微服務生態系統的發展,在規模上,它變得很容易受到如下問題的影響:

  1. 隨著組件數量的增長(2),集成的複雜度也以N^2 的級別增加。
  2. 網絡的形狀變得很難用先驗來推理;即,創建或維護測試環境或沙箱將需要進行大量的推理才能確保圖中的任何組件都不具有外部依賴性

我的一些朋友也提出了一些他們在使用大規模面向服務的架構時遇到的問題:

隨著SOA規模的增長,我發現的另一個問題是服務之間的循環依賴。由於我們是單獨發佈各個服務的,很少從頭開始構建整個系統,因此很容易引入循環並破壞DAG。

大規模SOA另一個值得注意的問題是:它們要求我們提前了解所有未來的客戶工作流。假設我們需要跨多個垂直領域來隔離單個工作流的數據,如果沒有做到提前了解,那麼我們要么會遇到性能問題,因為它將試圖保證跨多個持久化存儲的事務性;要么需要重新定義要用哪些垂直主服務器來複製(緩存,但實際上是持久化到數據庫中的)數據。

面向數據的架構

面向數據的架構( Data-Oriented Architecture,DOA)中,系統仍然圍繞小型的、松耦合的標準來組織組件,就像在SOA、微服務中一樣。但是DOA與微服務的區別主要體現在兩個方面:

面向數據的架構 4

  1. 組件通常是無狀態的

DOA沒有對每個相關組件的數據存儲進行組件化和聯合,而是要求按照集中管理的全局模式來描述數據或狀態層。

  1. 最小化了組件之間的交互,並通過數據層的交互來替代

在我們的交易系統中,接收不同證券報價的組件在我們的數據存儲中只是以一種規範的形式來發布價格。系統可以通過查詢數據層的價格來使用這些報價,而無需通過特定的API向某個特定的服務(或一組服務)請求價格。

這裡,集成的代價是線性的。變更DOA模式意味著最多只需要更新N個組件,而不是它們之間互聯的最大值N^2。

真正令人矚目的地方在於不同的提供者可以填充獨立的高級數據類型。如果我們用一張表來替換一個服務,這並不會帶來太大的簡化。但是當同一個通用數據類型有多個源時,這樣做就會有很大的幫忙。假設交易系統需要連接到多個市場,每個市場都會將客戶的請求發佈到詢價(RFQ)表中,那麼下游系統就可以查詢這個表,而無需關心客戶請求到底來自何處。

組件通信類型

由於在DOA中最小化了組件之間的交互,那麼如何通過數據層的交互來代替當今SOA中組件之間的通信呢?

1. 數據生產和消費

設計DOA系統的主要方法是將組件組織成數據的生產者和消費者。

如果我們能夠在較高層次上將業務邏輯編寫為一系列的map、filter、reduce、flatMap和其他一元(monadic)操作,那麼我們就可以將DOA系統編寫成一系列的組件,每個組件都查詢或訂閱其輸入並產生其輸出。 DOA面臨的挑戰在於這些中間步驟是可見的、可查詢的數據,這意味著需要對其進行良好的封裝和表示,並且需要將其與特定的業務邏輯概念對應。不過,它的優勢在於系統的行為是可從外部觀察、跟踪和審核的。

在SOA交易系統中,從市場上接收訂單的組件可能會使用RPC調用來確定如何對訂單進行定價、報價或交易。在DOA中,微服務接收來自市場的請求(通常是通過SOA的方式)並生成詢價(RFQ),而其他生產者則生產定價數據,等等。另一個微服務通過請求來查詢RFQ,該RFQ會結合它們的所有定價以輸出報價、訂單或任何其他需要響應的自定義數據。

2. 觸發動作和行為

有時,RPC是組件之間通信的最簡單方式。雖然在設計良好的DOA系統中(3),其大部分組件間的通信採用的是生產者/消費者模式,但是我們可能仍需要採用直接的方式來讓組件X告訴Y去做Z。

首先,必須考慮是否可以將RPC重組為事件(event)及其影響(effect)。即,不是讓組件X向發生事件E的組件Y發送RPC請求,而是詢問X是否可以生成事件E,並讓組件Y通過消費這些事件來驅動響應?

這種方法,我稱之為基於數據的事件(data-based events),它可以很好地逆轉我們通常使用的組件通信方式。它之所以如此強大是因為它使我們可以將“松耦合”這個術語提升到一個全新的層次。系統不需要知道誰在消費它的事件(即,系統不是一個絕對需要知道他們在調用誰的RPC調用方),生產者也無需擔心事件的來源,只需知道這些事件的業務邏輯語義即可。

當然,存在一種簡單的方法可以實現基於數據的事件,在這種方法中,每個事件都是以與RPC請求序列化版本1:1的對應關係持久化到自身表的數據庫中。在這種情況下,基於數據的事件根本不會使系統解耦合。為了使基於數據的事件能正常運行,則要求將請求/響應轉換成的持久化事件必須是有意義的業務邏輯結構。

基於數據的事件有時可能不太合適。例如,我們實際上要觸發某個特定組件中的行為。在這些情況下,可能仍然需要保留少量的實際組件到組件的RPC。

面向數據的架構的成功案例研究

高集成問題空間

我之所以一直以交易/財務軟件為例,部分原因在於財務通常需要較大的集成表面積。一個典型的允許較小客戶進行交易的賣方公司,通常會與許多市場進行整合,以與客戶進行互動,而許多流動資金提供者則會通過其獲取價格並下訂單。在請求進入市場到對客戶做出響應之間需要處理的業務邏輯是一個複雜的、多階段的流程。

在高集成問題空間中,單個服務可能需要了解許多其他服務。為了避免 O(N^2)的集成成本及具有高扇出比率的複雜獨立服務的出現,圍繞數據生產者和消費者的重新配置系統可以使集成更加簡單。假設要進行一個新的集成,不能編寫N個新系統,也不能編寫一個具有向N個其他系統進行複雜扇出的系統,那麼集成過程可能需要編寫一個適配器,該適配器以通用的DOA模式生產數據、消費最終的輸出並以正確的線性格式來呈現。

隱含地是,集成中出現了一種新的複雜性:需要考慮模式。任何新的集成對我們的系統而言都應該是原生的,並且我們的模式應該能在不添加補充、修改和特殊用例的情況下擴展。這本身就是一項艱鉅的任務。但是,當集成的數量足夠多時,難度就會降低,而且往往是值得的。

沙箱數據以及數據隔離的推理

面向數據的架構 5

如果我們要手動建模或測試,則希望最好能在生產之外進行。但是,某些SOA生態系統的架構方式通常意味著,想要知道某個服務所處的環境或特定環境是否完全獨立並不那麼容易。

環境是指內部一致、連接一致的服務集合,通常或理想情況下,它應與生產的拓撲結構相同。由於SOA服務通常是可獨立尋址的,因此,環境一致性斷言要求環境中的每個服務必須與環境中的其他服務就調用哪個地址能達成共識。 RPC、訂閱模式(pubsub)和數據流不能從一個環境洩漏到另一個環境中。

很明顯,在SOA中有很多方法可以解決這個問題,比如轉換到能為服務生成正確配置的服務註冊中心 (4),或者,如果是通過URI訪問服務,則隱藏直接的服務地址,以支持某個環境前綴下的不同路徑(5)

然而,在DOA中,環境的概念要簡單得多。知道組件連接到哪個數據存儲層就足以描述它所處的環境了。由於所有組件都不在內部存儲任何狀態,因此數據是根據定義來隔離的。組件僅通過數據存儲進行通信,因此不存在將數據從一種環境洩漏到另一種環境的危險。

面向數據架構比你想像的更接近現實

如今,有很多類似於面向數據的架構的通用案例。將所有(或大部分)數據保存在一個大型數據存儲中的數據單體,在系統架構上就非常接近於DOA。

例如,知識圖譜(Knowledge Graphs)就是一個廣義的數據單體。也就是說,它們通常不是很通用;許多與業務邏輯相關的狀態可能會丟失。

GraphQL通常被用作標準化的數據存儲層,就像數據單體一樣。 GraphQL是否能成功地成為DOA系統的後端,在很大程度上取決於系統對模式設計的選擇:選擇與業務邏輯概念相關的通用模式和表,而不是選擇特定於該數據特定源的模式和表。

權衡取捨

這種架構也不是萬能的。當面向數據的架構消除了某些類型的問題時,就會出現新的問題:它要求設計人員需要認真考慮數據的所有權。當多個寫程序修改同一記錄時,可能會很麻煩,它通常會鼓勵系統仔細劃分記錄的寫入所有權。而且,由於組件間的API是在數據中編碼的,因此必須採用需要謹慎考慮的共享全局模式。

我記得Google的Protocol Buffers文檔,在討論如何根據需要將模式中的字段標記為required時,它會警告說:“Required Is Forever”。在Broadway Technology,首席技術官(CTO)Joshua Walsky曾對DOA模式說過類似的話:數據是永遠存在(Data Is Forever)。事實證明,出於與Protobuf警告類似的原因,在松耦合的分佈式系統中,從表中刪除列確實非常困難。

我的建議是:如果您擔心自己的架構存在水平擴展問題,那麼就可以考慮以數據單體為中心來進行設計了。

備註

(1)服務到服務的API不一定是點對點的,但是組件到組件的直接通信通常意味著,為了達到某個給定的目的,需在兩者之間傳遞參數。

(2)一個架構的集成複雜度增長是否真能達到N^2,實際上取決於架構的拓撲結構。如果在我們使用的系統中集成是主要的瓶頸之一,則可能會遇到這個問題。

例如,集成了各種流動資金提供者和場外交易(OTC)市場的交易系統,在理想情況下不應處於這樣的場景中:每個管理市場訂單的組件都需要了解每個提供流動資金的組件。

(3)非常適合的DOA就是精心設計的。

(4)假設服務調用對方是基於直接地址的(例如,IP或正在運行的進程的某些內部地址模式),並且服務基於命令行參數能知道在何處訪問特定的服務,那麼就可能需要使用更適合的邏輯來包裝這些服務了,對應的邏輯需要根據環境來構造正確的標誌。

(5) 例如,與其通過IP地址或特定於某個服務的內部URI來訪問該特定服務,不如將每個服務構造在一個服務端路由的“路徑”下。例如,使用 ://env.namespace.company.com/Employees/* 而不是 ://process1.namespace.company.com/*

原文鏈接:

Data-Oriented Architecture