當前位置:
首頁 > 科技 > 數據轉換:從單體式應用到微服務的低風險演變

數據轉換:從單體式應用到微服務的低風險演變

接收程序員的 8 點技術早餐

作者 | Christian Posta

譯者 | 海松

來源 | EAWorld

1

技術

本主題第二部分、第三部分和第四部分中涉及到的技術如下,這些技術在我們的實踐過程中將具備一定的指導作用:

開發人員服務框架(Spring Boot[2],WildFly[3],WildFly Swarm[4])

API 設計 (APICur.io[5])

數據框架(Spring Boot Teiid[6],Debezium.io[7])

集成工具(Apache Camel[8])

服務網格(Istio Service Mesh[9])

資料庫遷移工具(Liquibase[10])

Dark launch / feature flag 框架(FF4J[11])

部署 /CI-CD 平台(Kubernetes[12]/OpenShift[13])

Kubernetes 開發工具(Fabric8.io[14])

測試工具(Arquillian[15],Pact[16]/Arquillian Algeron[17],Hoverfly[18],Spring-Boot Test[19],RestAssured[20],Arquillian Cube[21])

如果你想一起動手實踐,那麼可以和我一起使用 http://developers.redhat.com 上的 TicketMonster 教程作為示例項目,我借用了該教程用以演示如何完成從單體應用到微服務的演變。你還可以在 github 上找到相關的代碼和文檔(文檔還在編寫中):https://github.com/ticket-monster-msa/monolith

在第二部分中,我們開始添加一個將要從單體應用中剝離出來的微服務(Orders/Booking)。我們藉助 Hoverfly 模擬探索合適的 API 設計來開始這一步工作。

2

將 API 與實現進行對接

回顧下注意事項

在定義上,被抽取或新建的服務的數據模型和單體應用的數據模型緊耦合

單體應用很可能沒有提供在合適層級獲取數據的 API

即使我們獲取了數據,也需要大量的代碼樣例來進行數據轉換

我們可以臨時性的直接訪問後端資料庫對數據進行只讀查詢? 單體式應用很少改變其資料庫

第一部分中提到了一個直接連接到單體應用資料庫的解決方案。在這個示例中,我們需要採納這樣的方案,因為資料庫中的數據將為新的 Orders 服務所用,同時我們還要將這個新服務從單體應用中分離出來。此外,我們希望引入這個新服務之後,能負載流量,並與單體應用中的內容具有一致的視圖;例如,我們將在一段時間內同時運行兩種服務。注意,這項操作將直擊分解動作的核心:我們不可能就這樣神奇地調用新的微服務,使它在不影響當前負載的情況下,準確地封裝預訂或訂購的所有邏輯,這是不現實的。

那如果我們不想連接單體應用的資料庫,還能有什麼選擇?我可以枚舉一些…當然如果你還有其他建議,歡迎隨時評論或推我:

使用被單體應用公開的現有 API

創建一個新 API,專門用於訪問單體應用的資料庫;在我們需要數據的時候,隨時調用

從單體應用到新的微服務,做一個提取轉換載入 (ETL),這樣我們就有了數據

使用現有的 API

如果這麼做,一定要深思用法。通常情況下,現有的 API 都是相當粗粒度的,無法適用於低級別的使用,並且還可能需要做大量的調整才能讓其適應新服務中的數據模型。在這個新的 Orders 服務中,每項對新服務輸入調用,都需要查詢 (這裡可能是多個端點的) 遺留 API 或是單體應用 API,還要根據你自己的喜好再去處理響應值。這沒有什麼本質上的錯誤,除非你打算走捷徑,但走捷徑會讓單體應用、遺留的 API 或數據模型嚴重影響到新服務的數據模型。雖然在我的這個示例中,兩個數據模型一開始可能是類似的,但我們希望使用 DDD 來進行快速迭代,並獲得正確的域模型(domain model ),而不僅僅是獲得規範化的數據模型。

創建新的低級別 API

如果現有的單體應用沒有 API 或 API 粒度太粗,又或者你不想還繼續用它,那麼就可以創建一個新的低級別 API,使其直接連接到單體應用的資料庫,並以新 Orders 服務所需要的等級來公開數據。這倒也是一個可以接受的解決方案。另一方面,我的經驗是,新的 Orders 服務不會對這個低級別介面寫入大量的查詢或 API 調用,而會在內存連接中執行響應值,這類似於此前的做法。這就像是在執行一個資料庫。同樣,從本質上講,這沒什麼錯,但這需要為 Orders 服務編寫大量的冗餘代碼 (大量重複,只有少許不同),這些代碼往往只是一些臨時的、過渡性的方案。

從單體應用到新服務,做一個提取轉換載入 (ETL)

某種程度上來說,我們可能確實需要這麼做。但在研究新服務的域模型時,我們可能並不想再去處理舊的單體應用。此外,我們又想讓新服務與單體應用同時運行,二者都能負載流量。如果採納了 ETL 的方法,那麼我們需要想辦法來維持 Orders 服務的狀態更新,因為這些內容可能無法及時同步。這最終會成為大麻煩。

作為新 Order 服務的開發人員,我覺得從域模型 (注意:我不是指數據模型,二者之間是有區別的) 的角度來考慮問題才對新服務有意義。外部實現的影響應該儘可能去消除,因為這可能會影響域模型。區別在於:數據模型顯示了系統中的靜態數據如何關聯,這可能為如何在持久層中儲存數據提供了依據。域模型則用於描述域的解析空間的行為,更多地傾向於關注用例或事務行為。例如,我們用來識別問題的概念或模型就屬於域模型。DDD 大師 Vaughn Vernon[22] 寫了一系列文章 [23],更詳細地討論了這種區別。

我的解決方案是在 Ticket Monster Orders[24] 中引入了一個開源項目 Teiid[25],它能幫忙減少甚至消除往理想域模型添加數據處理模型的冗餘代碼。Teiid 歷來是一個數據聯合軟體 [26],它能夠獲取不同的數據來源 (如關係資料庫、非關係型資料庫、無格式文件等),並將其作為單個虛擬化視圖進行呈現。通常,數據分析人員會使用 Teiid 來聚合數據,用於彙報等。但是我們更感興趣的是開發人員如何使用它解決上述問題。幸運的是,來自 Teiid 社區的人,特別是 Ramesh Reddy[27],為 Teiid 和 Spring Boot [28] 創建了一些不錯的擴展程序來幫助消除在解決問題過程中產生的冗餘代碼。

關於 Teiid Spring Boot 的介紹

再次重申:我們必須專註於服務的域模型,但最初支持域模型的數據仍將存在於單體應用或後端資料庫中。我們是否可以將單體應用的數據模型結構與所期望的域模型結合,並且去掉與數據結合有關的冗餘代碼?

Teiid Spring Boot[29] 能讓我們專註於域模型,用 JPA @entity 為模型創建註解,這一點與其他模型一樣,同時,它還能把模型映射到我們的新資料庫中,以及虛擬地映射單體架構的資料庫。要開始使用 teiid - spring - boot,你只需要導入以下依賴項:

這是一個啟動項目,它會連接到 Spring 的自動配置,並嘗試設置我們的虛擬資料庫 (由單體應用的資料庫和本服務擁有的真實物理資料庫提供支持)。

接下來我們需要為每個後端定義 Spring Boot 中的數據源。在這個示例中,我用了兩個 MySQL 資料庫,但這只是一個細節。我們不僅僅限於兩個相同的數據源,也不應該局限於關係資料庫管理系統(RDBMs)。以下是舉例:

下面開始配置 teiid - spring -boot,來掃描我們的域模型,使其虛擬映射到單體應用。在應用屬性中,我們添加如下內容:

Teiid Spring Boot 允許我們將映射指定為 @entity 定義上的注釋。下面是一個舉例 (github 上可以參見域對象的完整實現和完整實施 [30]) :

在上面的例子中,我們使用 @SelectQuery 來定義遺留數據源 ( legacyDS.*) 和域模型之間的映射。需要注意,通常這些映射可能存在大量的 JOIN 操作,以便為模型獲取正確的數據;所以最好在一個 REST API 的註解中只寫一次 JOIN,因為該注釋在處理這些數據轉換的時候會嘗試編寫大量的冗餘代碼 (不僅僅是查詢,還包括對我們預期域模型的實際映射)。在上述情況下,只需要從單體應用的資料庫映射到域模型就行了,但是如果我們要在自己的資料庫中進行 merge 操作呢?可以這樣做 (完整實施參見 ticket.java[31]) :

請注意,在這裡,我們用關鍵詞 UNION ALL 將單體應用資料庫和本地 Orders 資料庫的兩個視圖結合起來。

那麼 Upgrade 和 Insert 的問題呢?

例如,我們的 Orders 服務應當存儲 Orders 或 booking。可以在整個 booking DDD 中添加 @ InsertQuery 注釋,像這樣:

想要獲取其餘的 teiid - spring -boot 注釋,可參見文檔 [32]。

可見,當我們保留一個新的 booking(如 JPA、spring 數據等等),虛擬資料庫知道將其存儲到自身的 Orders 資料庫中。如果你更傾向於使用 Spring Data,那麼你仍然可以充分利用 teiid - spring -boot。以下是另一個 teiid - spring - boot 示例 [33]:

如果我們選擇好一個合適 teiid - spring - boot 映射注釋,那麼這個 spring -data 存儲庫就能夠正確理解虛擬資料庫層,並能按照預期來處理域模型。

再次強調:這是微服務分解初始步驟中的暫時性解決方案而非最終方案。我們還是需要在運行示例中對其進行迭代。我們正在試圖通過手動的方式來減少做映射或轉譯時可能產生的樣板代碼和麻煩。

同樣,如果你仍有意要為訪問單體應用資料庫的低級別數據建立一個簡單的 API,那麼 teiid - spring -boot 也仍然會對你很有幫助。你可以很快地發布這類 API,該 API 中沒有使用通過 teiid - spring -boot 生成的 odata 集成。瀏覽 odata 模塊 [34] 可獲取更多內容 (注意,我們還在持續的編寫該項目的文檔)

在分解的這個節點上,理應有一個配合著合適的 API,域模型和連接到我們自身資料庫的 Orders 服務實施,並暫時創建一個虛擬映射到我們的單體資料庫,以便在域模型中使用該資料庫。接下來,我們需要將它部署到生產中,進行灰度上線。

3

發送 shadow traffic 到新的微服務(dark launch)

回顧下注意事項

將新訂單服務引入代碼路徑有風險

要以可控的方式將流量發送給新服務

希望流量能被引到新服務以及舊代碼路徑

要測量和監控新服務的影響

要設法標記「合成(synthetic)」事物,以防發生比較頭疼的業務一致性問題

希望新功能部署到特定的群組或用戶

接著我們在本主題第一部分中提到的內容,我們將通過修改單體應用來調用新的 Orders 服務。這會用到 Michael Feather 書中 [35] 的一些技術,來改造或者擴展單體應用中的現有邏輯,從而調用新服務。例如,我們的單體應用在實現 createBookings 時是這樣的:

就像其他任何單體應用一樣,這裡只展現了一小部分代碼,還有更多沒羅列出來,他們的內容又長又複雜,想要理解透徹著實不易。所以我們會將把它們轉換成這樣:

轉換以後的代碼,內容更少、更有條理且更容易執行。那麼究竟發生了什麼? ff.check(...) 又是什麼?

這裡要遵循的一個關鍵點是,單體應用的變更越少越好;理想情況下,我們要進行單元、組件、集成或系統測試來幫忙驗證這些更改是否會對其他內容產生負面影響。如果無法做到,那我們就需要有策略地進行重構,使其能夠進行測試。

在已經更改的部分中,現有的調用流最好保持原樣:於是,我們將早前的實現移動到一個名為 createBookingInternal 的方法中,並保持原樣。不過,我們還運用了一個新的手段來調用 Orders 服務的新代碼路徑。並將啟用一個特性標誌庫 [36],它能實現以下功能:

用於實現訂單的全時運行 / 配置控制項

禁用新功能

同時啟用新功能和舊功能

完全切換到新功能

刪除 switch all 功能

這裡用的是 Feature Flags 4 Java (FF4j)[37],當然還有其他編程語言的替代方案,包括像 Launch Darkly 這樣的託管 SaaS 提供商 [38]。當然,你也可以選擇自己來編寫框架,不過現有的這些項目功能都是現成的,完全可以直接拿來用。這和 Facebook(和其它) 的控制框架 [39] 非常相似。回顧部署和發布間的差異請參閱此處 [40]。

要使用 FF4j,依賴項需要被添加到 pom.xml 中

然後,我們可以在 ff4j.xml 文件中闡述特性,並將其進行組合等。更詳細的關於複雜特性或特性分組的信息,請參閱 ff4j 文檔 [41]:

然後,我們可以將一個 FF4j 對象實例化 [42],並用它來測試這些特性是否已經在代碼中啟用:

「即開即用(out of the box )」的實現採用了 ff4j.xml 配置文件來指定特性。隨後,就可以在運行時進行特性切換 (見下文),但在繼續下一步之前,我想指出的是,這些特性以及它們各自的狀態,比如啟用或禁用狀態下,都應該由重要(non-trivial)部署中的持久化存儲(persistent store)設備進行備份。請查看 ff4j 站點上的 featurestore 文檔 [43]。

在運行時,我們還希望能配置或改變特性在運行時的狀態。FF4j 有一個網頁控制台可以用來部署 [44],從而查看或改變應用程序中的特性狀態:

默認情況下,我們將只啟用舊特性來進行部署。也就是說,在默認情況下,代碼執行路徑和服務表現並沒有發生變化。然後,我們可以進行金絲雀部署,並使用特性標誌來同時啟用舊代碼路徑和新路徑,新路徑會調用新的 Orders 服務。對於某些服務,我們可能不需要太過關注,只需要啟用第二個代碼路徑即可。但是,我們需要通過設置這是一個「測試」或「合成(synthetic)」事務之類的提示,來避免一些會改變狀態的事件發生。此處,當舊代碼路徑和新代碼路徑同時啟用時,我們會把發送到 Orders 服務的消息標記為「合成(synthetic)」。這就提醒 Orders 服務應該將其作為一個正常的請求來處理,但隨後必須要將處理結果丟棄或回滾。這對於了解新代碼路徑正在做什麼,並將其與舊路徑進行例如各自的結果、負面影響、反饋時間或延遲的影響方面的比較,都是非常有價值的。如果只啟用新代碼路徑而禁用舊代碼路徑,那麼我們只會發送實時請求,其中不含合成(synthetic)指示或標誌。

4

指定服務契約

這時候,我們可能應該將單體應用連接到新的 Orders 服務,用於預訂和下單流程。現在對於單體應用來說,是一個明確其在調用 Orders 服務時在契約或數據方面要求的好時機。當然,Orders 服務是一個獨立、自治的服務,它承諾可以提供一些特定的功能或 SLA、SLO 等 [45],但當我們開始構建分散式系統時,有必要了解一下有關服務交互的假設,並理清楚。

通常,我們都是從供應商的角度出發看問題。而在本文案例中,我們則從用戶角度出發。在服務提供商看來,用戶實際使用或重視的是什麼?我們是否可以向提供商提供這種反饋,使他們了解所提供服務的使用情況,以及當服務變更時需要注意的事項,例如,我們不想破壞現有的兼容性。我們想利用用戶驅動契約 [46] 的想法,做出明確的假設(make assumptions explicit)。我們將使用一個名為 Pact 的項目 [47],一種無視編程語言的文檔格式,來指定服務之間的契約 (重點是用戶驅動契約)。據我所知,澳大利亞一家名為 DiUS 的科技公司 [48] 在不久前啟動了 Pact 項目。

上圖來自 Pact 文檔 [49]

讓我們再來看一個後端服務的示例 [50]。我們將為 backend-v2 應用程序創建一個用戶契約規則,這個規則概述了服務提供商 (Orders 服務) 的期望。當我們將 POST HTTP 請求發布到 /rest/bookings 時,我們可以通過以下方式強調一下期望。

當調用提供商提供的服務並將其傳入一個特定主體時,會有一個 HTTP 200 以及與契約匹配的響應值。我們來看一下。首先,先來看看如何指定預訂請求主體:

Pact-jvm[51] 允許我們將 pact - JVM - JUnit[52] 模塊連接到我們最熟悉的測試框架中 (即本例中的 JUnit)。如果將 Arquillian[53] 用於組件和集成測試,我們可以用 Arquillian Algeron[54] 將 Pact 連接到 Arquillian[55] 測試中。Alegeron 擴展了 Pact,使其在 Arquillian 測試中更好用,而且它還加入了一個通常你通常需要自己手動構建的功能,即在測試時自動發布契約到一個代理或者從一個代理處下載契約。這個功能對於 CI 或 CD 流水線至關重要。為了對 Java 應用程序做用戶契約測試,我強烈建議你關注一下 Arquillian 和 Arquillian Algeron[56]。

我們可以創建 PactDslJsonBody 代碼片段,並且使用「通配符」或「在此欄位中傳入任何內容」的語法。例如,我們用 body.integerType("attr_name", default_value) 來規定「將存在一個名為 X、並且有默認值的屬性」。如果去掉默認值參數,那麼該值實際上可以是任何值。在此代碼片段中,我們只規定請求的結構。注意,在此我們指定了一個合成(synthetic)屬性。並且對於每個屬性為 true 的請求,均會有一個具有特定結構的響應值。

在這裡,我們聲明用戶契約 (響應值) :

這是一個非常簡單的例子:對於這個測試,我們所期望的是,該響應值將會有一個屬性為:「synthetic: true」。這很重要,因為當發送合成(synthetic)預訂時,我們希望確保 Orders 服務確認這個預訂確實被當做一個合成(synthetic)請求進行處理。如果這個測試成功運行,我們將在目標構建目錄中生成這個 Pact 契約。(在本文例子中,它會出現./target/pacts 中。)

此處,可以將契約放入 Git[57]、Contract Broker[58] 或共享文件系統 [59] 中。在供應端 (Orders 服務) 上,我們可以創建一個組件測試,來確保提供商提供的服務實際上滿足了用戶契約中的期望。需要注意的是,用戶契約可以有多個,所有這些契約都是可以測試的(尤其當我們對供應商提供的服務進行更改時,可以通過影響測試來了解可能會受到影響的下游用戶)

請注意在這個簡單的示範中,我們將從附屬./pacts 下的文件系統中的一個文件夾中提取契約。

一旦採取了用戶驅動契約測試,我們就能更自如地對服務作出變更。有關此問題的工作示例,請參見 backend-v2 服務 [60] 以及供應商 Orders 服務 [61] 的示例。

5

金絲雀測試或滾動發布新的微服務

回顧下注意事項

確定群組,並將實時事務流量發送給新的微服務

直接連接資料庫仍然是需要的,因為在此期間,事務仍會從兩條代碼路徑通過

將所有流量轉到微服務後,就該放棄舊功能了

請注意,在將實時流量發送給微服務後,回滾到舊代碼路徑將遇到困難,需要協調

該場景另外一個重要部分是,我們需要通過具有特徵標誌的新部署來發送一小部分流量。我們可以使用 Istio 來精確地控制被調用的後端。例如,我們已經部署了 backend-v1,其已完全發布,並接受生產負載。當我們部署 backend-v2,且其具有控制新代碼路徑的特性標誌時,我們可以使用 Istio 來進行金絲雀發布,這與此前文章中的做法類似。從只發送 1% 的流量開始,然後緩慢增加 ( 5%,25% 等),發送的同時注意時刻觀察效果。我們還可以將這些特性進行切換,以便同時啟用舊代碼路徑和新代碼路徑。這是一項非常強大的技術,能幫助我們大大降低微服務架構改變和遷移時所帶來的風險。下面是一個 istio route - rule 的示例:

一些需要注意的事項:到了現在這一步,我們可能會使用新的 Orders 服務來同時啟用舊代碼路徑和新代碼路徑,且新 Orders 服務會執行合成事務。到目前為止,所描述的金絲雀將適用於 1% 的任何流量。如果僅向內部用戶或一小部分外部用戶發布,並實際通過實時 Orders 服務(即非模擬流量)對它們進行發布,那麼這可能是有用的。通過將基於用戶的修改路徑和將用戶分組到隊列的 FF4j 配置相結合,我們就可以啟用新 Orders 服務的完整代碼路徑(包括實時流量、非合成事務性載荷等)。然而,這一點的關鍵是,一旦用戶已被定向到 Orders 的實時代碼路徑, 為了方便以後的調用,會一直這樣發送。這是因為一旦用新服務進行下單,該 Orders 將不會出現在單體應用的資料庫中。對該用戶的所有查詢或更新都應該始終通過新的微服務。

此時,我們可以觀察流量模式或服務表現,並做出是否增加發布範圍的決定。最終,我們的目的是將所有流量發送到新服務上。

如果數據不在單體應用中,該怎麼辦? 你可能會選擇什麼都不做——新 Orders 服務現在是訂單或預訂邏輯加數據的合法所有者。對於這些新 Orders, 如果覺得有必要在單體應用之間進行集成,你可以選擇發布新 Orders 服務中的事件以及訂單詳細信息。這樣,單體應用也可以捕捉這些事件,並將它們儲存在其資料庫中。其他服務也可以監聽這些事件,並對其作出反應。事件發布機制還是有用的。

好啦,這篇博文已經夠長了!整體內容還剩下兩個小節,分別是「離線數據提取轉換載入(ETL)或遷移」和「斷開或解耦數據存儲」。因為我想妥善處理這部分內容,所以這裡必須收尾了,剩餘的部分會在第四部分呈現!第五部分將是網路廣播或視頻或 demo 演示,在展現整體內容。

參考地址:

[1]http://arquillian.org/arquillian-algeron/

[2]https://projects.spring.io/spring-boot/

[3]http://wildfly.org

[4]http://wildfly-swarm.io

[6]https://github.com/teiid/teiid-spring-boot

[7]http://debezium.io

[9]https://istio.io

[11]https://ff4j.org

[12]https://kubernetes.io

[13]https://www.openshift.org

[14]https://fabric8.io

[15]http://arquillian.org

[16]https://github.com/pact-foundation/pact-specification

[17]http://arquillian.org/arquillian-algeron/

[18]https://hoverfly.io

[19]https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html

[20]http://rest-assured.io

[21]http://arquillian.org/arquillian-cube/

[22]https://twitter.com/VaughnVernon

[23]https://vaughnvernon.co/?p=838

[24]https://github.com/ticket-monster-msa/monolith/tree/master/orders-service

[26]http://searchdatamanagement.techtarget.com/definition/data-federation-technology

[27]https://twitter.com/rareddy

[28、29]https://github.com/teiid/teiid-spring-boot/blob/master/docs/UserGuide.adoc

[30]https://github.com/ticket-monster-msa/monolith/tree/master/orders-service/src/main/java/org/ticketmonster/orders/domain

[31]https://github.com/ticket-monster-msa/monolith/blob/master/orders-service/src/main/java/org/ticketmonster/orders/domain/Ticket.java

[32]https://github.com/teiid/teiid-spring-boot/blob/master/docs/Reference.adoc

[33]https://github.com/teiid/teiid-spring-boot/blob/master/samples/rdbms/src/main/java/org/teiid/spring/example/CustomerRepository.java

[34]https://github.com/teiid/teiid-spring-boot/tree/master/odata

[35]https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

[36、37]https://ff4j.org

[38]http://blog.launchdarkly.com/feature-flags-dark-launches-and-canary-releases-for-all-launchdarkly-first-year-in-review/

[39]http://blog.launchdarkly.com/secret-to-facebooks-hacker-engineering-culture/

[40]https://blog.turbinelabs.io/deploy-not-equal-release-part-one-4724bc1e726b

[41]https://github.com/ff4j/ff4j/wiki/Advanced-Concepts

[42]https://github.com/ticket-monster-msa/monolith/blob/master/backend-v2/src/main/java/org/jboss/examples/ticketmonster/util/FF4jFactory.java

[43]https://github.com/ff4j/ff4j/wiki/Store-Technologies

[44]https://github.com/ff4j/ff4j/wiki/Web-Concepts#web-console

[45]http://blog.christianposta.com/microservices/3-easy-things-to-do-to-make-your-microservices-more-resilient/

[46]https://martinfowler.com/articles/consumerDrivenContracts.html

[47]https://github.com/pact-foundation/pact-specification

[48]https://twitter.com/dius_au

[49]https://docs.pact.io/documentation/

[50]https://github.com/ticket-monster-msa/monolith/tree/master/backend-v2

[51]https://github.com/DiUS/pact-jvm

[52]https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-junit

[53、55]http://arquillian.org

[54、56]http://arquillian.org/arquillian-algeron/

[57]http://arquillian.org/arquillian-algeron/#_git_publisher

[58]http://arquillian.org/arquillian-algeron/#_pact_broker

[59]http://arquillian.org/arquillian-algeron/#_folder_publisher

[60]https://github.com/ticket-monster-msa/monolith/tree/master/backend-v2

[61]https://github.com/ticket-monster-msa/monolith/tree/master/orders-service

期望得到更多優質技術乾貨,歡迎掃描群助手小波波二維碼,與近萬名技術人一起在 EAWorld 社群參與定期微課、視頻分享、探討關於大數據、微服務、DevOps 實踐等技術內容。入群暗號:312


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

還把配置當小事兒?那是你太不了解微服務了
春節假期技術圈都發生了哪些大新聞?

TAG:InfoQ |