隨著業務需求的變化和系統規模的擴大,傳統的單體式應用程式在開發、佈署和擴充套件方面逐漸顯露出侷限性。為了應對這些挑戰,微服務架構應運而生,它將應用程式拆解成一系列小型、獨立的服務,每個服務負責特定的功能,並透過 API 進行通訊。這種架構提高了系統的靈活性、可擴充套件性和可維護性,但也引入了新的挑戰,例如服務間通訊的複雜性和資料一致性問題。同時,不可變佈署策略的興起,結合容器化技術和反向代理,為微服務的佈署和更新提供了更可靠和高效的解決方案,實作了零停機時間佈署和快速回復。然而,在實際應用中,需要根據具體情況選擇合適的微服務拆分策略和佈署方案,並權衡微服務架構的優勢和挑戰。

系統架構

從這裡開始,本文將聚焦於一個大型專案。我們將經歷從開發到生產佈署及監控的所有階段。每個階段開始時,我們都會討論不同的實作路徑,並根據需求選擇最佳方案加以實施。目標是學習能夠應用於您自身專案的技術,因此請隨意調整指示以符合您的需求。

如同大多數其他專案,本專案始於高層級的需求。我們的目標是建立一個線上商店。目前已知的是,銷售書籍具有優先權。我們需要設計服務和Web應用程式,使其能夠輕易擴充套件。目前尚未獲得完整的計畫,但我們知道除了書籍外,還將銷售其他型別的商品,並且會有購物車、註冊和登入等其他功能。我們的任務是開發書店系統,並能夠快速回應未來的需求。由於這是一個新的嘗試,初期預期流量不高,但我們需要準備好在服務成功時能夠輕易且快速地擴充套件。我們希望能夠在無停機時間的情況下快速推出新功能,並能夠從失敗中還原。

讓我們開始著手架構設計。很明顯,目前的需求非常籠統,沒有提供太多細節。這意味著我們需要為未來可能發生的變化和新功能請求做好準備。同時,業務需求我們建立一個小型但可擴充套件的系統。我們該如何解決這些問題?

首先,我們需要決定如何定義即將構建的應用程式的架構。哪種方法能夠允許未來的方向變化、額外的(目前未知)需求以及擴充套件需求?我們應該從檢查應用程式架構中最常見的兩種方法開始:單體架構和微服務架構。

單體式應用程式

單體式應用程式被開發和佈署為單一單元。以Java為例,通常會產生單一的WAR或JAR檔案。對於C++、.Net、Scala等許多其他程式語言,也有類別似的情況。

大多數軟體開發的簡短歷史都標誌著我們正在開發的應用程式規模不斷增加。隨著時間的推移,我們不斷為應用程式新增更多功能,不斷增加其複雜性和規模,並降低我們的開發、測試和佈署速度。

我們開始將應用程式劃分為多個層次:表示層、業務層、資料存取層等。這種劃分更多是邏輯上的而非物理上的,每一層都傾向於負責一種型別的操作。這種架構通常提供了即刻的好處,因為它明確了每一層的責任。我們在高層次上實作了關注點的分離。一段時間內,大家都覺得生活很美好。生產力提高了,上市時間縮短了,整體程式碼函式庫的清晰度也更好了。

圖 3-1:單體式應用程式

隨著時間的推移,我們的應用程式所需支援的功能數量不斷增加,複雜性也隨之增加。UI層的一個功能需要與多個業務規則進行通訊,而這些規則又需要多個DAO類別來存取多個不同的資料函式庫表格。無論我們多麼努力,各層內部的細分和層之間的通訊變得越來越複雜,給予足夠的時間,開發人員開始偏離最初的道路。畢竟,最初的設計往往無法透過時間的考驗。因此,對某一層的某一部分的修改往往變得更加複雜、耗時且具有風險,因為它們可能會影響系統的許多不同部分,且往往具有不可預見的後果。

圖 3-2:功能增加的單體式應用程式

隨著時間的推移,情況開始變得更糟。在許多情況下,層數增加了。我們可能會決定新增一個具有規則引擎的層、API層等。通常情況下,各層之間的流程是強制性的。這導致了這樣的情況:我們可能需要開發一個簡單的功能,在不同的情況下只需要幾行程式碼,但由於我們的架構,這幾行程式碼卻變成了數百甚至數千行,因為所有層都需要透過。

開發並不是唯一受到單體式架構影響的領域。每當有變化或發布時,我們仍然需要測試和佈署所有內容。在企業環境中,測試、建置和佈署應用程式需要數小時並不罕見。測試,尤其是迴歸測試,往往是一場噩夢,在某些情況下會持續數月。隨著時間的推移,我們修改隻影響一個模組的能力正在下降。層的主要目標是使其能夠輕易被替換或升級。這一承諾幾乎從未真正實作。在大型單體式應用程式中,更換任何東西都很難且有風險。

擴充套件單體式應用程式通常意味著擴充套件整個應用程式,從而導致資源利用非常不平衡。如果我們需要更多的資源,我們不得不將所有內容在新的伺服器上複製一份,即使瓶頸只是一個模組。在這種情況下,我們最終往往會在多個節點上複製單體式應用程式,並在頂部加上負載平衡器。這種設定充其量只是次優的。

圖 3-3:擴充套件單體式應用程式

水平分割服務

導向服務的架構(SOA)被創造出來作為解決由經常緊密耦合的單體式應用程式所產生的問題的一種方法。該方法根據四個我們應該實作的主要概念。

  • 界限是明確的
  • 服務是自治的
  • 服務分享模式和契約,但不分享類別
  • 服務的相容性根據策略

SOA曾經非常流行,以至於許多軟體供應商紛紛加入並創造出能夠幫助我們進行轉型的產品。其中最常用的型別是企業服務匯流排(ESB)。同時,經歷了單體式應用程式和大型系統問題的公司也紛紛跳上這趟列車,並以ESB作為頭車開始了SOA轉型。

內容解密:

本段落主要討論了系統架構的不同方法。首先介紹了單體式應用程式的概念及其優缺點,接著討論了導向服務的架構(SOA)及其主要概念。SOA是一種用於解決單體式應用程式問題的方法,其核心思想是將服務進行水平分割,使其具有明確的界限、自治性、分享模式和契約等特性。同時,也提到了企業服務匯流排(ESB)作為SOA轉型中的重要工具。

此圖示說明瞭單體式應用程式和麵向服務的架構之間的對比,以及企業服務匯流排在SOA轉型中的作用。

內容解密:

此圖使用Plantuml圖表呈現了單體式應用程式與導向服務架構(SOA)之間的比較。可以看出,單體式應用程式往往導致維護困難和資源利用不平衡的問題,而SOA透過明確界限、自治服務、分享模式和契約以及根據策略的相容性來解決這些問題。同時,企業服務匯流排(ESB)在幫助企業進行SOA轉型、提高系統靈活性方面發揮了重要作用。

微服務架構的演進與實踐

微服務架構的發展源自於對傳統單體式應用程式(Monolithic Application)的反思與改進需求。過去,隨著系統規模的擴大,單體式架構逐漸暴露出諸多問題,如開發、佈署、擴充套件的困難等。為瞭解決這些問題,服務導向架構(SOA)被提出,但其實施過程中出現了諸如企業服務匯流排(ESB)產品變得過於龐大和複雜等問題。微服務架構正是在這種背景下應運而生。

從 SOA 到微服務

SOA 的初衷是將應用程式分解為多個服務,以提高系統的靈活性與可擴充套件性。然而,在實際實施過程中,許多企業選擇了 ESB 產品來實作服務之間的溝通與協調,但這往往導致新的單體式應用程式的出現。微服務架構則是對 SOA 思想的繼承與革新,其核心在於將應用程式拆解為一系列小型、獨立的服務,每個服務負責特定的功能,並透過定義良好的 API 進行資料交換。

微服務的特點

微服務架構具有以下幾個關鍵特點:

  • 每個微服務負責單一功能或業務邏輯
  • 微服務之間是松耦合的,各自獨立開發、測試和佈署
  • 可以使用不同的技術堆疊和程式語言開發不同的微服務
  • 支援 DevOps 實踐,方便持續交付和佈署
  • 系統的可擴充套件性和彈性大大提高

微服務實作解密:

微服務架構要求每個服務都能夠獨立運作,這意味著每個服務都需要有自己的資料儲存方案。雖然可以使用集中式資料儲存,但分散式資料儲存也是一種可行的選擇。將資料儲存在各自的服務中,可以進一步提高系統的松耦合度。

微服務的優勢與挑戰

微服務架構的優勢在於其能夠提高開發效率、系統可擴充套件性和彈性。然而,在採用微服務架構時,也需要考慮到諸如服務之間的通訊、資料一致性、系統監控等挑戰。

微服務的實施策略

對於已經存在的單體式應用程式,可以透過逐步演進的方式引入微服務架構。首先,可以將新的功能模組開發為微服務,然後將現有的單體式應用程式逐步拆解為微服務。這種方式可以降低遷移的風險,並充分發揮微服務架構的優勢。

輕量級代理伺服器的角色

在微服務架構中,輕量級代理伺服器扮演著重要的角色。它負責協調來自外部或內部的請求,並將請求路由到相應的微服務。透過使用輕量級代理伺服器,可以簡化微服務之間的通訊,並提高系統的可擴充套件性。

此圖示說明瞭單體式應用程式如何拆解為多個微服務,並透過輕量級代理伺服器進行請求路由。

隨著技術的不斷進步,微服務架構將繼續演進。未來,我們可以預期看到更多創新性的解決方案,以應對微服務架構所帶來的挑戰。同時,如何更好地實施微服務架構,將是企業和開發團隊需要持續關注的問題。

微服務與單體式應用程式架構比較

在瞭解單體式應用程式與微服務的基本概念後,我們來比較兩者的優缺點。儘管微服務在許多情況下是更好的選擇,但並非所有情況下都是如此。微服務有其自身的缺點,例如增加維運和佈署的複雜性,以及遠端程式呼叫所帶來的效能問題。

維運和佈署複雜性

反對微服務的主要論點是其增加的維運和佈署複雜性。這個論點有一定的正確性,但可以透過相對較新的工具來緩解。組態管理(CM)工具可以輕鬆處理環境設定和佈署。使用 Docker 容器化技術可以顯著減少微服務佈署所帶來的問題。CM 工具與容器化技術結合,可以快速佈署和擴充套件微服務。

內容解密:

  • 組態管理(CM)工具:用於自動化管理伺服器組態和佈署流程的工具,例如 Ansible、Puppet 或 Chef。
  • Docker 容器化技術:用於封裝應用程式及其依賴項的技術,確保在不同環境中的一致性。

遠端程式呼叫

另一個支援單體式應用程式的論點是微服務的遠端程式呼叫會降低效能。內部呼叫透過類別和方法比遠端呼叫更快,這是無法避免的問題。這種效能損失的影響取決於具體情況。如果將系統劃分為極小的微服務(有人建議每個微服務不應超過 10-100 行程式碼),這種影響可能會很大。比較好的做法是根據有界上下文或功能(如使用者、購物車、產品等)來建立微服務,這樣可以減少遠端程式呼叫的次數。

微服務的優勢

那麼,微服務相對於單體式應用程式有哪些優勢呢?以下列出的優勢並非詳盡無遺,也不是微服務獨有的,但它們在微服務架構中更為突出。

擴充套件性

微服務比單體式應用程式更容易擴充套件。對於單體式應用程式,我們需要將整個應用程式複製到新的機器上。另一方面,對於微服務,我們只需要複製需要擴充套件的服務。這樣不僅可以擴充套件需要的部分,還可以更好地分配資源。例如,可以將 CPU 使用率高的服務與記憶體使用率高的服務放在一起,同時將另一個 CPU 使用率高的服務移到不同的硬體上。

創新

單體式應用程式一旦初始架構確定後,就不容易進行創新。可以說,單體式應用程式扼殺了創新。由於其本質,進行變更需要時間,而實驗是危險的,因為它可能影響到整個系統。使用微服務,可以為每個服務選擇最合適的解決方案,不同的服務可以使用不同的技術堆疊,如 Apache Tomcat 或 NodeJS、Java 或 Scala 等。

大小

由於微服務較小,因此更容易理解。需要閱讀的程式碼量較少,這大大簡化了開發過程,尤其是對於新加入專案的人員。此外,其他一切都變得更快了。IDE 在小型專案上的工作速度比在大型單體式應用程式上更快,因為不需要載入龐大的伺服器和大量的函式庫。

佈署、回復和故障隔離

使用微服務,佈署更快、更容易。佈署小型服務總是比佈署大型應用程式更快、更容易。如果發現問題,這個問題的影響範圍有限,可以更容易地回復。在回復之前,故障被隔離在系統的一小部分。持續交付或佈署可以以更快的速度和更高的頻率進行。

承諾期限

單體式應用程式的一個常見問題是承諾。我們通常需要在一開始就選擇將持續很長時間的架構和技術。使用微服務,這種長期承諾的需求大大減少。可以更改某個微服務的程式語言,如果證明是好的選擇,再將其應用到其他服務。如果實驗失敗或不是最佳選擇,則只需要重新做系統的一小部分。

佈署策略

我們已經討論了持續交付和佈署策略需要我們重新思考應用程式生命週期的所有方面。在進行架構選擇時,這一點最為明顯。我們將重點放在兩個主要的決策上:一個是架構相關的,在單體式應用程式和微服務之間進行選擇;另一個是與如何封裝待佈署的工件相關,即是否應該執行可變或不可變的佈署。

此圖示說明瞭單體式應用程式與微服務之間的比較及其各自的特點。

可變的龐然大物伺服器(Mutable Monster Server)與不可變佈署(Immutable Deployments)

現今,應用程式的構建與佈署大多採用可變的龐然大物伺服器模式。我們建立一個包含完整應用程式的網頁伺服器,每當有新版本釋出時就對其進行更新。變更可能涉及組態(如屬性檔案、XML 檔、資料函式庫表格等)、程式碼構件(如 JAR、WAR、DLL、靜態檔案等)以及資料函式庫結構和資料。由於每次釋出都會進行變更,因此這種伺服器是可變的。

可變伺服器的問題

在可變伺服器的架構下,我們無法確定開發、測試和生產環境是否完全相同。甚至生產環境中的不同節點也可能存在不應有的差異。程式碼、組態或靜態檔案可能未在所有例項中更新。

這種伺服器被稱為「龐然大物」,因為它包含了我們所需的一切,是一個單一例項。它包含了後端、前端、API 等,並且隨著時間的推移不斷增長。隨著時間的流逝,沒有人能夠確切知道生產環境中所有元件的組態細節。要在其他地方(如新的生產節點、測試環境等)準確重現生產環境,通常只能複製虛擬機器並對組態進行調整(如 IP 位址、主機檔案、資料函式庫連線等)。我們不斷地對其進行修改,直到失去對其內容的追蹤。只要時間足夠,你原本「完美」的設計和令人印象深刻的架構就會變成完全不同的東西。新層被新增,程式碼被耦合在一起,補丁疊加在補丁上,人們開始在程式碼迷宮中迷失方向。你的小專案逐漸變成了一個龐大的怪物。你曾經引以為傲的作品現在成了人們茶餘飯後的談資。人們開始說,最好的做法是將其丟棄並重新開始。但這個怪物已經太龐大,無法重新開始。投入了太多資源,重寫需要太多的時間,風險也太大。我們的單體架構可能會繼續存在很長一段時間。

圖 3-5:最初設計的可變應用伺服器

此圖示展示了最初設計的可變應用伺服器架構,其中包含了後端、前端、API 等多個元件,並且會對組態、程式碼構件和資料函式庫進行更新。

可變佈署看似簡單,但實際上並非如此。透過將所有內容耦合在一起,我們隱藏了複雜性,從而增加了不同例項之間出現差異的可能性。

內容解密:

  1. 可變伺服器的更新問題:每次更新都可能引入新的問題,且難以保證所有例項的一致性。
  2. 龐然大物的風險:隨著時間的推移,伺服器的複雜度增加,維護難度加大。
  3. 圖表說明:此圖示清晰地展示了可變應用伺服器的架構及其更新內容,有助於理解其複雜性。

重新啟動這樣一個伺服器以接收新版本所需的時間可能相當可觀。在此期間,伺服器通常無法運作。新版本釋出所導致的停機時間意味著金錢和評價的損失。如今的業務期望我們能夠 24/7 不間斷運作,而釋出新版本到生產環境往往意味著團隊需要在夜間工作,期間我們的服務不可用。在這種情況下,實施持續佈署(Continuous Deployment)簡直是遙不可及的夢想。

測試也是個問題。無論我們在開發和測試環境中對新版本進行了多少測試,第一次在生產環境中佈署並對所有使用者開放時仍然會遇到問題。此外,這種伺服器的快速回復幾乎是不可能的。由於它是可變的,除非我們建立整個虛擬機器的快照(這會帶來一系列新的問題),否則沒有前一版本的「快照」。

這樣的架構無法滿足前述的任何要求。由於無法實作零停機時間和輕鬆回復,我們無法頻繁佈署。由於其架構的可變性,全自動化佈署存在風險,從而阻礙了我們實作快速佈署。

不可變伺服器與反向代理

每個「傳統」的佈署都會引入與伺服器變更相關的風險。如果我們將架構改為不可變佈署(Immutable Deployments),就能立即獲得好處。環境的準備變得更加簡單,因為我們不再需要考慮應用程式的變更(它們是不可變的)。每當我們將映像或容器佈署到生產伺服器時,我們都知道它與我們構建和測試的完全相同。

不可變佈署減少了與未知相關的風險。我們知道每個已佈署的例項都與其他例項完全相同。不像可變佈署,當一個套件是不可變的並且包含所有必要內容(應用伺服器、組態和構件)時,我們不再需要關心這些事情。它們已經在佈署Pipeline中為我們封裝好了,我們只需要確保將不可變套件傳送到目標伺服器即可。它與我們在其他環境中測試的套件相同,可變佈署可能引入的不一致性已經消失。

不可變佈署流程

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title 微服務架構設計與實踐

package "Docker 架構" {
    actor "開發者" as dev

    package "Docker Engine" {
        component [Docker Daemon] as daemon
        component [Docker CLI] as cli
        component [REST API] as api
    }

    package "容器運行時" {
        component [containerd] as containerd
        component [runc] as runc
    }

    package "儲存" {
        database [Images] as images
        database [Volumes] as volumes
        database [Networks] as networks
    }

    cloud "Registry" as registry
}

dev --> cli : 命令操作
cli --> api : API 呼叫
api --> daemon : 處理請求
daemon --> containerd : 容器管理
containerd --> runc : 執行容器
daemon --> images : 映像檔管理
daemon --> registry : 拉取/推送
daemon --> volumes : 資料持久化
daemon --> networks : 網路配置

@enduml

此圖示展示了使用反向代理和不可變應用映像的佈署架構,確保了佈署的一致性和可靠性。

內容解密:

  1. 不可變佈署的優勢:透過使用不可變佈署,我們可以確保生產環境與測試環境的一致性,降低了風險。
  2. 反向代理的作用:反向代理用於路由流量到不可變應用映像,實作零停機時間佈署。
  3. 圖表說明:此圖示清晰地展示了不可變佈署的架構,有助於理解其工作原理和優勢。

透過使用反向代理,我們可以實作零停機時間。不可變伺服器與反向代理結合,可以簡化為以下形式:首先,我們使用一個指向完全自足的不可變應用套件的反向代理。這個套件可以是虛擬機器或容器。我們將這個應用稱為應用映像,以與可變應用區分開來。在應用之上是一個代理服務,它將所有流量路由到最終目的地,而不是直接暴露伺服器。

不可變佈署與微服務架構的最佳實踐

不可變佈署(Immutable Deployment)是一種透過佈署新的映像檔(Image)到新的伺服器來實作零停機時間(Zero-Downtime)佈署的技術。這種方法確保了新版本的應用程式在不影響使用者的情況下進行測試和驗證。一旦新版本透過測試,反向代理(Reverse Proxy)就會被組態為將流量路由到新版本,而舊版本則保留一段時間以備需要回復。

不可變佈署的步驟

  1. 佈署新版本:將新版本的應用程式佈署為一個新的映像檔到一個新的伺服器。
  2. 測試新版本:在新版本上執行測試,包括自動化測試和手動測試,以確保其正常運作。
  3. 切換流量:一旦新版本透過測試,將反向代理組態為將流量路由到新版本。
  4. 移除舊版本:在確認新版本穩定後,移除舊版本。

微服務架構的優勢

微服務架構透過將應用程式分解為小的、獨立的服務,提高了開發、測試和佈署的效率。每個微服務可以獨立佈署、擴充套件和管理,從而提高了整體系統的靈活性和可擴充套件性。

微服務佈署

微服務的佈署遵循與不可變佈署相同的模式。每個微服務被佈署為一個不可變的映像檔,可以獨立於其他服務進行更新和擴充套件。

微服務最佳實踐

  1. 容器化:使用容器技術(如 Docker)來封裝微服務,以簡化佈署和管理。
  2. 獨立佈署:每個微服務應該能夠獨立於其他服務進行佈署和更新。
  3. 自動化測試:實施自動化測試,以確保每個微服務的品質和穩定性。

程式碼範例:Dockerfile 用於構建微服務映像檔

# 使用官方 Python 映像檔作為基礎
FROM python:3.9-slim

# 設定工作目錄
WORKDIR /app

# 複製 requirements.txt 檔案到工作目錄
COPY requirements.txt .

# 安裝依賴項
RUN pip install --no-cache-dir -r requirements.txt

# 複製應用程式碼到工作目錄
COPY . .

# 暴露應用程式的埠
EXPOSE 8000

# 執行應用程式
CMD ["python", "app.py"]

內容解密:

  1. FROM python:3.9-slim:使用官方 Python 3.9 映像檔作為基礎映像檔,以減少映像檔大小。
  2. WORKDIR /app:設定容器內的工作目錄為 /app
  3. COPY requirements.txt .:將 requirements.txt 檔案從主機複製到容器內的工作目錄。
  4. RUN pip install --no-cache-dir -r requirements.txt:安裝 requirements.txt 中指定的 Python 依賴項。
  5. COPY . .:將當前目錄下的所有檔案複製到容器內的工作目錄。
  6. EXPOSE 8000:宣告容器將監聽 8000 埠。
  7. CMD ["python", "app.py"]:設定容器的預設命令為執行 python app.py