以微服務的方式構建新項目并不困難,新架構帶來的新承諾也著實令人充滿期待。然而,現實與想象往往相去甚遠。本文是該作者 Arnold Galovics 關于微服務系列文章中的第二篇。感興趣的朋友可以點擊此處閱讀第一篇《新項目別一上來就用微服務》,在第一篇文章中,Arnold 介紹了微服務架構對于基礎設施的要求、更快的部署特性、給組織文化提出的挑戰以及天然的故障隔離優勢。
Arnold 提示道:“本系列文章中提到的所有觀點都是個人心得,畢竟不同環境、組織和項目都會給開發工作帶來變數。沒準我踩過的坑反而是你開發流程中最順暢的部分,我的考慮方式也未必符合各位的實際情況。總之,內容僅供參考,請大家輕拍。”
本系列目的在于提醒大家,只有對新項目建立起深入的評估與理解,才能真正找到最適合的架構選項。當然,也期待大家在留言中分享自己的真知灼見。以下是正文:
易于理解
很多朋友都覺得微服務架構的理解難度更低……但事實真是這樣嗎?
當然了,我們只需要具體管控各個肩負明確職責的微服務項目,所以每種元素在干嘛、需要干嘛、系統整體狀況如何不就更清晰了嗎?畢竟我們面對的是一個個服務,而非彼此交織的架構整體。
但我要給大家潑點冷水:這完全就是騙人的。沒錯,確實有一些微服務架構做出了優秀的邊界定義,任何半路介入的參與者都能快速理解某項微服務的實際作用。但也有很多項目做得不好,導致某項服務要么做得太多、要么做得太少;甚至部分單一功能也被過度拆分成了多項服務,因此系統的混亂度大幅提升,任何單一服務的故障都可能將整體應用拖入崩潰的深淵。
大家可能會說,“對,這種情況是有,但那是你的問題、不是微服務的問題”,或者“你笨啊,笨還能怪架構?”有道理,但問題是項目絕對不可能百分之百受控,真的不可能。團隊、組織、項目,各個層面都有出錯的可能,所以邊界定義不清的幾率會遠遠高于邊界定義良好的幾率。
但我也承認,這種情況在單體式架構中也可能帶來麻煩。我也見過那些搞不清在干什么的單體式應用,所有功能就像飄香拌面一樣混雜成大坨,代碼庫碩大無朋、缺少必要的測試、說明文檔含糊不清、不同功能的編程風格格格不入等等。
不過單體式架構修改起來還是更輕松一點,它相較于微服務的優越性也正在于此——模塊化。我們同樣像為微服務定義邊界那樣進行代碼構建,只是不再將這些“服務”或者說模塊視為其他應用的元素,而是同一整體中的各個組成部分。
所以對我來說,這種雜亂的毛病主要還是出在微服務架構身上。只是我也承認,如果要想搞砸,那在單體式架構中也一樣可以搞砸。
可擴展性
提到微服務,網上總在強調“你可以橫向擴展各項服務——也就是為同一服務啟動多個實例,從而輕松應用負載增長。”
說得倒是輕巧,但具體實現起來有那么簡單嗎?我們用以下架構為例(沿用系列第一篇文章的用例):
假定你的應用中有大量活躍用戶,所以系統需要處理眾多用戶會話。那很明顯,我們就得啟動多個會話服務。
如果會話服務可以通過 HTTP 協議訪問,那用多實例實現就會比較困難。如果是通過 API 網關或者登錄服務來接入會話服務,那就得調用會話的 HTTP API。但使用 HTTP API 時,我們得調用表達特定會話服務實例位置的特定 URL(主機+端口)。所以如果同時啟動多個會話服務,消費方就得調用多個主機+端口組合。
這個問題倒不至于無解,常見的辦法就是使用服務注冊表或者負載均衡器。如果是使用服務注冊表,那么當實例啟動時,它們會將自身注冊到某種類型的存儲當中,再通過存儲檢索實例的相應位置。這時候當消費方服務打算跟特定服務對話時,它就會先到服務注冊表去檢索實例位置,之后再使用該位置實現與特定實例的對話。
另一種解決方案則是負載均衡器。這時候我們無需直接與服務對話,而是使用中間層、即另一項服務(負載均衡器)來維持各實例的可訪問性并代理通信負載。例如,大家可以將其理解成類似于 DNS 負載均衡的功能,或者是像 AWS ALB 那種負載均衡器服務。
所以很明顯,當我們使用 HTTP 進行服務間通信時,就必須通過位置解析才能正確處理服務實例。這可是有成本的哦,畢竟天底下沒有免費的午餐。
如果會話服務可以通過消息進行訪問,那在本文的示例中我們就要用到 Kafka,而這又會讓問題躍上新的層次。具體為何,容我細細道來。
理由很簡單,因為在使用 Kafka 時,我們可以做到有多少主題分區、就啟動多少服務實例。假定我們有一個 session-service-topic 主題,這個主題明顯需要由會話服務進行讀取。Kafka 主題的擴展取決于主題分區的數量,每個分區對應一個消費方。如果我們希望將會話服務擴展至 4 個實例,那就需要建立 4 個分區來存儲各實例所需讀取的主題。
這有什么問題嗎?我們可以隨時增加分區數量呀。沒錯,但請大家注意,消息的順序只能存留在主題分區之內,這可是有很大影響的。
我們假設會話服務需要處理兩種消息類型:
- UserLoggedInEvent
- UserActivityEvent
在收到 UserLoggedInEvent 之后,會話服務將創建一個內部“會話”,這可能涉及相關的數據庫表之類。而在收到 UserActivityEvent 之后,會話服務則須更新現有用戶會話的過期時間,這可能涉及相關的數據庫條目。
問題在于,假定我們有 2 個會話服務實例,而每個主題又對應 2 個用于消息發送的分區。主題生產方選擇使用循環分區策略,意味著一條消息進入第一分區、接下來第二條消息進入第二分區。這時候,就會有一項服務接收到 UserLoggedInEvent,而另一項服務則接收到 UserActivityEvent。
在這種情況下,接收 UserActivityEvent 的服務在處理速度上可能快于接收 UserLoggedInEvent 的服務;如果雙方共享同一數據庫,就有可能引發問題。因為初始會話記錄還沒有被相應的服務實例正確寫入至數據庫。
聽起來問題不大,但實際應用起來可是相當復雜。這種情況當然也有解決方案,但它本來可以不必存在,只是因為我們選擇了微服務架構、就必須多承擔調試壓力,有點不劃算。
我也見過很多比較復雜的系統,由于很難明確該如何進行數據分區、如何解決排序問題,所以幾乎無法使用 Kafka 實現橫向擴展。另外,也有一些系統設計者為了避免這類問題,而選擇使用單一 Kafka 主題實現所有服務間通信。這種方法雖然回避了分區的困擾,但也徹底犧牲掉了擴展的可能性。而且在大部分場景下,HTTP 其實就完全夠用了,著實沒必要搞那么麻煩。
我想強調的一點是:千萬別以為微服務的橫向擴展能力是默認的、“免費的”。如果不開動腦筋,這種擴展能力根本就實現不了。不知道大家有沒有嘗試過在微服務環境下調試排序問題,卻發現問題只發生在常規開發/測試環境中,卻沒法在本地計算機上重現。這真的很讓人頭大,不提了。
技術自由
終于進入了我最喜愛的環節。這是種幸運、也是種不幸,我本人對技術自由這事有著深切的體會。有時候問題的根源并不是無奈的意外,而是……開發者們實在太有創意了。
所以大家會天然更喜歡微服務架構。畢竟在單體式架構中,我們總要被編程語言和技術棧的條條框框所束縛,但在微服務里卻可以使用不同的編程語言和技術棧編寫不同服務。比方說,我們可以在某一服務中使用 JAVA,在另一服務中使用 Node.js,在第三項服務中使用 Go 等等,完全沒有問題。
但這種優勢,有時候反而成為最大的弊端。我當然不反對創新,但我理解的創新是用前所未有的方法解決問題、而不是用前所未有的方法創造問題。
大家可能覺得異構微服務架構沒什么問題,但前提是你得有明確的職能劃分,保證由專人專門管理特定微服務項目。只有這樣,我們才真正能說“沒什么問題”。
但如果我們還沒有做好使用微服務架構的萬全準備,甚至才剛剛踏出探索的第一步,那人員與服務間的對應關系恐怕沒有那么清晰。換句話說,大家在開發新功能時,往往不免要觸及到由其他人編寫的服務。
假定我們是一支負責開發登錄服務的團隊,服務本體由 Java 編寫。另一支團隊則開發會話服務,至少在項目啟動之初是如此。每個人都對產品萬分期待、充滿動力,并努力用新鮮元素滿滿的創新方案解決現實問題。于是乎,會話服務是用 Node.js 編寫的,因為最近剛剛面世了一套全新 JS 框架,大家都贊不絕口、說它能把生產力提高好幾倍之類的。
接下來,MVP 已經初步成型,產品開始在生產環境下運行。向個月后,會話服務(Node.js 服務)的構建者開始過渡到其他項目,甚至離職去了其他公司。接下來這段時間就成了空窗期,大家不再為會話服務開發任何相關功能。
突然之間,產品負責人跳出來說“大家好,我們需要上線新功能,用來擴展平臺上的會話服務。”但這時候原本的會話服務創建者已經離職,另一支接手團隊卻沒有任何會話服務或者 Node.js 開發經驗。
于是大家傻眼了,根本不知道自己能不能接下這樣一份重擔。
下面,咱們再來聊聊 DevOps。我倒不是想刻意針對 DevOps,但如今各類組織都在積極組建獨立于工程團隊的專職 DevOps 部門,這真有必要嗎?DevOps 并不是什么萬金油,DevOps 工程師也不可能了解每種語言和技術細節。所以就算頂著這個光芒四射的頭銜,他們也很可能搞不定服務運營工作。
另外,很多產品負責人或者公司老板也不夠負責,他們壓根不重視由不同編程語言帶來的種種隱患。比如說繼續沿用 Java 開發某項服務只需要 2 周時間,但為了跟其他服務保持統一,我們最好花 6 周時間用另一種語言來編寫,他們會同意嗎?估計夠嗆,畢竟短期來看時間就是生命。但如果真這么隨性,一旦人員離職、產生代碼交接需求,后面的麻煩完全可以預見。
不止于此,異構系統還會帶來其他跨越性的新難題。比如如何實現標準化……
- 日志記錄
- 安全保護
- 狀態監控
- 國際化
- 錯誤處理
我們得在不同的語言和技術棧上一遍遍重復這些標準化調整,這工作可不輕松,而且要求各個團隊付出大量精力。我記得我們就曾在一個已經開發 5 個月的項目中使用到 80 多項微服務,當時大家打算標準化 API 錯誤處理,保證生成一致的 HTTP 代碼。5 個小隊最初的預估周期就長達 107 天,而且這還不屬于異構系統——所有代碼都是用 Java + Spring Boot 編寫的。可以想象一下,如果代碼涉及三、四種不同語言和技術棧,工作量會膨脹到什么地步。
我當然不反對使用多種語言/技術棧,但這事最好要有明確的理由,比如切實需要某些語言/技術棧中的功能特性。我也會在低延遲負載中使用 Go 或者 Node.js,并傾向于使用 Java 開發邏輯更復雜、但對性能要求不高的任務——但一定要有理有據。
這里再分享一點在異構架構方面的經驗。我接觸過的一套架構涉及五種語言,分別是 Java、Scala、Node.js、Erlang 等。團隊當然就得隨時維持這五種代碼,可以想象會有多困難。更要命的是,里面還涉及不同的 Java 版本、不同的開發框架等。事實證明,很多語言的引入根本沒有必要,開發者這么干只是因為他們好奇、想要實驗一下。
我當然相信產品的成功源自人的成功,而人的成功源自對創新的探索。但我也覺得創新這事不能泛濫,我們在制定決策時必須小心謹慎、保證充分理解“創新”背后的含義。從個人角度來看,“我就是想試試”不能叫理由,這種隨心所欲的風格只會給項目留下無數暗傷、拖累后續發展。
團隊擴張
團隊的擴張可以說是使用微服務架構的最佳論據。設想一下,一支 10 人小隊在一個單體式代碼庫上工作,效果很好、復雜性始終不高。
但如果擴大團隊規模,讓 100 個人同時處理一個單體式代碼庫,結果會如何?代碼庫相同、工作方式相同,一切都不做變動。大家會把代碼推送至同一個 git repo,使用相同的類、相同的測試、處理 10 項不同功能。
這很快就會引發沖突,大家發現單體式架構太過“擁擠”,容不下大規模作戰。微服務的要義就是把單一團隊的工作從整體代碼庫中抽離出來,形成新的獨立代碼庫。這樣人們才能并行工作,保證不對其他開發者的行動產生干擾。
但這個適合微服務架構的規模臨界點在哪里?我不太清楚,具體要視組織情況而定。如果管理者既不負責、也沒水平,那 10 個人就足夠把項目攪成一鍋粥了。但在這樣的團隊里,難道微服務就能發揮作用?我壓根不信。
總結
從負責任的角度出發,我不會輕易斷言大家該用單體式架構、還是微服務架構。
我的個人看法是,這個艱難的選擇無法回避、必須在項目起步階段就預先設定完成。我覺得大概四分之三的新項目都可以無腦選擇單體式架構,再配合適當的模塊化設計保證后續有必要時能比較輕松地轉化成微服務架構。那什么叫“有必要”呢?就是轉化的工作量低于繼續維護原有單體式架構的工作量時。
剩下的四分之一可能天然更適合微服務架構,但還是要先整理出明確的理由。總之,如果不假思索地盲選,我個人肯定是先單體、后微服務。
架構判斷絕非易事,我們需要對產品做出未來一到兩年的發展預期、估算未來會有多少人/什么樣的人參與到項目中來,會有哪些基礎設施限制,我們的預算、產品功能路線等等。綜合各項因素,最后得出的才是安全可靠的架構決斷。
如果你的產品只是一款普通的終端消費級 Web 應用程序,例如網上商店,日活用戶 5000 左右,前五年月均訂單量 100 份上下,那就完全沒必要選擇微服務。另外,如果你的初始團隊是一位老手帶多位新手,那微服務同樣不太適用。最后,如果項目預算有限,同樣記得遠離微服務——它帶來的很可能是一套沒人愿意維護的混亂系統。
微服務這個概念屬于聽起來簡單,做起來卻極為困難。相信我,沒人天生就能編寫出完美的微服務項目,我們都需要不斷摸索和學習、圍繞新概念打磨自己的業務水平。雖然開發頂尖單體式應用程序的難度也不低,但它的結構特性更符合我們的思維本能。而單體式架構中最難學習的正是模塊化、可測試性、關注點分離等要素,也就是那些跟微服務架構最相似的部分。本篇文章到此為止,總之,兩種架構各有各的挑戰。
你給解釋解釋,什么叫微服務?






