一個經過優化的微服務架構案例
前言
大家都知道,基于單體(Monolith)和微服務(Microservice)架構的爭論已經存在多年,正如我們對胖客戶端、瘦客戶端孰好孰壞的爭論一樣,有必然的歷史演化,也有各自的優缺點。架構師們總是在考慮,我們是要一個中心化、全能多才的單體,還是百花齊放、各自為政的微服務群體,各種基于成本、交互、部署等等的討論應該不會停下腳步。這里,我們不做過多的深入探討和介紹,而本文正是這些討論中的一個很好的例子,供大家思考。
Joakim Tengstrand 近期在他的推特上提到,看到 Robert C. Martin(Bob 大叔)多年前描述的基于 JAR 包 微服務 的不足之處,于是寫下了這篇有趣的稱為 微單體 (micro monolith)的文章,并在Git上發布了基于 Java 和 Clojure 的兩種源碼包供下載。接下來將逐一介紹微單體的概念、工作方式、各自的優缺點、示例、實踐等環節。
微單體是什么
用成千上萬或數百萬行代碼來編寫高質量的軟件,可能是開發人員所能承擔的最具挑戰性和最復雜的任務之一。Tengstrand 利用軟件模塊化這種非常簡單的方式,實現了一個方案并期望能有助于完成這項挑戰,提供了基于 Java 和 Clojure 的 代碼 供參考。
隨著系統變得越來越大,最終會達到一個臨界點,作為一個 單體 ( monolith )它變得難以管理。每一行代碼的添加,都會讓系統變得更加難以理解、變更和重用。雖然 微服務 ( microservice )試圖解決這些問題,但也帶來了額外的復雜性以及集成的成本。
微單體架構的核心原則是保持硬件、軟件和數據緊密地結合在“一個地方”。這樣處理可以簡化事情,擺脫不必要的協調工作。如果我們從同一個地方直接訪問數據,性能也會得到改善。當設計系統時,可以像微服務一樣使用小的、孤立的、可組合的構建塊(building blocks),又可以像單體一樣通過一個地方執行它們,就能從兩方面都達到最優。
從上面的介紹可以看出,單體就像一個胖服務,微單體利用微服務架構的優勢將處理工作從邏輯上分拆出去,在此基礎上增加一層調度(編排服務)來管理所有的微服務。這樣一來,使得業務的完整性和一致性得到了較好的保證,也解決了跨服務集成的問題。用一個簡單的例子來描述,單體就好像把所有的文件放在了一個文件夾里,微服務則試圖將它們分類并放在不同文件夾中,而微單體的方法是生成了一個叫做 development 的文件夾,里面保存了所有文件的快捷方式(shortcut),這樣更易于根據不同的業務場景來管理和訪問這些“文件”。
如何工作
在版本控制系統里為每個服務生成一個項目,這樣可以得到各自的 JAR 包(假設在 JVM 上運行,或者類似平臺)。它們就是構成系統的構建塊,并最終組成整個生產系統。生成一個 development 項目,通過服務把所有源代碼連接起來,這樣就可以在里面直接運行源代碼,就像是只有這一個項目一樣。
接下來介紹微單體架構的優缺點:
優點
- 簡單性(關注點分離,代碼直接調用,消除了網絡 API 的復雜度)
- 卓越的性能(沒有訪問服務的網絡調用)
- 模塊化、可組合的服務(在不同的系統中重復利用)
- 跨服務事務的數據一致性(無需考慮最終一致性)
- 減少 DevOps 和硬件/托管的費用(在單機上運行的系統)
- 易于測試(可以對整個系統進行一體化測試)
- 更快和更有效的開發體驗(跨服務導航、重構和調試 + 變更無需重建服務)
缺點
- 必須在所有服務中使用相同的編程語言(*)
- development 項目需要一些額外的設置(創建符號鏈接 symbolic links)
- 操作系統必須支持符號鏈接 symbolic links
- 共享相同路徑的資源在所有服務中必須具有唯一的名稱(它們有不同的內容)
- IDE 內置的版本控制失效(因為微單體架構可能不支持 IDE)
(*) 并沒有強迫只使用一種編程語言,目標是要讓 development 項目發揮其優勢(例如跨服務的重構和調試),這時最好的選擇是使用一種語言。其次是混合使用,可以使用在同一個平臺(比如 JVM)上運行的語言,像 Java、Scala、JRuby、 Clojure(如果使用本地接口 JNI ,還可以選擇 C),但如果一個服務做了變更,就需要生成一個新的 JAR 包并共享給其他服務。
上面介紹了微單體方法的概念,還沒有提到它是如何在實踐中工作的,現在讓我們通過 Java 和 Clojure 的兩個示例來進行演示。所有示例的代碼可以在 這里 找到。注:在實際系統中,它們被存儲在單獨的資源庫里并且彼此隔離,這里為了方便起見,它們被存儲在同一個資源庫中。
下面 Java 和 Clojure 的例子會實現相同的“解決方案”,利用一個假造的 REST API 來編排一些服務,并暴露findaddresses,douserstuff 和 domoreuserstuff 供調用。
Java - 示例代碼
Java 是一種流行語言,這里將展示在面向對象語言里的微單體架構是什么樣子的。
在處理 development 項目時,你在大部分時間里會是一個開發人員。雖然所有的服務都被存為各自獨立的項目( Git ),這里我們通過一個技巧,利用 符號鏈接 把所有源碼“放到”一個單獨的項目中。IDE 并不關心地址是“真正的”還是一個鏈接,都采用箭頭來標記它們(至少在 這個例子 里是這樣的):
項目在本地check out后,這些鏈接在 Linux 或 Unix 上 馬上就能工作。如果是其它平臺,可以參考 這樣 類似的腳本,手動地創建 development 項目。
建立了這個項目,通常意義的甚至跨服務的開發環境所具備的調試、重構和搜索等等方面的好處,我們都可以實現。這一點非常強大并且節省了時間,每次代碼變更不需要重建服務,這樣使得工作流程非常高效和快樂。
依賴性
在設計系統時,需要決定是否允許服務獲得外部庫(external libraries)具體實現的信息。在本文的例子里,我們選擇信息透明,最好在所有服務中使用相同版本的外部庫。
另一種選擇是通過在內部服務與外部庫之間添加接口來集成,這樣一個服務就不用知道具體庫的信息,比如 log4j-1.2.17.jar,只需要生成一個接口 log4j-api,這樣編排服務(orchestrator service)就能把它注入到所需的服務中去了。
編排服務(The orchestrator service)
編排服務就是把所有服務都放到一起的那個服務。一個系統可以有多個編排服務,這里的例子只有一個 RestService ,它依賴地址,電子郵件和用戶這幾個服務,并在 pom.xml 里進行指定。
如果服務 A 需要調用服務 B 里的函數 f,可以通過編排服務把函數 f 注入到服務 A 中。這里沒有強制要求一次只注入一個函數,但是這樣做可以降低服務間的耦合,增加可變性(changeability)和可測性(testability)。我們將使用“微注入”這個術語,特指一次只注入一個函數的意思。
測試
微單體架構鼓勵讓測試變得簡單、容易,就像微服務一樣,要讓每個服務的獨立測試更方便。相比于微服務, 微單體就像一體化部署在一臺機器上一樣,它讓測試變得更加的簡單(比如本例的 REST API )。
例子里包含了一個測試數據生成器,它幫助我們在已知的狀態下建立數據庫。可以存在一個用戶表和一個地址表相關聯,然后就得到一個 UserService 和一個 AddressService。 測試數據生成器 可以方便地設置數據庫的已知狀態,這有助于編寫集成測試。這可以在某個服務中完成并且實現跨服務調用,例如 AddressServiceTest 和 UserServiceTest 。
Clojure - 示例代碼
Clojure 是一個功能強大的語言,能在 JVM 上運行。下面將展示在 Clojure 這樣的函數式語言中如何使用微單體架構。所有 Clojure 代碼可以在 這里 找到。
它的 development 項目看起來是這樣的:
Clojure 版本的結構基本類似 Java 版本,但函數都存儲在不同命名空間而不是類中,也不需要像Java一樣為地址和電子郵件服務添加額外的 API 層。Clojure 版本更加簡潔,可以用大約 200 行代碼實現 Java 里 400 行代碼能完成的工作。
Clojure 里的微注入會更簡單,可以用宏注入(inject macro)的方式來注入函數。這里的例子里,在命名空間 rest.service 的第8行,就是用函數 email/send-pdf-email! 替換了原來的 user.service/send-pdf-email! 。
實踐經驗
Tengstrand 和他的團隊已經把微單體架構應用到了一個真正的生產系統。他們搭建了這樣一個架構,每一個服務在 Git上 有各自的資源庫。遷移到微單體是非常順利的,要做的就是 丟棄 30% 的代碼 ,并把所有 REST 服務調用替換成簡單的函數調用。這個過程中消失的不僅僅是 REST 部分,還有很多復雜的狀態和錯誤處理。
從一開始,就像生產環境一樣設置開發環境,每個服務就是一個 Java 存檔(JAR 文件)。缺點是每次的服務變更,必須重建這個 JAR 文件,以便它可以供其他服務使用。另一個缺點是,我們必須重新啟動 REPL (是的,使用的是Clojure!),這樣做耗費了時間并把在 REPL 上工作的一些快樂也帶走了。
于是,Tengstrand 的團隊想出了新的方法來設置 development 項目,只需啟動一次 REPL ,然后可以繼續工作而不會被打斷,這樣一來開發人員就幸福多了。另一件事是,他們意識到服務里有一些灰色標記的死掉(dead)的代碼,現在也可以去掉了。
另一個設計上的選擇是采用 Datomic 數據庫,它真的與微單體架構很適用,既簡單又強大。你可以在 這里 了解它的更多架構細節。
他們使用測試數據生成器來處理幾乎所有的服務,讓集成測試更簡單。之前他們發現可以改進服務中的一些變量和函數的命名,但更改之前必須手動搜索和替換所有的服務,這樣做費時又容易出錯。最后他們并沒有做這些小的改動,而是通過development 項目,利用 IDE 對重構的支持瞬間就做到了變量和函數的重命名。
總結
微單體提出了如何構建系統的一個簡單模式,雖然和微服務競爭但并不能完全取代它,因為后者肯定有它的位置。如果有需要的話,隨時可以同時使用它們。
最后 Tengstrand 建議,如果在構建系統時非常關注簡單性和可組合性,那么一定要嘗試下微單體架構,享受這個架構帶來的高效,以及測試、生產環境的簡潔。
來自:http://www.infoq.com/cn/articles/an-optimized-micro-service-architecture-case