雲原生架構下微服務最佳實踐-如何拆分微服務架構
王啟軍,目前就職於華為公司架構部,負責華為公司的Cloud Native、微服務架構推進落地,前後參與了華為手機祥雲4.0、物聯網IoT 2.0的架構設計。曾任噹噹架構師,主導電商平臺架構設計,包括訂單、支付、價格、庫存、物流等。曾就職於搜狐,負責手機微博的研發。十餘年的技術歷練,也曾作為技術負責人帶領過近百人的團隊。公眾號“奔跑中的蝸牛”的作者
1 第2章 微服務架構
通過第1章可知,微服務架構是Cloud Native的重要組成部分,微服務架構給我們帶來收益的同時,也會帶來副作用,我們應該在什麼階段採用微服務架構?如何拆分微服務架構?拆分粒度多大比較合適?本章內容從問題開始,循序漸進,帶領讀者逐步深入微服務架構的各個角落。
1.1 微服務架構的起源
2005年,Peter Rodgers博士在雲端運算博覽會上提出的微Web服務(Micro-Web-Service),將程式設計成細粒度的服務(Granular Services),以作為Microsoft下一階段的軟體架構,其核心思想是讓服務由類似Unix管道的存取方式使用,而且複雜的服務背後是使用簡單URI來開放介面,任何服務都能被開放(exposed)。這個設計在HP的實驗室被實現,具有改變複雜軟體系統的強大力量。
2014年,Martin Fowler與James Lewis共同提出了微服務的概念,定義了微服務架構是以開發一組小型服務的方式來開發一個獨立的應用系統,每個服務都以一個獨立的程序的方式執行,每個服務與其他服務使用輕量級(通常是HTTP API)通訊機制,這些服務是圍繞業務功能構建的,可以通過全自動部署機制獨立部署,同時服務會使用最小規模的集中管理(例如Docker)能力,服務可以用不同的程式語言和資料庫。
實際上,微服務的誕生絕非偶然,敏捷開發幫助我們減少浪費,快速反饋,以使用者體驗為目標;持續交付促使我們更快、更可靠、更頻繁地改進軟體;基礎設施即程式碼(Infrastructure As Code)幫助我們簡化環境的管理,這些都是推動微服務誕生的重要因素。如果沒有這些基礎,微服務架構在展現魅力的同時,可能由於各種問題導致最終失敗。
從SOA架構到服務化架構、再到微服務架構,是一個逐步演進的過程,Amazon被認為是微服務的鼻祖,2015年我曾經接觸過一個Amazon的工程師,他並不是特別瞭解微服務這個名詞,直到看完Martin Fowler關於微服務的文章,才發現自己一直在做的就是微服務架構。可以說微服務架構並不是什麼技術創新,而是開發過程發展到一個階段對技術架構的要求,是在實踐中不斷摸索而來,每個公司所信奉的架構思想有相同之處,但是也不盡相同。這種化繁為簡的拆分方式,不只在技術上帶來突破,更帶來了很多潛在的價值,如關注點分離、溝通效率提升、快速演進、快速交付、快速反饋等。
1.2 為什麼採用微服務架構
1.2.1 單體架構VS微服務架構
就像很難用一個絕對的方式去判斷架構好壞一樣,在大多數場景下,我們也很難從一個外部的視角去判斷服務拆分粒度的合理性,需要對上下文非常瞭解才能做出一個好決策。例如,團隊規模多大,程式碼規模多大,有沒有平臺化,有沒有工具鏈,是否需要持續交付,團隊文化如何等。因此,一個外部的架構師是很難在短時間內將架構規劃合理的,這需要一個過程,當真正瞭解這一切之後,不斷權衡,最終確定。在劃分之前,有必要參考表2-1,綜合各方面的情況,最終做出決策。
表2-1 單體架構VS微服務架構
因素 | 單體架構 | 微服務架構 | 說明 |
交付速度 | 較慢 | 較快 | 服務拆分後,各個服務可以獨立並行開發、測試、部署,交付效率提升,產品的更新速度會更快,使用者體驗更好。程式碼規模越大,微服務的優勢越明顯 |
故障隔離範圍 | 執行緒級 | 程序級 | 服務獨立執行,通過程序的方式隔離,使故障範圍得到有效控制、架構變得更簡單可靠。根據業務的重要程度劃分服務,把核心的業務劃分為獨立的服務,這樣可以從資料庫到服務,保持有效的故障隔離,進而保持穩定 |
整體可用性 | 較低 | 更高 | 微服務架構由於故障範圍得到有效隔離,整體可用性更高,降低一點故障對整體的影響 |
架構持續演進 | 困難 | 簡單 | 由於微服務的粒度更小,架構演進的影響面就更小。不存在大規模重構導致的各種問題。微服務架構對架構演進更友好 |
溝通效率 | 低 | 高 | 業界普遍認為團隊規模越大,溝通效率越低,微服務架構按業務構建全功能團隊,把權利下放,不會出現決策瓶頸點,降低溝通規模,提升溝通效率 |
技術棧選擇 | 受限 | 靈活 | 如果某個業務需要獨立的技術棧,可以通過服務劃分,介面整合的方式實現。例如搜尋,技術棧、專業細分領域都不相同,通常採用獨立的服務實現 |
可擴充套件性 | 受限 | 靈活 | 微服務架構可以根據服務對資源的要求以服務為粒度擴充套件,符合AKF擴充套件立方體中的Y軸擴充套件,而單體架構只能整體擴充套件,只能做到AKF擴充套件立方體中的X軸擴充套件 |
可重用性 | 低 | 高 | 微服務架構可以實現以服務為粒度通過介面共享重用 |
實現業務複雜性分解難度 | 困難 | 容易 | 微服務架構通過將業務分解為更多的服務,業務邊界更清晰,更容易把一個複雜的問題分解為簡單的小問題 |
產品創新複雜度 | 困難 | 容易 | 微服務架構以服務為粒度獨立演進,團隊有更多的自主決策權,更多的試錯機會,更利於創新 |
一致性實現成本 | 低 | 高 | 服務劃分後,如果服務A同時呼叫服務B和服務C,如何保證同時成功或失敗?單體架構下的單庫事務變成了分散式事務問題 |
時延 | 低 | 高 | 服務劃分後,呼叫次數增加,響應時間必然升高,吞吐量降低,如何彌補 |
資源成本 | 低 | 高 | 吞吐量的下降意味著要增加更多的資源,對於交付型專案,特別是小規模部署的場景下,是比較致命的 |
關聯查詢複雜度 | 簡單 | 複雜 | 微服務架構的一個非常明顯的特徵就是一個服務所擁有的資料只能通過這個服務的API來訪問。通過這種方式來解耦,這樣就會帶來查詢問題。以前通過join就可以滿足要求,現在如果需要跨多個服務整合查詢就會非常麻煩 |
遠端呼叫 | 不涉及 | 涉及 | 微服務存在更多的遠端呼叫,需要額外考慮序列化、通訊協議、資料壓縮、服務間的負載均衡、容錯等問題 |
服務治理 | 不涉及 | 涉及 | 由於服務數量變多,微服務架構需要額外考慮服務的註冊發現、依賴關係、治理等問題 |
對開發人員的要求 | 低 | 高 | 微服務架構更復雜,開發人員端到端負責,既要考慮介面定義,又要考慮資料庫設計,對開發人員的水平要求更高 |
對工具的依賴 | 較低 | 較高 | 微服務架構中服務的數量較多,使用工具的效果更明顯,依賴程度更高 |
運維複雜度 | 低 | 高 | 微服務架構中服務的數量較多,對服務的監控、健康檢查要求更高,整體運維複雜度更高 |
1.2.2 什麼時候開始微服務架構
產品初期,應該以單體架構優先。面對一個新的領域,對業務的理解很難在開始階段就比較清晰,往往是經過一段時間之後,才能逐步穩定。很多時候,從一個已有的單體架構中逐步劃分服務,要比一開始就構建微服務簡單得多。另外,在資源受限的情況下,採用微服務架構風險較大,很多優勢無法體現,效能上的劣勢反而會比較明顯。
單體、元件化、微服務架構成本趨勢,如圖2-1所示。當業務複雜度達到一定程度後,微服務架構消耗的成本才會體現優勢,並不是所有的場景都適合採用微服務架構,服務的劃分應逐步進行,持續演進。產品初期,業務複雜度不高的時候,應該儘量採用單體架構。
圖2-1單體、元件化、微服務架構成本趨勢
下面內容摘自Martin Fowler的部落格。
As I hear stories about teams using a microservices architecture, I've noticed a common pattern.
1. Almost all the successful microservice stories have started with a monolith that got too big and was broken up.
2. Almost all the cases where I've heard of a system that was built as a microservice system from scratch, it has ended up in serious trouble.
簡單翻譯如下。
當我聽到關於使用微服務架構的故事的時候,我注意到了一種通用的模式。
1. 幾乎所有成功的微服務架構都是從一個巨大的單體架構開始的,並且由於太大而被拆分為微服務架構。
2. 幾乎所有我聽說過從一開始就構建為微服務架構的故事,最終都遇到了巨大的麻煩。
在服務劃分之前,應該保證基礎設施及公共基礎服務已經準備完畢,可以通過監控快速定位故障,通過工具自動化部署、管理服務,通過服務化框架降低服務開發的複雜度,通過灰度釋出提升可用性,通過資源排程服務快速申請、釋放資源,通過彈性伸縮快速擴充套件應用。
1.2.3 如何決定微服務架構的拆分粒度
微服務架構中的微字,並不代表足夠小,應該解釋為合適。但是合適過於含糊,每個人理解的合適都不盡相同。實際上,有時候對於一個對業務理解不夠深入,對團隊情況又不是很瞭解的人,根本無權協助確定服務的粒度。況且,就算本團隊的架構師,也很難確定粒度。隨著業務發展,開發人員水平的提升,粒度可能會發生變化。這是一個磨合的過程,一個不斷演進的過程,沒有絕對的對與錯。
如果實在找不到合適的依據,可以參考表2-2,決策佔比是從通用的角度考慮,並不適用所有的情況,某些公司認為團隊規模是決定性的,也有些公司認為架構演進是決定性的,還有些公司認為交付速度是決定性的,找到那個你認為的決定性因素,去做合理的拆分即可。
表2-2 微服務拆分粒度決策參考表
序號 | 因素 | 決策佔比 | 說明 |
1 | 團隊規模 | 50% | 當團隊規模變大,會出現決策瓶頸點,即所有的決策都要依賴於某個會議或某個人,沒有人願意承擔責任,效率十分低下 |
2 | 交付速度要求 | 30% | 毫無疑問,拆分粒度越小,交付時受到的約束越小,速度就越快 |
3 | 其他 | 20% | 例如,對佔用資源的要求、對效能的要求、對一致性的要求、對架構演進速度的要求、對創新速度的要求 |
1.3 微服務設計原則
在微服務架構的設計過程中,我們應該遵循哪些原則?以下原則在微服務架構中經常被提起,遵循這些原則能夠讓我們少走彎路。
垂直劃分優先原則
應該根據業務領域對服務進行垂直劃分,因為水平劃分服務可能會導致如下問題。
• 呼叫次數更多導致效能大幅下降。
• 實現一個功能要跨越更多服務,溝通成本升高。
垂直劃分服務可以以最簡單的方式緩解上述問題,並且可以讓團隊從上至下關注業務實現,端到端負責,持續改進。圖2-2簡單描述了一個按業務領域垂直劃分的微服務架構示例,在業務垂直方向切分服務,通過API Gateway聚合內容。
圖2-2 垂直劃分服務
持續演進原則
服務數量快速增長帶來架構複雜度急劇升高,開發、測試、運維等環節很難快速適應,會導致故障率大幅增加,可用性降低,非必要情況,應逐步劃分,持續演進,避免服務數量的爆炸性增長,這等同於灰度釋出的效果,先拿出幾個不太重要的功能拆分出一個服務做試驗,如果出現故障,則可以減少故障的影響範圍。另外,除了業務服務數量的增加,還需要準備持續交付的工具、微服務框架等,加強監控。
服務自治、介面隔離原則
儘量消除對其他服務的強依賴,這樣可以降低溝通成本,提升服務穩定性。服務通過標準的介面隔離,隱藏內部實現細節。這使得服務可以獨立開發、測試、部署、執行,以服務為單位持續交付。
直接訪問對方的資料庫會造成一定的耦合性,應該儘量避免。
自動化驅動原則
部署與運維的成本會隨著服務的增多呈指數級增長,每個服務都需要部署、監控、日誌分析等運維工作,成本會顯著提升。在服務劃分之前,應該首先構建自動化的工具及環境。開發人員應該以自動化為驅動力,簡化服務在建立、開發、測試、部署、運維上的重複性工作,通過工具實現更可靠的操作。避免微服務數量增多帶來的開發、管理複雜度問題。自動化可以從多個方面節省時間、提升效率,它可以快速跟蹤整個交付過程並實時向所有參與者報告這個過程,賦予參與者責任感和成就感,如研發過程中,推行持續整合的文化就特別重要,而持續整合所依賴的工具就是一種自動化的體現。
很多網際網路公司都遵循“一切皆自動化”的原則,特別是存在跨地域的研發模式時,使用自動化工具將是至關重要的,如開源的協作模式。
1.4 微服務架構實施的先決條件
不提倡從一開始就建立微服務架構的原因之一是沒有做好準備,下面我們來看一下建立微服務架構前,需要從哪些方面做準備。
1.4.1 研發環境和流程上的轉變
在實施微服務架構之前,我們要準備相關的環境和流程,可以簡單地通過以下幾方面建立基本的條件。
自動化工具鏈
微服務架構的一大優勢是快速交付,快速交付不只是體現在服務的粒度更小,可以獨立交付,還體現在整個流程更快速,微服務架構基於自動化的工具鏈,以流水線交付的方式串聯整個DevOps流程,小團隊可以基於服務獨立開發、測試、部署、運維。傳統的交付週期以月為單位,而微服務架構的交付週期能做到以天為單位,如果按照傳統的開發模式是無法滿足要求的。
微服務框架
微服務框架可以封裝、抽象分散式場景下的一些常用能力,例如負載均衡、服務註冊發現、容錯、遠端通訊等能力,可以讓開發人員快速開發出高質量的服務,在採用微服務架構之前,應該先進行微服務框架的選型,試用。
快速申請資源
如果以天為單位進行交付,就必須能夠快速申請資源。基礎設施即程式碼可通過程式設計的方式管理虛擬機器或容器,免去了手動配置、更新各個硬體的需要,這就使得基礎設施極具彈性,能夠快速、高效、準確的進行重複性操作,開發人員使用同一套配置或程式碼,就可部署並管理成千上萬臺物理機。基礎設施即程式碼能夠得到更快的速度、更低的成本和更可靠的環境。用程式碼定義伺服器配置意味著在眾多伺服器之間有絕對的一致性,容易形成標準化。手動調整配置往往會有一些微妙的差異,難以追溯和除錯,並且會導致許多詭異的問題。
故障發現反饋機制
當服務數量增多,頻繁交付的時候,故障次數可能會大幅上升,我們需要通過全面的監控發現故障,及時處理併發出報警。當生產環境出現問題的時候,需要將故障進行分級,評估影響面,並分配給相應的架構師或者開發人員,開發人員需要不斷更新故障的狀態,便於管理者、客服、銷售人員等問題相關人瞭解進度,以提供更好的使用者體驗。
研發流程上的轉變
需要重新組建團隊,以服務為核心,按照業務領域劃分全功能團隊,改變原有的研發流程、決策機制。例如,倡導敏捷文化,快速迭代,做更多的自動化測試,加強Code Review,給團隊更多的自主決策權等,可以參考本書第九章和第十章。
1.4.2 拆分前先做好解耦
解耦這個詞彙來源於數學,是指使含有多個變數的數學方程變成能夠用單個變量表示的方程組,即變數不再同時共同直接影響一個方程的結果,從而簡化分析計算。
在軟體世界裡,解耦強調的是每個單元可以獨立變化,儘量減少外界的影響。說白了也就是,如果把Memcache換成Redis,那麼需要多少工作量,涉及的修改面有多大。但是,解耦也會帶來工作量的增加,架構或者程式碼變得複雜等問題。例如很多人會假設把Oracle換成MySQL,Memcache換成Redis,但是在實際的情況中,並不是所有的業務發展速度都有這麼快,如果能預料到短期將發生變化,為什麼不直接使用MySQL呢?通常這是一個偽命題。如果在未來幾年後才發生變化,那麼現在去做相應的適配,這不符合敏捷開發的哲學思想,也不是一個高效率的思路。
在轉向微服務架構之前,業務服務存在狀態、資料庫中存在觸發器和儲存過程、服務之間繞過介面呼叫等問題是我們首先要解決的。
狀態外接
無狀態(Statelessness)指的是服務內部變數值的儲存。有狀態的服務伸縮起來非常複雜,可以通過將服務的狀態外接到資料庫、分散式快取中,使服務變成無狀態。通常業界用牲畜來比喻無狀態,用寵物來比喻有狀態,寵物是需要呵護的,是有名字的,不能被輕易替換的,而牲畜是沒有名字的,只生產肉和奶,死掉一個,用新的來替換即可。所以,我們期望服務可以做到無狀態,可以被輕易地替換掉。
但是,無狀態不代表狀態消失了,只是把狀態轉移到分散式快取和資料庫中了,業務服務伸縮的時候,還是要考慮分散式快取和資料庫所能承受的壓力限制。那為什麼還要外接呢?因為一方面即使不外接到資料庫,資料庫也存在狀態,另一方面,這樣可以把複雜度抽象到統一的位置,便於集中處理。例如,服務端的Session資訊可以放到分散式快取中,這一設計方法既可以讓業務服務在一定範圍內(分散式快取的上限)伸縮時不受狀態的限制,又可以把複雜度抽象到特定的位置,讓專業領域開發人員統一做有狀態的伸縮。雖然絕大多數服務都可以狀態外接,但是並不是所有的業務服務都能設計成無狀態,例如客戶端與服務端的長連線,這種狀態很難外接。
以下三種常見的狀態需要和業務服務拆分開來,否則擴充套件性將受到很大限制。
(1)定時任務。
因為大多數任務不能重複觸發,輕則重複做無用功(冪等的情況下),重則會導致不一致。例如從A表中把資料遷移到B表中,如果在兩個服務中同時處理,沒有一個協調器的話,會導致重複拉取。所以,需要把定時任務從業務服務中提取出來,通過分散式任務排程統一協調。
(2)本地儲存。
在本地儲存檔案也是比較常見的,當有多個例項的時候,要麼全部同步一遍,要麼需要根據使用者路由到同一個例項,並且在伸縮的過程中,需要遷移。
(3)本地快取。
某些業務會將資料存放在本地做快取,例如Session資料,如果要去掉本地快取,則可以通過分散式快取和Cookie解決業務服務帶狀態的問題。
當然,本地快取也有適用的業務場景,不能一概而論。
去觸發器、儲存過程
觸發器、儲存過程在系統規模比較小的時候,的確非常簡單實用。但是,隨著業務的發展,業務服務比較容易擴充套件,資料庫通常變成了伸縮的瓶頸,許多方案都是為了平衡資料庫的壓力,觸發器、儲存過程可能會帶來如下問題。
• 整體的伸縮受到資料庫的限制,因為觸發器、儲存過程難以擴充套件。
• 當存在水平分表的時候,可能無法滿足需求。
• 如果觸發器、儲存過程過多,則會導致運維複雜度升高。
解決方案通常是通過外部的業務服務或者定時任務替換觸發器及儲存過程。
通過介面隔離
直接訪問其他服務的資料庫,如圖2-3所示。CRM直接呼叫OA的資料庫,沒有通過介面呼叫,當我們對CRM進行微服務架構拆分之前,需要先理清系統的外部依賴關係,如果存在多個系統共享一個數據庫,就會導致耦合問題,影響可用性和擴充套件性,可能出現如下問題。
• 當CRM中的資料結構發生變化的時候,OA也要跟著變化,導致開發的過程互相依賴。
• 有可能在CRM進行的限流是沒用的,因為OA沒有通過CRM提供的介面進行呼叫。
• 假設隨著業務的發展,需要在CRM的資料庫上做快取,可能存在多個地方要考慮快取的問題。
圖2-3 直接訪問其他服務的資料庫
總之,介面應該作為唯一對外提供的訪問方式,這代表的是控制力。解決方法就是通過介面呼叫,逐步去除資料庫的直接訪問。
1.5 微服務劃分模式
雖然服務是逐步被拆分出來的,隨著業務的演進,在某一時刻,可能需要我們重新審視服務劃分的是否合理。本節向大家推薦兩種服務劃分的方法。首先介紹如何選擇服務劃分的方法。
1.5.1 基於業務複雜度選擇服務劃分方法
根據業務複雜度劃分服務,如圖2-4所示。當業務複雜度足夠高的時候,應該基於領域驅動劃分服務,而領域驅動本身足夠複雜,很多概念比較抽象,應用範圍並不是特別廣泛,所以當業務複雜度較低時,可以選擇基於資料驅動模式劃分服務,資料驅動模式更容易理解和上手,也就是說,除非業務複雜度非常高,否則應該優先以資料驅動模式劃分服務。這裡的業務複雜度專指業務邏輯,而非資料量、併發量等相關複雜度。
圖2-4 根據業務複雜度劃分服務
在做出選擇的時候,還有一個參考指標是,團隊以前是否已經基於領域驅動開發業務,也就是說如果產品已經基於領域驅動開發了一段時間,具備了領域驅動開發的能力,那麼當然推薦繼續選擇領域驅動劃分服務。如果是一個全新的產品,則可以靈活選擇。
選擇服務劃分的方法要重點考慮的條件如下。
• 業務複雜度。
• 團隊對領域驅動的熟悉程度。
1.5.2 基於資料驅動劃分服務
資料驅動是一個自下而上的架構設計方法,資料驅動強調的是資料結構,也就是通過分析需求,確定整體資料結構,根據表之間的關係劃分服務。
通常基於資料驅動劃分服務的步驟如下。
第一步,需求分析。通過領域專家(或者產品經理)確定目標,然後總結user story,確定核心的業務流程。通過工具呈現比較粗糙的介面,內部討論。不斷迭代此環節,直到滿意為止。
第二步,抽象資料結構。根據需求總結use case,協助分析需求,從中抽象資料結構。
第三步,劃分服務。分析資料結構,識別服務,服務應該滿足高內聚、低耦合、單一職責等特徵。
第四步,確定服務呼叫關係。先分析出主要流程,根據請求需要呼叫的服務確定服務呼叫關係。如果存在問題,需要從第一步重新開始。
第五步,業務流程驗證。重新回到user story,以服務為粒度實現時序圖,注意此階段重點是要驗證服務劃分是否合適,要關注如下問題。
• 一次更新操作如果要跨越更多服務,那麼一致性的要求是什麼。
• 跨服務查詢時,是否要做關聯查詢,一個服務內是否能解決問題。
• 效能是否能滿足要求。
• 成本是否滿足要求。
第六步,持續優化。
1.5.3 基於領域驅動劃分服務
領域驅動是一個自上而下的架構設計方法,通過和領域專家建立統一的語言,不斷交流,確定關鍵業務場景,逐步確定邊界上下文。領域驅動更強調業務實現效果,認為自下而上的設計可能會導致技術人員不能更好地理解業務方向,偏離業務目標。
通常基於領域驅動劃分服務的步驟如下。
第一步,通過模型和領域專家建立統一語言。建立統一語言是為了更深入的理解需求,通用語言儘量以業務語言為主,而非技術語言。通用語言和程式碼一樣,需要不斷的重構。
第二步,業務分析。確定核心的業務流程,然後逐步擴充套件到全部。最好通過工具呈現比較粗糙的介面,內部討論。
第三步,尋找聚合。顯式地定義了領域模型的邊界。為大家介紹一下最近比較熱門的事件風暴,事件風暴是一種基於領域驅動分析業務,劃分服務的方法。
事件風暴就是把所有的關鍵參與者都召集到一個很寬敞的屋子裡來開會,並且使用便利貼來描述系統中發生的事情,如圖2-5所示。
• 用桔黃色的便利貼代表領域事件,在上面用一句話描述曾經發生過什麼事情。
• 用藍色的便利貼代表命令。命令的發起