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