歡迎光臨
每天分享高質量文章

從初創公司的角度來看微服務

在開展微服務的過程中,瞭解要考慮哪些因素可能是非常有挑戰性的事情。沒有可以直接使用的金科玉律。每個過程都是不同的,因為每個組織面臨的都是不同的環境。在本文中,我將從初創公司的角度分享我們學習到的經驗和麵臨的挑戰,以及我下次引入微服務時,會在哪些方面採取不同的做法。

核心要點

  • 從一個易於抽取的小候選功能開始,以便於儘早獲得微服務的體驗;
  • 要預先重點關註構建和部署自動化以及監控;
  • 儘早處理橫切性的關註點,避免給生產效率帶來負面的影響,比如為單體應用繼續增加功能或者為每個微服務重新實現橫切性的關註點;
  • 將系統的事件驅動功能設計得易於演化,考慮採用事件流的方案以減少資料副本的成本並降低添加新微服務的門檻;
  • 需要註意,轉換至微服務的過程並不是獨立運轉的。相反,它受到很多環境因素的影響。當心那些阻礙你前進或拖你後腿的環境因素,對它們進行相應的調整,或者至少要在整個組織中意識到這些問題。

在開展微服務的過程中,瞭解要考慮哪些因素可能是非常有挑戰性的事情,對於小團隊來講更是如此。遺憾的是,沒有可以直接使用的金科玉律。每個過程都是不同的,因為每個組織面臨的都是不同的環境。在本文中,我將從初創公司的角度分享我們學習到的經驗和麵臨的挑戰,以及我下次引入微服務時,會在哪些方面採取不同的做法。

從單體應用到微服務的旅程該如何開始?

最初,從各個方面看,我都是從單體應用開始的:我們整個團隊基於一個相互協作的產品開展工作,將其實現為同一個代碼庫並且基於同一個技術棧。在一段時間內,這種方式能夠很好地運轉。

隨著時間的推移,所有的事情都在演化:團隊在增長,我們為產品不斷添加越來越多的特性,代碼庫變得越來越大,用戶的數量也在不斷增長。這聽起來非常不錯,對吧?但是……

現在,要完成一件事情需要非常長的時間:會議、討論和決策都要比以往消耗更長的時間。職責無法清晰地劃分,明確具體責任需要花費一定的時間,比如當出現了 bug 的時候。我們的過程變得更加緩慢,生產效率也受到了影響。

我們添加的特性越多,產品使用起來就越複雜。產品的可用性和用戶體驗因為不斷的特性修改而受損。我們不但沒有很好地解決用戶的問題,反而讓他們更加困惑。

因為採用單體軟體架構,我們很難在不影響整個系統的情況下添加新的特性,釋放新的變更也變得非常複雜,即便我們只修改了幾行代碼,也需要重新構建和部署整個產品。這導致部署會具有很高的風險性,因此部署的頻率也不那麼頻繁,因為新特性的發佈非常緩慢。

因此,對系統進行分離和轉換的需求就出現了。

在三年前,我們改變了產品策略。我們關註可用性和用戶體驗的提升並將我們的產品 JUST SOCIAL 拆分成了多個獨立的應用,其中每個應用負責特定的場景。我們不斷演化這個理念,提供不同的應用來共享文件、實時交流、管理任務、共享可編輯的內容和協作的新聞以及管理 profile。

同時,我們將整個團隊拆分成了多個更小的團隊,併為每個團隊分派了特定的一組協作應用(collaboration app),從而實現定義了良好的職責劃分。我們想要建立自治化的團隊,能夠讓他們按照自己的節奏獨立地圍繞系統不同的組成部分開展工作,將跨團隊的影響降低到最小。

在將我們的產品拆分為多個獨立的協作應用並將團隊分為多個更小的團隊之後,接下來順理成章的步驟就是將自治性和靈活性反映到軟體架構中,這是通過引入微服務實現的。

我們引入微服務的驅動力在於讓系統的不同組成部分能夠實現自治,讓他們按照自己獨立的節奏開展工作,將跨團隊的影響降到最低。通過獨立地開發、部署和擴展協同應用,我們希望能夠快速地發佈變更。

我們的微服務之旅首先是從識別適合採取微服務的候選功能開始的。為了識別合適的候選功能,我們必須要考慮如何建模良好服務的核心概念。核心概念遵循服務間松耦合和服務內高內聚的原則。服務內的高內聚通常反映在保持相關行為的一致性方面。在領域驅動設計中,相關行為反應為限界背景關係(Bounded Context)。限界背景關係是領域模型中的語意邊界,服務會負責定義良好的一個業務功能,限界背景關係會對服務進行描述。

在我們的場景中,我們使用協作應用作為高層級的限界背景關係,它反映了粗粒度的服務邊界。這是一個很好的起點,後續我們會將它們拆分為更加細粒度的服務層。

我們首先從 JUST DRIVE 的限界背景關係開始,也就是負責文件管理的協作應用。每個文件都是由作者創建的。作者相關的資料來自 profile,而後者又是由 profile 管理的限界背景關係來進行管理的,這個功能依然位於單體應用中。

我們從頭構建了一個共存(co-existing)的服務。它實際上並不完全與當前功能的相同,相反,我們引入了新的 UI、添加了更多的特性並將資料結構做了重大的變更。新服務的限界背景關係包括負責業務邏輯的領域模型、編排用例的和管理事務的應用服務以及輸入輸出的配接器,比如 REST 端點和用於持久化管理的配接器。新服務會獨占文件狀態,也就是說,它是唯一能夠讀取和寫入文件的服務。

如前文所述,每個文件都是由作者創建的,作者的資料來源於單體應用所管理的 profile 資料。


那麼問題就來了,新服務和單體應用之間該如何交互呢?

為了避免每次展現文件的時候都從 profile 服務中獲取作者資料,我們在新的服務中保留了相關作者資料的一個本地副本。只要不破壞資料的所有權,資料冗餘是沒有問題的,在我們這個場景中,只要 profile 相關的限界背景關係依然獨占 profile 狀態即可。

由於本地副本和原始的資料會隨著時間的推移而產生差異,所以單體應用需要在 profile 更新的時候通知我們。在 profile 發生變化的時候,單體應用會發佈一個 ProfileUpdatedEvent 事件,新服務需要訂閱這個事件。新服務消費該事件並相應地更新本地副本。

這種事件驅動的服務集成方式降低了服務之間的耦合,因為我們現在不需要跨背景關係遠程直接查詢單體應用了。這種方式增加了自治性,新服務能夠對本地副本做任何事情,而且能夠讓資料連接(join)更加高效,因為它可以使用本地副本連接作者資料,無需通過網絡。

我們從頭構建了一個共存的服務,並且為了實現資料複製的目的,引入了事件驅動形式的服務交互。

我們遇到了什麼挑戰以及是如何解決的

從頭開始構建共存的服務通常是一種很好的分解策略,當你想要擺脫某些東西的束縛時,更是如此,比如想要脫離過時的業務邏輯或者現有的技術棧。但是在解耦第一個服務的時候,我們一次性做了太多的事情。如前文所述,我們不僅從頭構建了一個共存的服務,還引入了新的 UI、添加了更多的特性,還對資料結構做了重大的變更。在開始的時候,我們承擔了太多的責任,所以在很晚的時候才看到結果。但是,在開始階段,快速得到結果以獲取使用微服務的經驗和信心是非常重要的。

在下一個備選服務中,我們採取了不同的方式。我們關註 chat 應用的高層級限界背景關係,並遵循自上而下的漸進式分解策略,逐步抽取已有的代碼。我們首先將 UI 抽取為單獨的 Web 應用,併在單體應用側引入了 REST-API,這樣被抽取出來 Web 應用可以訪問該 API。在這一步,我們可以獨立地開發和部署 Web 應用,從而能夠對 UI 進行快速迭代。

在抽取完 UI 之後,我們就可以更進一步,解耦業務邏輯。分解業務邏輯會對代碼帶來重大的變更。根據依賴關係,我們可能需要提供一個臨時的 REST API 供單體應用使用,以解決業務邏輯抽取後所帶來的問題。此時,我們依然共享相同的資料儲存。

為了實現非耦合的獨立服務,我們最終需要切分資料儲存,以確保新服務能夠獨占 chat 的狀態。

在每個 chat 討論中,都會涉及到參與者。chat 參與者的資料來源於單體應用中的 profile 資料。如前面描述的 DRIVE 樣例類似,我們儲存一個 chat 參與者資料的本地副本,並訂閱 ProfileUpdatedEvent 事件,從而讓本地副本資料與單體應用中原始資料的保持同步。

從此處開始,我們就可以繼續從單體應用中抽取下一個限界背景關係,或者將我們的粗粒度服務隨後拆分為更細粒度的服務。

另外一項挑戰是對授權的處理

幾乎對於每個服務,我們都會面臨如何授權的問題。我為你描述一個背景:授權處理是非常細粒度的,一直向下延伸到領域物件級別。每個協作應用都要控制其領域物件的權限,比如文件的權限是由該文件所在的父檔案夾的授權設置來控制的。

另一方面,授權不僅僅是細粒度的,還依賴於服務之間的交互,在某些場景下,領域物件的授權還依賴於父領域物件的授權信息,而父領域物件的授權信息是位於其他服務中的,比如,要讀取某個內容頁相關的文件或者為內容頁添加文件的話,需要依賴於這個頁面的授權設置,而這個頁面的授權配置位於與文件本身不同的服務中。

因為這些複雜的需求,解決分佈式授權的問題給我們帶來了很大的困擾,而且我們沒有在早期提供解決方案。這樣帶來的結果完全適得其反。其中一個後果就是我們添加了一個新的服務到單體應用中,而單體應用其實早就已經解決過授權的問題了。我們讓單體應用變得更大了,而不是讓它變得更小。另外一個後果就是,我們開始在每個服務上都實現授權。起初,這種做法看上去是合理的,因為我們最初的假設是授權屬於領域模型所在的限界背景關係,但是我們忽略了服務之間的依賴關係。所以,我們不斷地來回覆制資料,增加了衝突的風險。

長話短說:我們最終將授權處理合併到了一個中心化的微服務中。

與中心化服務一併出現的是引入分佈式單體應用的風險。當修改系統中的某一部分時,你必須要同時修改其他的組成部分,這是已引入分佈式單體應用的強烈信號。以我們的場景為例,當引入需要授權的新協作應用時,我們需要同時修改中心化的授權服務。我們同時遇到了單體應用和分佈式應用的缺點:服務是緊耦合的,而且服務還需要通過緩慢、不穩定的網絡來進行通信。

於是,我們提供了一個通用的契約,這個契約屬於授權服務,所有的下游服務都必須要遵守該契約。在我們的場景中,服務會將授權相關的行為轉換成授權服務能夠理解的契約,授權服務不需要額外的轉換。這種轉換是在每個下游服務中發生的,而不是在中心化的授權服務中發生的。這種通用契約能夠確保我們在引入新的服務時,不需要同時修改和重新部署中心化的認證服務了。有個先決條件是這個通用的契約是穩定的,或者說至少向下兼容,否則的話,我們會將問題轉移給下游服務,這會導致它們需要不斷進行更新。

我們學習到了什麼

在開始階段需要特別註意,最好從易於提取的小型服務開始,以便於快速得到結果並獲取使用微服務的早期經驗。如果要處理粗粒度的大型服務,就我們而言,將拆分過程分為增量式的步驟會更加易於管理,例如增量式地由上到下進行分解,也就是每次只執行一個可管理的步驟。

儘早處理橫切性的關註點非常重要,這樣能夠避免適得其反的後果,比如不斷擴大單體應用而不是縮減它,或者在每個服務中都重新實現橫切性的關註點。

在引入中心化的橫切服務時,需要註意不要引入分佈式單體應用。在這種情況下,通用且穩定的契約能夠幫助我們避免出現分佈式單體應用。

要設計易於演化的系統,事件驅動的服務交互方式是實現服務間高度解耦的關鍵。事件可以用作通知,也可以用於生成資料副本(關於事件驅動的狀態轉移,參見上文關於從頭構建共存服務的內容),我們還可以通過長期保留事件將事件儲存作為主要的資料源。

當事件單純用於通知的目的時,其他背景關係中的額外資料通常會以跨背景關係查詢的方式直接進行請求,比如 REST 請求。我們可能會更喜歡遠程查詢的簡潔性,而不願處理本地維護資料集所帶來的開銷,在資料集會不斷增長的情況下更是如此。但是遠程查詢增加了服務之間的耦合性,並且在運行時將服務系結在了一起。

我們可以將對其他背景關係的遠程查詢進行內部化處理,這是通過引入相關跨背景關係資料的本地副本來實現的。如上面的 JUST DRIVE 樣例所述,為了避免每次展現文件的時候都從 profile 服務中請求相關的作者資料,我們複製了作者資料,併在文件微服務中保留了一個本地副本。我們需要保證副本資料和原始資料的同步,這意味著當原始資料變化的時候,要立即同步我們的本地副本。為了獲取已修改資料的通知,服務需要訂閱包含資料變化的事件並相應地更新本地副本。在本例中,事件是用來生成資料副本的,這樣能夠避免遠程查詢並降低服務之間的耦合性。這種方式也能實現更好的自治性,因為服務能夠對本地副本執行任何操作。

對於事件驅動服務的交互,我們在早期就引入了 Apache Kafka,這是一個分佈式、具有容錯性、可擴展的日誌提交服務。最初,我們使用 Apache Kafka 的主要目的是實現通知和生成資料副本的功能。最近,我們引入 Apache Kafka Streams 作為共享的事實源,以減少資料複製的開銷並實現服務的高可插拔性,降低新服務進入的壁壘。

流是無界有序且持續更新的結構化資料記錄組成的序列。資料記錄有一個 key-value 對組成。

當你的服務在 Apache Kafka 流背景關係中啟動時,Kafka 主題將會加載到你的流中,你可以在服務的範圍內處理它。主題通常是一個邏輯分類,表明瞭哪些服務可以發佈和訂閱。每個流都會緩衝到一個狀態儲存中,這是一個輕量級的基於硬碟的資料。加載的流會在你自己的代碼中使用,不會在 Kafka 代理中運行,它運行在你的微服務行程中。流能夠讓資料出現在任何需要的地方,這會增強性能和自治性。

Apache Kafka 提供了一個 Stream API。Stream 可以借助領域特定語言(Domain Specific Language,DSL)進行連接、過濾、分組或聚合,流中的每條訊息都可以使用類似函式的操作進行處理,比如映射、轉換或窺探等。

在實現流處理的時候,通常會同時需要流以及進行功能增強的資料庫。Kafka 的 Streams API 通過對流和表的核心抽象提供了該功能。在流和表之前其實存在緊密的關聯關係,也就是所謂的流 – 表二元性(stream-table duality)。流可以看做表的變更日誌,流中的每條資料記錄都捕獲了表中的一次狀態變更。表可以視為快照,對應於流中每個 key 的最新值。

當我們想要展現一條文件及其作者資料時,借助 Kafka Streams,我們可以這樣做:文件服務根據 document 主題創建一個 KStream,並根據 profile 主題得到的作者相關 profile 資料來完善該文件。在這個增強的過程中,文件服務會根據 profile 主題創建 KTable。現在,我們可以將流和表進行連接,並將它的結果儲存為新的狀態儲存,這樣就可以在外部進行訪問了,運行方式類似於內置的 Materialized View。每當 profile 或文件更新的時候,它相關的 Materialized View 也會進行更新。

將 Apache Kafka Streams 與其他的事件驅動方式進行對比的話,它不需要維護本地副本,這減少了維護資料副本和保持資料同步的開銷。Apache Kafka Streams 會將資料推送到需要的地方,並且運行在與服務相同的行程中。它增加了可插拔性,你可以插入新的服務並立即使用流,不需要搭建額外的資料儲存。它能夠減少開銷,增強性能、自治性並降低新服務的進入壁壘。

這個轉換的過程並不是隔離運行的,它會受到各種環境因素的影響:團隊的規模、結構和技能都會影響到怎樣做才是可控的,尤其是在開始階段,如果是一個的團隊並且 DevOps 經驗很欠缺的話,將會對轉換的速度造成一定的影響。

你的轉換過程還會受到一個因素的影響,那就是你依然要處理遺留的系統。維護它所耗費的時間會相應地減少進行轉換的時間。運行時環境也會影響這個過程。你是在內部環境中運行還是作為雲原生應用運行?你是否能夠依賴托管服務,比如托管的 API- 網關,還是需要自行搭建和維護?

如果你的策略是在短期內引入新特性的話,那麼就會面臨決策上的糾結,那就是將新需求在何處實現:如果作為新的獨立服務的話,會耗費一定的時間,如果採取快捷的方式,將其添加到單體應用上,那就會帶來讓單體應用越來越大,而不能對其進行縮減的風險。

註意那些阻礙前進或減緩速度的環境因素,並相應地調整它們,或者至少在你的組織中引起註意。記住: 每一次過程都是不同的,你的過程可能和我們的完全不同。

如果下次繼續引入微服務的話,在哪些方面的做法會有所不同

首先,我會檢查組織的戰略是否與微服務的標的相一致,那就是最大化產品的敏捷性以及獨立快速地發佈變更,例如,如果你的組織關註較長的發佈周期並希望將所有內容部署在一起,那麼微服務可能不是最佳選擇,因為無法充分利用微服務的優勢。

如果你決定採用微服務的話,每個人都必須投入其中,包括管理層。每個人都需要意識到這個過程是非常複雜和耗時的,當你還沒有多少經驗的時候更是如此。

與產品相符的、跨功能的、自治的團隊可以很好地與微服務架構樣式協作,但是應該儘早考慮向 DevOps 文化的轉變。每個團隊都應該為持續的迭代做好準備,並且能夠開發、發佈、運維和監控他們負責的服務。

將單體應用拆分成多個獨立的服務,只是整個過程的一部分,而如何運維它們則是另外一回事兒。你擁有的服務越多,它們的自動化構建和部署流程就變得越重要。

如果我重做一次的話,我將從一個易於抽取的小型候選服務開始,不僅要關註它的拆分,還要關註構建和部署的自動化,並預先監控第一個服務,它可以作為後續服務的基礎。要搭建這個基礎環境,可能需要從每個組抽取一個人形成一個臨時的任務組。

每個微服務從一開始就應該有自己的 CI/CD 管道。另一個需要考慮的問題是將每個微服務進行容器化,從而能夠得到輕量級、封裝好的運行時環境,它能夠在各個階段中保持一致,如果你以後想要在雲環境中運行服務的話,更需如此。

另外,還需要儘早考慮監控的問題,包括日誌聚合。監控不僅包括服務器,還包括服務指標,如請求延遲、吞吐量和錯誤率,以便於跟蹤服務的健康狀況和可用性。要形成結構化和標準化的日誌輸出,如時間格式(如 ISO8601)和時區(如 UTC),並引入具有 correlation id 和日誌聚合的請求背景關係,這有助於問題的診斷和剖析。

很多事情需要預先處理,這非常耗時並且需要得到整個組織的關註。微服務是實現最大化產品敏捷性的投資,而不在於削減成本。

為了保持在市場上的競爭力,產品的敏捷性和持續改進是區別於競爭對手的關鍵因素。微服務可以提升產品的敏捷性並持續改善,但是它需要每個人的貢獻,包括管理者。

關於作者

Susanne Kaiser 是來自德國漢堡的獨立技術咨詢師,她曾經擔任過初創公司的 CTO,並將該公司的 SaaS 解決方案從單體架構遷移為微服務架構。她具有計算機科學的背景,在軟體開發和軟體架構方面有超過 15 年的經驗,經常在國際性的技術會議上演講。

原文地址:https://www.infoq.cn/article/31IdBpWgTQZU7e5-uwh1


.NET社區新聞,深度好文,歡迎訪問公眾號文章彙總 http://www.csharpkit.com


 

    閱讀原文

    赞(0)

    分享創造快樂