微服務(wù)依賴管理的陷阱與模式(微服務(wù)架構(gòu)深度解析與最佳實踐)
去年,在 QCon Plus 期間,我分享了我在谷歌工作的 10 多年里遇到的一些微服務(wù)依賴管理中的陷阱和模式。這次演講不是為了介紹任何特定產(chǎn)品或團(tuán)隊,而是為了分享我自己作為谷歌軟件工程師的經(jīng)驗和個人學(xué)習(xí)成果。
我是基于上述前提登臺演講的。這些場景都有一些關(guān)鍵的頓悟時刻,通過這些瞬間我意識到了微服務(wù)環(huán)境中的許多方面是何等重要。也有不少遭遇失敗或出錯的時刻。我精心挑選出了這些故障場景,并告訴大家我做了哪些事情來避免將來發(fā)生類似的情況,這樣你就可以在你自己的環(huán)境中找出可能導(dǎo)致類似故障的跡象。
所有這些場景中我都在與其他工程角色協(xié)作。有時我是一名軟件工程師,與某位項目經(jīng)理共事。還有些時候,我是一名站點(diǎn)可靠性工程師,與其他開發(fā)人員協(xié)作。在最后一個場景中,我基本上在和團(tuán)隊中的所有人配合,目標(biāo)是構(gòu)建可靠的服務(wù)。
這些場景都對團(tuán)隊中的各種角色大有助益:能否成功構(gòu)建可靠的微服務(wù)環(huán)境,不僅取決于某個人或某個單一角色。
每次更改系統(tǒng)時,更改都會影響整個產(chǎn)品的許多部分和組件——這些組件可能是由你的公司、在云中或由第三方提供商運(yùn)營的。系統(tǒng)更改會產(chǎn)生連鎖反應(yīng),一直影響到客戶一側(cè):客戶恰恰是你每時每刻都要考慮的群體。出于這個原因,你需要從整體的角度來看待系統(tǒng)——這也是我在演講中試圖傳達(dá)的一部分內(nèi)容。
在介紹這些場景(以及所有可以從它們中學(xué)到的東西)之前,讓我們快速了解一下行業(yè)是如何從單體服務(wù)過渡到微服務(wù),然后再邁出最后一步將服務(wù)上云的。我們還將研究各種模式和流量增長、故障隔離,以及如何在每個后端都有不同提示的世界中規(guī)劃合理的服務(wù)級別目標(biāo)(SLO)。
單體、微服務(wù)和云端
我們的旅程(這里我們會使用一個通用服務(wù))從一個二進(jìn)制文件開始,它最終(且迅速)會演變?yōu)榘ū姸鄰?fù)雜功能的文件,例如數(shù)據(jù)庫、用戶身份驗證、流量控制、運(yùn)維監(jiān)控和 HTTP API(這樣我們的客戶就可以在線找到我們)。起初,這個單一的二進(jìn)制文件只打算運(yùn)行在一臺機(jī)器上,但隨著業(yè)務(wù)的增長,我們有必要在多個地理位置復(fù)制這個二進(jìn)制文件,同時為流量增長留出額外的空間。
在復(fù)制我們的單體之后不久,有幾個原因要求我們將它們解耦為一些單獨(dú)的二進(jìn)制文件?;蛟S你可以想到其中一些因素。一個常見的原因與二進(jìn)制文件的復(fù)雜性有關(guān)。隨著我們向其添加愈加復(fù)雜的功能,代碼庫變得幾乎無法維護(hù)(更不用說添加其他新功能了)。將單體分解成許多單獨(dú)的二進(jìn)制文件的另一個常見原因事關(guān)獨(dú)立邏輯組件的獨(dú)特需求。例如,我們可能有必要為特定組件增加硬件資源,而不影響其他組件的性能。
像這樣的場景最終促成了微服務(wù)的誕生:微服務(wù)是一組松散耦合的服務(wù),這些服務(wù)可獨(dú)立部署、高度可維護(hù)并組織起來形成(或服務(wù))一個復(fù)雜的應(yīng)用程序。在實踐中,這意味著我們會部署多個二進(jìn)制文件并通過網(wǎng)絡(luò)讓它們通信,其中每個二進(jìn)制文件都實現(xiàn)了自己的微服務(wù),但它們都服務(wù)于并代表單一的一個產(chǎn)品。圖 1 展示了一個 API 產(chǎn)品的示例,該產(chǎn)品解耦為五個獨(dú)立的微服務(wù)(API、Auth、Control、Data 和 Ops),這些微服務(wù)通過網(wǎng)絡(luò)相互通信。在微服務(wù)架構(gòu)中,網(wǎng)絡(luò)也是產(chǎn)品的重要組成部分,因此你必須始終牢記這一點(diǎn)。每個服務(wù)——現(xiàn)在既是單個二進(jìn)制文件又是應(yīng)用程序的一個組件——可以獨(dú)立增加硬件資源,并且工程團(tuán)隊可以輕松控制其生命周期。
圖 1:一個 API 產(chǎn)品解耦為五個獨(dú)立的微服務(wù)
微服務(wù)的好處
在微服務(wù)架構(gòu)中運(yùn)行產(chǎn)品提供了一系列好處??傮w而言,因為產(chǎn)品所有者可以在不同位置部署松散耦合的二進(jìn)制文件,所以他們能夠在具有成本效益和高可用性的部署方案中做出選擇,在云中或在他們自己的機(jī)器中托管每項服務(wù)。它還允許獨(dú)立的垂直或水平擴(kuò)展:增加每個組件的硬件資源,或復(fù)制允許使用不同獨(dú)立區(qū)域的組件。
另一個好處與開發(fā)生命周期有關(guān)。由于每個服務(wù)在邏輯上都與其他服務(wù)分離,并且內(nèi)部復(fù)雜性較低,因此開發(fā)人員更容易推理其實現(xiàn)中的更改,并保證新功能具有可預(yù)測的結(jié)果。這也意味著每個組件都能做到獨(dú)立開發(fā),允許開發(fā)人員在不干擾其他服務(wù)的情況下對一個或多個服務(wù)進(jìn)行本地更改。發(fā)布可以獨(dú)立推進(jìn)或回滾,從而帶來對中斷的更快反應(yīng)速度和更專注于核心的生產(chǎn)更改。
微服務(wù)面臨的挑戰(zhàn)
盡管有那么多優(yōu)點(diǎn),但基于微服務(wù)的架構(gòu)也可能會讓某些流程處理起來更加困難。在接下來的部分中,我將展示我之前提到的一些場景(雖然我改動了一些其中涉及的真實姓名)。我將詳細(xì)介紹每個場景,包括一些與管理微服務(wù)相關(guān)的令人難忘的痛點(diǎn),例如調(diào)整前端和后端之間的流量和資源增長需求。我還將討論如何設(shè)計故障域,以及如何基于所有微服務(wù)的組合 SLO 計算產(chǎn)品 SLO。最后,我將分享一些有用的技巧,希望這些技巧可以幫助節(jié)省你的時間并預(yù)防最終的客戶中斷。
場景一:PetPic
我們的第一個場景圍繞一個名為 PetPic 的虛構(gòu)產(chǎn)品展開。如圖 2 所示,PetPic 是一項全局服務(wù),可為兩處地理區(qū)域(Happytails 和 Furland)的狗狗愛好者提供狗的照片。該服務(wù)目前在每個地區(qū)都有 100 個客戶,總共有 200 個客戶。前端 API 運(yùn)行在獨(dú)立的機(jī)器上,每臺機(jī)器都位于其中一個區(qū)域。作為一個復(fù)雜的服務(wù),PetPic 有多個組件,但在第一次研究中我們將只考慮其中一個組件:數(shù)據(jù)庫后端。該數(shù)據(jù)庫運(yùn)行在云端的一個全局區(qū)域中,并為 Happytails 和 Furland 兩個區(qū)域提供服務(wù)。
圖 2:全局 PetPic 服務(wù)
問題:調(diào)整流量增長
目前該數(shù)據(jù)庫在高峰時使用了其所有資源的 50%??紤]到這一點(diǎn),產(chǎn)品負(fù)責(zé)人決定在 PetPic 中實現(xiàn)一項新特性,讓它也可以向客戶提供貓的照片。新特性實現(xiàn)后,工程師決定首先在 Happytails 區(qū)域推出這項特性。這樣,他們可以在向所有人提供新特性之前找出意外的熱情用戶流量或資源使用變化。考慮到兩個地區(qū)的用戶基數(shù)相同,這在當(dāng)時似乎是一個非常合理的策略。
為準(zhǔn)備上線,工程師將 Happytails 中 API 服務(wù)的處理資源增加了一倍,數(shù)據(jù)庫資源增加了 10%。上線很成功??蛻粼鲩L了 10%,這可能表明一些愛貓人士加入了 PetPic。數(shù)據(jù)庫資源利用率在峰值時為 50%,再次表明額外資源確實是必要的。
所有信號都表明用戶增長 10%需要數(shù)據(jù)庫資源也增長 10%。為了準(zhǔn)備在 Furland 區(qū)域推出新特性,PetPic 工程師向數(shù)據(jù)庫中添加了 10%的額外資源。他們還將 Furland 的 API 資源增加了一倍,以應(yīng)對新客戶的需求。這些更改與在 Happytails 推出新功特性時所做的完全相同。
他們在周三為 Furland 用戶推出了這項新特性。然后,在午餐時間,工程師開始收到大量警報,報告用戶了服務(wù)返回HTTP 500錯誤代碼——這意味著用戶無法使用該服務(wù)了。這與 Happytails 的上線經(jīng)歷完全兩回事。此時,數(shù)據(jù)庫團(tuán)隊聯(lián)系到工程部,提到數(shù)據(jù)庫資源利用率在兩小時前(上線后不久)就達(dá)到了 80%。他們試圖分配更多的 CPU 來處理額外的流量,但這一更改在今天之內(nèi)不太可能實現(xiàn)。同時,API 團(tuán)隊檢查了用戶增長圖,并報告沒有出現(xiàn)與預(yù)期不同的變化:該服務(wù)現(xiàn)在共有 220 個客戶。由于工程團(tuán)隊中沒有人能找出任何明顯的中斷原因,他們決定中止發(fā)布并在 Furland 中回滾該特性。
圖 3:流量增長的意外影響
在 Happytails 中推出的特性帶來的 10%的客戶增長與 10%的數(shù)據(jù)庫流量增長相一致。然而,在分析日志后工程團(tuán)隊發(fā)現(xiàn),在 Furland 推出該特性后,即使沒有一個新用戶注冊,數(shù)據(jù)庫的流量也增長了 60%?;貪L后,急于在午休時間看到貓貓照片的不滿客戶們開了幾張客戶支持票。工程師們總算了解到,F(xiàn)urland 的客戶實際上多為愛貓人士,當(dāng)只有狗狗圖片能看時,他們沒什么興趣與 PetPic 互動。
要點(diǎn)
上面的場景告訴我們,貓圖片特性在吸引 Furland 的現(xiàn)有客戶方面取得了巨大成功,但新特性的部署策略完全沒能預(yù)料到會取得如此大的成功。這里的一個重要教訓(xùn)是,每種產(chǎn)品都會經(jīng)歷不同類型的增長過程。正如我們在這個場景中看到的,客戶數(shù)量的增長與現(xiàn)有客戶參與度的增長是不一樣的——不同類型的增長并不總是相互關(guān)聯(lián)。處理用戶請求所需的硬件資源可能因用戶行為而異,用戶行為也可能因許多因素(包括地理區(qū)域)而異。
在準(zhǔn)備在不同地區(qū)推出產(chǎn)品時,最好在所有地區(qū)進(jìn)行特性實驗,以更全面地了解新特性將如何影響用戶行為(以及資源?利用率)。此外,每當(dāng)新發(fā)布需要額外的硬件資源時,明智的做法是讓后端所有者有更多時間來動手分配這些資源。分配新機(jī)器需要采購訂單、運(yùn)輸過程和硬件的物理安裝過程。發(fā)布策略需要考慮到這部分額外時間。
場景二:故障隔離
從架構(gòu)的角度來看,我們剛剛研究的這個場景涉及了一項在運(yùn)行中成為單點(diǎn)故障的全局服務(wù),以及一次導(dǎo)致兩個區(qū)域中斷的本地部署。在單體應(yīng)用的世界中,跨組件隔離故障是非常困難,甚至無法做到的。這種困難的主要原因是所有邏輯組件共存于同一個二進(jìn)制文件中,因此它們也會處于同一個執(zhí)行環(huán)境中。使用微服務(wù)的一個巨大優(yōu)勢是我們可以允許獨(dú)立的邏輯組件孤立地發(fā)生故障,防止故障在整個系統(tǒng)中廣泛傳播并危及其他組件。分析服務(wù)如何共同失敗的設(shè)計過程通常稱為故障隔離。
在我們的示例中,PetPic 獨(dú)立部署在兩個不同的區(qū)域:Happytails 和 Furland。但是,這些區(qū)域的性能表現(xiàn)與為這兩個區(qū)域提供服務(wù)的全局?jǐn)?shù)據(jù)庫的性能密切相關(guān)。正如我們目前所觀察到的,Happytails 和 Furland 的客戶有著截然不同的興趣,因此很難調(diào)整數(shù)據(jù)庫來高效地為這兩個地區(qū)提供服務(wù)。Furland 客戶訪問數(shù)據(jù)庫方式的變化可能讓 Happytail 用戶遇到糟糕的用戶體驗,反之亦然。
有一些方法可以避免此類問題,例如使用有界本地緩存,如圖 4 所示。本地緩存可以帶來增強(qiáng)的用戶體驗,因為它還可以減少響應(yīng)延遲和數(shù)據(jù)庫資源使用。緩存大小可以適應(yīng)本地流量而不是全局利用率。它還可以在后端中斷的情況下提供保存的數(shù)據(jù),從而實現(xiàn)服務(wù)的優(yōu)雅降級。
緩存也可能會帶來特定于應(yīng)用程序或業(yè)務(wù)需求的問題——例如你有很高的數(shù)據(jù)新鮮度或擴(kuò)展需求時。常見問題包括由于資源限制和查詢各種緩存時的一致性導(dǎo)致緩存延遲緩慢增加。此外,服務(wù)不應(yīng)該依賴緩存的內(nèi)容來提供服務(wù)。
圖 4:使用有界本地緩存進(jìn)行故障隔離
產(chǎn)品架構(gòu)中的其他組件呢?對所有內(nèi)容都使用緩存是否合理?你能否將在云中運(yùn)行的服務(wù)隔離到特定區(qū)域?這兩個問題的答案都是肯定的,如果可以,你應(yīng)該實施這些策略。在云中運(yùn)行服務(wù)并不能避免它成為全局中斷的根源。運(yùn)行在不同云區(qū)域的服務(wù)仍然可以作為全局服務(wù)運(yùn)行,因此可以成為單點(diǎn)故障來源。將服務(wù)隔離到故障域是一種架構(gòu)決策,并不能僅由運(yùn)行服務(wù)的基礎(chǔ)架構(gòu)來保證。
讓我們考慮另一個 PetPic 的使用場景,但這次將重點(diǎn)放在控制(Control)組件上。該組件會執(zhí)行一系列內(nèi)容質(zhì)量驗證。開發(fā)團(tuán)隊最近基于機(jī)器學(xué)習(xí)(ML)將自動濫用檢測程序集成到了控制組件中,這使得每張新圖片在上傳到服務(wù)后立即得到驗證。當(dāng) Happytails 的新客戶開始將大量不同動物的圖片上傳到 PetPic 時,問題開始出現(xiàn)了,因為 PetPic 設(shè)計為只提供狗和貓的圖片。上傳流激活了我們控制組件中的自動濫用檢測,但新的 ML 例程無法跟上請求的數(shù)量。
該組件運(yùn)行在 1000 個線程池中,并將專用于濫用例程的線程數(shù)量限制為一半,即 500 個線程。如果大量長處理請求一起到達(dá),這應(yīng)該有助于防止線程饑餓,就像我們這里的例子一樣。工程師沒想到的是,一半的線程最后消耗了所有可用的內(nèi)存和 CPU,導(dǎo)致這兩個地區(qū)的客戶在將圖像上傳到 PetPic 時開始遇到很高的延遲。
我們?nèi)绾螠p輕用戶在這種情況下所經(jīng)歷的痛苦呢?如果我們將控制組件運(yùn)維活動隔離到單個區(qū)域,就可以進(jìn)一步限制這種濫用情況的影響范圍。即使服務(wù)運(yùn)行在云中,確保每個區(qū)域都有自己的專用控制實例可以保證只有 Happytails 中的客戶會受到不良圖像上傳流的影響。請注意,無狀態(tài)服務(wù)很容易被限制在故障域中。隔離數(shù)據(jù)庫并不總是可行的,但你可以考慮從緩存中實現(xiàn)本地讀取,以及偶爾的跨區(qū)域一致性作為一個很好的折衷方案。處理棧應(yīng)該盡可能實現(xiàn)區(qū)域隔離。
要點(diǎn)
將服務(wù)棧中的所有服務(wù)保持在同一位置,并限制在同一故障域中,可以防止廣泛傳播的全局中斷。將無狀態(tài)服務(wù)隔離到故障域通常比隔離有狀態(tài)組件更容易。如果無法避免跨區(qū)域通信,請考慮優(yōu)雅降級和最終一致性的策略。
場景三:規(guī)劃 SLO
在這最后一個場景中,我們將查看 PetPic 的 SLO,并驗證每個數(shù)據(jù)包提供的 SLO 情況。簡而言之,SLO 是提供服務(wù)時要達(dá)成的目標(biāo),可以通過合同綁定在我們與客戶的 SLA 中。讓我們看一下圖 5 中的表格:
圖 5:PetPic 的 SLO
此表顯示了工程師眼中將為 PetPic 客戶提供出色用戶體驗的 SLO。在這里,我們還可以看到每個內(nèi)部組件提供的 SLO。請注意,API SLO 必須基于 API 后端(例如 Control 和 Data)的 SLO 構(gòu)建。如果需要更好的 API SLO,但我們又無法做到,我們需要考慮更改產(chǎn)品設(shè)計并與后端所有者合作以提供更高的性能和可用性??紤]到我們最新的 PetPic 架構(gòu),讓我們看看 API 的 SLO 是否有意義。
讓我們從運(yùn)維后端(我們將其稱為“Ops”)開始,它是后端的一部分,用于收集 PetPic API 的健康指標(biāo)。API 服務(wù)僅調(diào)用 Ops 來提供與運(yùn)維相關(guān)的請求、錯誤和處理時間的監(jiān)控數(shù)據(jù)。所有對 Ops 的寫入都是異步完成的,故障不會影響 API 服務(wù)質(zhì)量??紤]到這些因素,我們在為 PetPic 設(shè)計外部 SLO 時可以忽略 Ops SLO。
圖 6:將讀取 SLO 與數(shù)據(jù)庫對齊
現(xiàn)在,讓我們來看看從 PetPic 讀取圖片的用戶旅程。內(nèi)容質(zhì)量只在新數(shù)據(jù)注入 PetPic 時才會進(jìn)行驗證,因此數(shù)據(jù)讀取不會受到控制服務(wù)性能表現(xiàn)的影響。除了檢索圖像信息外,API 服務(wù)還需要處理請求,我們的基準(zhǔn)測試表明這需要大約 30 毫秒。準(zhǔn)備好發(fā)送圖像后,API 需要構(gòu)建一個響應(yīng),平均需要大約 20 毫秒。僅在 API 中,每個請求的處理時間就增加了 50 毫秒。
如果我們能保證至少有一半的請求會命中本地緩存中的一個條目,那么承諾第 50 個百分位數(shù)和 100 毫秒的 SLO 是非常合理的。請注意,如果我們沒有本地緩存??,請求延遲將至少為 150 毫秒。對于其他所有請求,圖像需要從數(shù)據(jù)庫中查詢。數(shù)據(jù)庫需要 100 到 240 毫秒才能回復(fù),并且它可能不會與 API 服務(wù)共存。網(wǎng)絡(luò)延遲平均為 100 毫秒。如果我們考慮這些數(shù)字的最壞情況,請求可能花費(fèi)的最長時間是 50 毫秒(API 處理) 10 毫秒(考慮緩存未命中) 100 毫秒(網(wǎng)絡(luò)) 240 毫秒(數(shù)據(jù)),總計 400 毫秒。如果我們查看圖 6 左列中的 SLO,我們可以看到這些數(shù)字與 API 后端結(jié)構(gòu)很好地對齊了。
圖 7:將寫入 SLO 與控制和數(shù)據(jù)庫組件對齊
按照相同的邏輯,我們來檢查上傳新圖像的 SLO。當(dāng)客戶請求向 PetPic 加載新圖像時,API 必須請求控制組件以驗證內(nèi)容,這需要 150 毫秒到 800 毫秒。除了檢查濫用內(nèi)容之外,控制組件還會驗證圖像??是否已存在于數(shù)據(jù)庫中?,F(xiàn)有圖像被視為已驗證(并且不需要重新驗證)。歷史數(shù)據(jù)顯示,F(xiàn)urland 和 Happytails 的客戶傾向于在兩個地區(qū)上傳相同的圖像集。當(dāng)數(shù)據(jù)庫中已經(jīng)存在圖像時,Control 組件可以為其創(chuàng)建一個新 ID,而無需復(fù)制數(shù)據(jù),這需要大約 50 毫秒。這段旅程適合大約一半的寫入請求,50%的延遲情況總計為 250 毫秒。
包含濫用內(nèi)容的圖像通常需要更長的時間來處理??刂平M件返回響應(yīng)的時間限制為 800 毫秒。此外,如果圖像是狗或貓的有效圖片,并且假設(shè)它不在數(shù)據(jù)庫中,則數(shù)據(jù)組件可能需要長達(dá) 1000 毫秒來保存它??紤]到所有數(shù)字,在最壞的情況下,返回響應(yīng)可能需要近 2000 毫秒。正如你在圖 7 的左列中所見,2000 毫秒遠(yuǎn)遠(yuǎn)高于當(dāng)前 SLO 工程師為寫入而預(yù)測的時間,這表明他們在提出 SLO 時可能忘了包括不良場景。為了緩解這種不匹配現(xiàn)象,你可以考慮將第 99 個百分位的 SLO 與請求截止時間綁定。這種情況也可能導(dǎo)致服務(wù)性能不佳或錯誤。例如,數(shù)據(jù)庫可能會在 API 向客戶端報告超過操作期限后才完成圖像寫入過程,從而導(dǎo)致客戶端混亂。在這種情況下,最好的策略是與數(shù)據(jù)庫團(tuán)隊合作提高數(shù)據(jù)庫性能或調(diào)整 PetPic 的寫入 SLO。
要點(diǎn)
確保你的分布式產(chǎn)品為客戶提供正確的 SLO 是非常重要的。在構(gòu)建外部 SLO 時,你必須考慮所有后端的當(dāng)前 SLO。你應(yīng)該考慮所有不同的用戶旅程以及請求生成響應(yīng)可能采取的不同路徑。如果需要更好的 SLO,請考慮更改服務(wù)架構(gòu)或與后端所有者合作以改進(jìn)服務(wù)。將服務(wù)和后端保持在同一位置,可以更輕松地確保 SLO 對齊。
作者介紹
Silvia Esparrachiari 已在谷歌擔(dān)任軟件工程師 11 年了,她曾在用戶數(shù)據(jù)隱私、垃圾郵件和濫用預(yù)防領(lǐng)域任職,最近在谷歌 Cloud SRE 工作。她擁有分子科學(xué)學(xué)士學(xué)位和計算機(jī)視覺和人機(jī)交互碩士學(xué)位。她目前在谷歌的工作重點(diǎn)是促進(jìn)一個相互尊重和多元化的環(huán)境,讓人們可以更好地提高他們的技術(shù)技能。
Betsy Beyer 是谷歌紐約分部的技術(shù)作家,專門研究站點(diǎn)可靠性工程(SRE)。她與他人合著了《站點(diǎn)可靠性工程:谷歌如何運(yùn)行生產(chǎn)系統(tǒng)》(2016 年)、《站點(diǎn)可靠性工作簿:實施 SRE 的實用方法》(2018 年)和《構(gòu)建安全可靠的系統(tǒng)》(2020 年)。在她迄今為止的職業(yè)生涯中,Betsy 還學(xué)習(xí)了國際關(guān)系和英國文學(xué),她擁有斯坦福大學(xué)和杜蘭大學(xué)的學(xué)位。