Java微服務實用指南(一)

本文將爲大家介紹什麼是Java微服務,瞭解Java微服務的體系架構,以及如何設計、開發、部署和測試。

Java微服務:基礎

要真正理解Java微服務,就必須從最基本的東西開始:爲人詬病的Java 大型獨體應用,它是什麼,它的優點和缺點是什麼。

什麼是Java 大型獨體應用?

假設你正在爲一家銀行或一家金融科技初創公司工作。你爲用戶提供一款可以用它來開設新的銀行賬戶的移動應用程序。

如果用Java代碼來寫,可以實現一個簡化版的控制器類,如下所示。

@Controller
class BankController {
    @PostMapping("/users/register")
    public void register(RegistrationForm form) {
        validate(form);
        riskCheck(form);
        openBankAccount(form);
        // 略……
    }
}

這段代碼要:

  1. 驗證註冊表單。
  2. 對用戶的地址進行風險檢查,以決定是否可以給他一個銀行帳戶。
  3. 打開這個銀行賬戶

部署的時候,你會將BankController類與所有其他源代碼一起打包到bank.jar或bank.war中:在遠古時期,這個龐然大物還是不錯的,它包含你的銀行系統運行所需的所有代碼。(粗略估算,一開始你的jar或war文件的大小會在1-100MB範圍之內。)

然後在服務器上運行.jar文件——這就是部署Java應用程序所需要做的全部工作。

Java大型獨體應用存在什麼問題?

本質上,Java大型獨體應用沒有什麼問題。但通過以往的項目經驗,我們可以明顯發現,如果你:

  1. 讓許多不同的程序員/團隊/顧問……
  2. 面臨着高壓和不明確需求,圍繞同一款大型獨體應用工作……
  3. 好幾年……

那麼你那個小小的bank.jar文件就會變成一隻巨大的、有千兆字節的代碼怪物,每個人都不敢部署它。

如何讓Java大型獨體應用變得更小?

這自然就引出瞭如何縮小大型獨體應用的問題。現在,你的bank.jar是在一個JVM中運行的,一 臺服務器上運行一個進程。不多也不少。

現在,你可能會產生一個想法:那個風險檢查服務是公司其他部門使用的,我的這款銀行應用與它沒什麼關係,不妨把它切離出去,將它作爲自己的產品去部署,作爲單獨的進程來運行。

什麼是Java微服務?

實際上,這意味着你不需要在你的BankController中調用riskCheck()方法,而是將該方法或bean及其所有輔助類移動到它自己的Maven/Gradle項目中,對其進行源代碼配置管理,並將其獨立部署,不依賴於你的銀行系統。

整個提取過程本身會不會使你新的RiskCheck模塊成爲微服務呢,大家對微服務定義有着不同的解釋:

  • 如果它裏面只有5-7個類,那麼算不算微?
  • 100或者1000個類仍然算是微嗎?
  • 這和類的數量有什麼關係嗎?

我們不去鑽理論上的牛角尖,而是關注其實用性,做以下兩件事:

  1. 調用所有可單獨部署的服務(即與大小或領域邊界無關的微服務)。
  2. 重點關注服務間的通信這一重要主題,因爲你的微服務需要相互通信的方式。

所以,總結一下:你之前擁有一個JVM進程,即一個銀行大型獨體應用。現在,除了這個銀行JVM進程,還有一個在自己JVM進程中運行的RiskCheck微服務。你的大型獨體應用現在必須調用這個微服務進行風險檢查。

怎麼做呢?

如何在Java微服務之間進行通信?

基本上,有兩種選擇:同步通信或異步通信。

(http)/rest(同步通信)

同步微服務通信通常通過HTTP和返回XML或JSON的類似rest的服務來完成——儘管這不是必需的(例如,參考谷歌的協議緩衝區)。

如果你需要立即響應,可以使用REST通信,具體到我們的案例就是這樣做的,因爲在開戶之前必須進行風險檢查:不做風險檢查,就不給開戶。

在工具方面,可以看看哪些類庫最適合同步Java REST調用

消息傳遞(異步通信)

異步微服務通信通常通過JMS實現和/或AMQP等協議的消息傳遞來完成。通常,是因爲實際上如email/smtp驅動集成的數量是不可低估的。

有時,使用一個微服務並不需要得到立即響應,比如用戶按下“立即購買”按鈕並希望生成發票,則當然不必在用戶購買這一請求/響應週期內完成。

在工具方面,可以看看哪些代理最適合異步Java消息傳遞

示例:在Java中調用REST API

假設我們選擇使用同步微服務通信,那麼我們上面的Java代碼看起來就像是更底層的代碼。因爲對於微服務通信,通常會創建client類庫,將實際的HTTP調用抽象出來。

@Controller
class BankController {
    @Autowired
    private HttpClient httpClient;
    @PostMapping("/users/register")
    public void register(RegistrationForm form) {
        validate(form);
        httpClient.send(riskRequest, responseHandler());
        setupAccount(form);
        // 略......
    }
}

看到這段代碼就會發現,現在必須部署兩個Java(微)服務:Bank和RiskCheck服務。最終會得到兩個jvm,兩個進程。之前的關係圖看起來將是這樣的:

圖片

這就是開發一個Java微服務項目所需的全部內容:構建和部署更小的部件(.jar或.war文件)。

但這就留下了一個問題:你究竟如何切分或配置這些微服務?這些小部件是什麼?多大合適?

讓我們來看看現狀。

Java微服務體系結構

實際上,公司可以通過各種方式來設計或架構微服務項目。具體情況取決於你是試圖將一個現有的大型獨體應用變成一個微服務項目,還是從一個全新的項目開始。

從大型獨體應用到微服務

一個更有機的想法是將微服務從現有的整體中分離出來。請注意,這裏的“微”實際上並不意味着提取出來的服務本身很小,它們本身可能仍然相當大。

我們來看一些理論。

想法:將一個大型獨體應用拆分成微服務

將遺留項目轉換爲微服務,主要是出於以下三個原因:

  1. 它們通常難以維護/變更/擴展。
  2. 每個人,都想讓事情變得更簡單,從開發人員、運維人員到管理人員。
  3. 在某種程度上,你對你的領域有着明確的邊界界定,也就是說:你知道你的軟件應該做什麼。

這意味着你可以好好看看你的Java銀行應用這個龐然大物,並嘗試沿着領域邊界拆分它,這不失爲一種明智之舉。

  • 你可以得出這樣的結論:應該有一個“賬戶管理”微服務,它可以處理用戶的姓名、地址、電話號碼等數據。
  • 還有前面提到過的“風險模塊”,用來檢查用戶的風險級別,可以供公司的許多其他項目甚至部門使用。
  • 還有一個發票模塊,它通過PDF或實際的郵件發送發票。

現實:讓別人來做

雖然這種方法在在紙上和uml類圖上呈現出來很美,但是它也有缺點。最主要的一點是,使用這種方法需要很強的技術能力。爲什麼呢?

因爲在理解將高度耦合的帳戶管理模塊從你的大型獨體應用中提取出來是個好主意是一回事,正確地去執行它是另一回事,兩者之間存在着巨大的差異。

大多數企業項目都到了這樣一個階段,即開發人員不敢將已經用了7年的Hibernate版本升級到新的版本,這只是更新一個類庫而已,但也需要做大量工作以確保不會破壞任何東西。

這些開發人員現在要深入挖掘舊的遺留代碼(它們沒有清晰的數據庫事務邊界),並提取定義良好的微服務?可能是吧,但通常是真正的挑戰,是無法在白板或架構會議上解決的了的。

這是本文中第一次引用推特上@simonbrown的話:

我一直都說……如果你不能正確地構建大型獨體應用,那麼微服務也幫不了你。
Simon Brown

全新項目的微服務架構

開發全新的Java項目時,情況看起來有點不同。現在,這三點與之前那三點略有不同:

  1. 你要從頭開始,所以沒有要保留的舊包袱。
  2. 開發人員希望事情在未來保持簡單。
  3. 問題:你對領域邊界的認識還非常模糊:你不知道你的軟件實際上想要做什麼(提示:敏捷)。

於是就產生了各種不同的方法,公司可以使用它們嘗試處理全新的Java微服務項目。

技術型微服務架構

對於開發人員來說,馬上就會想到這樣一種方法,儘管我們強烈建議不要使用它。Hadi Hariri在IntelliJ中提出了“提取微服務(Extract Microservice)”重構功能,這一點很是值得稱道。

下面的例子做了極度的簡化,但實際項目中的情況卻與之非常接近。

微服務之前

@Service
class UserService {
    public void register(User user) {
        String email = user.getEmail();
        String username =  email.substring(0, email.indexOf("@"));
        // ...
    }
}

使用了一個substring 的Java微服務

@Service
class UserService {
    @Autowired
    private HttpClient client;
    public void register(User user) {
        String email = user.getEmail();
        // 在這裏,通過http調用substring微服務
        String username =  httpClient.send(substringRequest(email), responseHandler());
        // ...
    }
}

於是,你實際上是將一個Java方法調用包裝成一個HTTP調用,而這麼做並沒有特別明顯的理由。而一個可能的原因是:缺乏經驗而試圖強行採用Java微服務方法。
建議:不要這樣做。

面向工作流的微服務體系架構

下一個常見的方法是,在工作流之後對Java微服務進行模塊化。

舉個現實生活中的例子:在德國,當你去看(公共)醫生時,他需要在他的健康軟件CRM中把你的預約記錄下來。

爲了讓保險報銷,他將把你的治療數據和他所治療的所有其他患者的數據通過XML發送給仲裁機構。

仲裁機構會看一下那個XML文件並做出處理(已做簡化):

  1. 驗證該文件是否是正確的XML
  2. 驗證它的合理性:一個1歲大的孩子每天從婦科醫生做三次牙齒清潔,這合理嗎?
  3. 使用其他一些形式數據對XML加以補充
  4. 將XML轉發給保險以觸發報銷
  5. 然後向醫生反饋結果,其中包括“成功”的消息或“數據有異常,請重新覈實修改後再次發送”

現在,如果你嘗試使用微服務對這個工作流進行建模,至少會包括以上內容。

注意:在本例中,微服務之間的通信與主題無關,但如果真要提一下的話,可以通過RabbitMQ之類的消息代理異步完成,因爲醫生不需立即得到反饋。

同樣的,從紙面上看,這似乎看起來挺不錯的,但我們馬上會發現以下幾個問題:

  • 你覺得需要部署六個應用程序來處理一個xml文件嗎?
  • 這些微服務真的相互獨立嗎?它們每一個可以獨立部署嗎?每個都具有不同的版本和API模式?
  • 如果驗證微服務停了,那麼合理性微服務會做什麼?系統還會保持運行嗎?
  • 這些微服務現在是否共享同一個的數據庫(它們確實需要數據庫表中的一些公共數據),或者你是否會採取更大的動作來爲它們提供屬於自己的數據庫?
  • 以及,基礎設施或運維方面其他的大量問題。

有趣的是,對於一些架構師來說,上面的圖理解起來更簡單,因爲現在每個服務都有它確切的、定義良好的用途。以前,它看起來像這個可怕的大型獨體應用:

雖然這些圖畫起來簡單,但是你肯定需要解決些額外的運維挑戰。

你……

  • 不僅需要部署一個應用程序,而是需要至少部署六個。
  • 甚至可能需要部署多個數據庫,這取決於你希望微服務體系架構走多遠。
  • 必須確保每個系統都保持在線、健康和工作。
  • 必須確保微服務之間的調用實際上是有彈性的(參見如何使Java微服務具有彈性?
  • 以及這麼部署帶來的一切差異——從本地開發配置到集成測試。

建議

除非:

  • 你是Netflix(但顯示,你不是)……
  • 你擁有超強的運維技能包:你打開開發IDE,就會跳出一隻調皮猴,刪掉你的生產數據庫,你能輕易在5秒內自動恢復。
  • 或者你覺得自己像@monzo,僅僅因爲認爲自己能行,就嘗試了1500個微服務。

否則:

不要這麼做。

儘管,沒那麼誇張。

嘗試根據領域邊界對微服務建模是一種非常明智的方法。但是,領域邊界(比如用戶管理和發票)並不意味着拿來一條工作流將其分解爲幾個最小的部分(接收XML、驗證XML、轉發XML)。

因此,每當你開始一個新的、領域邊界還非常模糊的Java微服務項目時,且領域邊界仍然非常模糊,請儘量保持微服務的規模。你總是能夠在之後添加更多模塊的。

確保在整個團隊/公司/部門都擁有非常強大的DevOps技能,以支持你新的基礎架構。

多語言或面向團隊的微服務架構

還有第三種,幾乎是以自由意志主義的方法來開發微服務:讓你的團隊甚至個人有可能使用他們想用的任何語言或微服務來實現用戶故事(行業術語:多語言編程)。

因此,上面的合理性微服務是用Haskell編寫的(爲了讓它看上去更數學),保險的轉發微服務應該用Erlang編寫(因爲它確實需要擴展),而XML驗證服務可以用Java編寫。

從開發人員的角度來看很有趣的東西(即在一套隔離的環境中使用你的完美語言開發一個完美的系統),基本上不是組織想要的同質化和標準化。

這意味着,應該有一套相對標準化的語言、庫和工具,這樣即便你不在了,其他開發人員將來也可以繼續維護Haskell微服務。

有趣的是,回溯歷史可以發現,標準化走得太遠了。某些財富500強公司甚至不允許他們的開發人員使用Spring,因爲它“不在公司的技術藍圖中”。

建議:

如果你打算使用多語言,請嘗試減少同一編程語言生態系統中的多樣性。例如:Kotlin和Java(它們都基於JVM,彼此之間100%兼容),而不是Haskell和Java。

Java微服務的部署和測試

請快速回顧一下本文開頭提到的基礎知識,這對本節會有所幫助。任何服務器端的Java程序,都是.jar或.war文件,因此也包括微服務。

在Java生態系統(更確切地說是JVM)中,有一件事情很棒:只寫一次Java代碼,基本上就可以在任何你想要的操作系統上運行,只要你用來編譯代碼的JVM版本不高於運行代碼的JVM版本即可。

理解這一點很重要,尤其是涉及到Docker、Kubernetes或雲這樣的主題時。爲什麼呢?讓我們看看以下幾個不同的部署場景:

一個簡單的Java微服務部署示例

我們繼續以上文的銀行系統爲例,我們現在有一個monobank.jar文件(那個大型獨體應用)和新提取的riskengine.jar(第一個微服務)。

我們假設這兩個應用程序與世界上的任何其他應用程序一樣,都需要.properties文件,裏面保存數據庫url和憑證。

因此,最簡單的部署可能只包含兩個目錄,大致如下:

-r-r------ 1 ubuntu ubuntu     2476 Nov 26 09:41 application.properties
-r-x------ 1 ubuntu ubuntu 94806861 Nov 26 09:45 monobank-384.jar
ubuntu@somemachine:/var/www/www.monobank.com/java$ java -jar monobank-384.jar
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
...

-r-r------ 1 ubuntu ubuntu     2476 Nov 26 09:41 application.properties
-r-x------ 1 ubuntu ubuntu 94806861 Nov 26 09:45 risk-engine-1.jar
ubuntu@someothermachine:/var/www/risk.monobank.com/java$ java -jar risk-engine-1.jar
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
...

現在,還剩下一個問題:如何將.properties和.jar文件放到服務器上呢?
不幸的是,這個問題的答案可就多嘍。

使用構建工具、SSH & Ansible 進行Java微服務部署

對於Java微服務的部署,最無聊但又完美的答案是過去20年中管理員爲公司部署Java服務器端程序的方式。它包括:

  • 你最喜歡的構建工具(Maven、Gradle)
  • 古老但好用的SSH/SCP,用於將你的jar包複製到服務器
  • 用於管理部署腳本和服務器的Bash腳本
  • 或者一些更好的:Ansible 腳本。

如果你並不想自動處理所有的負載均衡,隨時防備着調皮猴的攻擊,時時關注着ZooKeeper的leader選舉,那麼這種配置就足以應付很長時間了。

陳腐老舊、毫無新意,但的確有效。

如何使用Docker進行Java微服務部署

轉回到這個誘人的選擇。幾年前,出現了Docker和容器化的主題。

如果你以前沒有使用過它,那麼可以先了解一下它對最終用戶或開發人員的意義所在:

  1. 容器(簡化過的)就像一個古老的虛擬機,但更輕量級。看看這個Stackoverflow上的回答,理解一下輕量級在這個上下文中意味着什麼。
  2. 容器保證是可移植的,它可以在任何地方運行。這一點是不是有點耳熟?

有趣的是,由於JVM的可移植性和向後兼容性,這個好處聽起來好像沒那麼了不起。你可以在任何服務器、樹莓派(甚至是移動電話)上下載一個JVM.zip文件,解壓縮後運行任何你想要運行的.jar文件。

但是,對於PHP或Python之類的語言來說,情況就有點不同了,因爲這些語言的版本不互相兼容或其部署配置歷來都比較複雜。

或者,如果你的Java應用程序依賴於大量其他要安裝好的服務(使用正確的版本號):比如像Postgres之類的數據庫或者像Redis之類的鍵值存儲。

所以,Docker對於Java微服務,或者說Java應用程序的主要好處在於:

  • 使用像Testcontainers這樣的工具來搭建同質化的測試或集成環境。
  • 使複雜的部署“更容易”。以 Discourse 論壇軟件爲例。你可以用一個Docker鏡像部署它,它包含了你需要的所有東西:從用Ruby編寫的Discourse 軟件,到Postgres數據庫,再到Redis和幾乎所有的一切。

如果你想在開發機上運行一個小巧的Oracle數據庫,那麼試試Docker吧。

所以總結來說,現在不再是簡單地scp一個.jar文件,而是:

  • 將jar文件打包成Docker鏡像
  • 將該docker鏡像傳輸到一個私有的docker註冊表
  • 在目標平臺上拉取該鏡像,然後運行它
  • 或者,將Docker鏡像直接scp到你的生產系統,然後運行它

如何使用Docker Swarm或Kubernetes來部署Java微服務

假設你正在嘗試Docker。現在每次部署Java微服務時,你都要創建一個Docker鏡像,它綁定了你的.jar文件。你有若干這樣的Java微服務,你希望將這些服務部署到若干機器上:即集羣。

那麼問題來了:如何管理集羣,也就是運行Docker容器、執行健康檢查、發佈更新、擴展,等等等等?

答案可能有兩個:Docker Swarm和Kubernetes。

由於篇幅所限,本指南不可能詳細介紹它們,但實質上:它們最終都是基於你編寫YAML文件來管理你的集羣(參見本文“不是問題:YAML縮進的故事”)。如果你想知道在實踐中大概怎麼做,可以簡單在網上搜一搜。

那麼,Java微服務的部署過程現在看起來可能是這樣的:

  • 安裝和管理Docker Swarm/Kubernetes
  • 執行上面所述的Docker 步驟
  • 編寫和執行YAML,直到所有的東西都工作正常

如何測試Java微服務

假設你解決了在生產環境中部署微服務的問題,但是在開發過程中如何集成測試微服務呢?如何查看完整的工作流是否工作正常,而不僅僅是單一的局部呢?

在實踐中,你會找到三種不同的方法:

  1. 做一點點額外的工作(如果你正在使用Spring之類的框架),你可以把你所有的微服務包裝成一個運行器類,使用一個Wrapper.java類來啓動所有的微服務,當然,前提是你的機器上有足夠的內存來運行所有的微服務。
  2. 你可以嘗試在本地也安裝一套Docker Swarm 或Kubernetes。
  3. 不再在本地進行集成測試,而是搭建一套專用的開發/測試環境。很多團隊實際上都是這麼做的,以避免承受在本地部署微服務之痛。

此外,除了Java微服務之外,你可能還需要一個運行一個消息代理(比如:ActiveMQ或RabbitMQ),或者一個電子郵件服務器或任何其他消息傳遞組件,你的Java微服務需要通過這些組件來彼此通信。

可見,DevOps方面的複雜度被大大低估了。可以瞭解一下微服務測試類庫,能在這方面對你有所幫助。

原文鏈接:

Java Microservices: A Practical Guide

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章