微服務,再見

微服務,再見

 

來源:https://madao.me/goodbye-microservices/

本文翻譯自Alexandra Noonan 的 《Goodbye Microservices: From 100s of problem children to 1 superstar》

內容是描述 Segment 的架構如何從 「單體應用」 -> 「微服務」 -> 「140+ 微服務」 -> 「單體應用」的一個歷程。翻譯比較粗糙,如有疏漏,請不吝指教。

注:下文說的目的地就是對應的不同的數據平臺(例如Google Analytics, Optimizely)

除非你生活在石器時代,不然你一定知道「微服務」是當世最流行的架構。我們Segment早在2015年就開始實踐這一架構。這讓我們在一些方面上吃了不少甜頭,但很快我們發現:在其他場景,他時不時讓我們吃了苦頭。

簡而言之,微服務的主要宣傳點在於:模塊化優化,減少測試負擔,更好的功能組成,環境獨立,而且開發團隊是自治的(因爲每一個服務的內部邏輯是自洽且獨立的)。

而另一頭的單體應用:「巨大無比且難以測試,而且服務只能作爲一個整理來伸縮(如果你要提高某一個服務的性能,只能把服務器整體提高)」

2017 早期,我們陷入了僵局,複雜的微服務樹讓我們的開發效率驟減,並且每一個開發小組都發現自己每次實現都會陷入巨大的複雜之中,此時,我們的缺陷率也迅速上升。

最終,我們不得不用三個全職工程師來維護每一個微服務系統的正常運行。這次我們意識到改變必須發生了,本文會講述我們如何後退一步,讓團隊需要和產品需求完全一致的方法。

 

爲什麼微服務曾經可行?

Segment 的客戶數據基礎設施吸收每秒成百上千個事件,將每一個夥伴服務的API 請求結果一個個返回給對應的服務端的「目的地」。

而「目的地」有上百種類別,例如Google Analytics, Optimizely,或者是一些自定義的webhook。

幾年前,當產品初步發佈,當時架構很簡單。僅僅是一個接收事件並且轉發的消息隊列。

在這個情況下,事件是由Web或移動應用程序生成的JSON對象,例子如下:

{
"type": "identify",
"traits": {
"name": "Alex Noonan",
"email": "[email protected]",
"company": "Segment",
"title": "Software Engineer"
},
"userId": "97980cfea0067"
}

事件是從隊列中消耗的,客戶的設置會決定這個事件將會發送到哪個目的,這個事件被紛紛發送到每個目的地的API,這很有用。

開發人員只需要將他們的事件發送到一個特定的目的地,也就是Segment的API,而不是你自己實現幾十個項目集成。

如果一個請求失敗了,有時候我們會稍後重試這個事件。一些失敗的重試是安全的,但有些則不。可重試的錯誤可能會對事件目的地不造成改變。

例如:50x錯誤,速率限制,請求超時等。不可重試的錯誤一般是這個請求我們確定永遠都不會被目的地接受的。例如:請求包含無效的認證亦或是缺少必要的字段。

微服務,再見

 

此時,一個簡單的隊列包含了新的事件請求以及若干個重試請求,彼此之間事件的目的地縱橫交錯,會導致的結果顯而易見:隊頭阻塞。

這意味着在這個特定的場景下,如果一個目的地變慢了或者掛掉了,重試請求將會充斥這個隊列,從而整個請求隊列會被拖慢。

想象下我們有一個 目的地 X 遇到一個臨時問題導致每一個請求都會超時。這不僅會產生大量尚未到達目的地 X的請求,而且每一個失敗的事件將會被送往重試的隊列。

即便我們的系統會根據負載進行彈性伸縮,但是請求隊列深度突然間的增長會超過我們伸縮的能力,結果就是新的時間推送會延遲。

發送時間到每一個目的地的時間將會增加因爲目的地X 有一個短暫的停止服務(因爲臨時問題)。客戶依賴於我們的實時性,所以我們無法承受任何程度上的緩慢。

微服務,再見

 

爲了解決這個隊頭阻塞問題,我們團隊給每一個目的地都分開實現了一個隊列

這種新架構由一個額外的路由器進程組成,該進程接收入站事件並將事件的副本分發給每個選定的目標。

現在如果一個目的地有超時問題,那麼也僅僅是這個隊列會進入阻塞而不會影響整體。這種「微服務風格」的架構分離把目的地彼此分開,當一個目的地老出問題,這種設計就顯得很關鍵了。

微服務,再見

 

 

個人Repo的例子

每一個目的地的API 的請求格式都不同,需要自定義的代碼去轉換事件來匹配格式。

一個簡單的例子:還是目的地X,有一個更新生日的接口,作爲請求內容的格式字段爲 dob ,API 會對你要求字段爲 birthday,那麼轉換代碼就會如下:

const traits = {}
traits.dob = segmentEvent.birthday許多現代的目的地終點都用了Segment 的請求格式,所以轉換會很簡單。但是,這些轉換也可能會十分複雜,取決於目的地API 的結構。

起初,目的地分成幾個拆分的服務的時候,所有的代碼都會在一個repo 裏。一個巨大的挫折點就是一個測試的失敗常常會導致整個項目測試無法跑通。我們可能會爲此付出大量的時間只是爲了讓他像之前一樣正常運行通過測試。

爲了解決這個問題,我們把每一個服務都拆分成一個單獨的repo,所有的目的地的測試錯誤都只會影響自己,這個過渡十分自然。

拆分出來的repo 來隔離開每一個目的地會讓測試的實現變得更容易,這種隔離允許開發團隊快速開發以及維護每一個目的地。

 

伸縮微服務和Repo們

隨着時間的偏移,我們加了50多個新的目的地,這意味着有50個新的repo。

爲了減輕開發和維護這些codebase 的負擔,我們創建一個共享的代碼庫來做實現一些通用的轉換和功能,例如HTTP 請求的處理,不同目的地之間代碼實現更具有一致性。

例如:如果我們要一個事件中用戶的名字,event.name 可以是任何一個目的地裏頭的調用。

共享的類庫會去嘗試判斷event 裏的 name 或者 Name 屬性,如果沒有,他會去查 first name,那麼就回去查找first_name 和 FirstName,往下推:last name 也會做這樣的事情。然後吧first name 和last name 組合成full name.

Identify.prototype.name = function {
var name = this.proxy('traits.name');
if (typeof name === 'string') {
return trim(name)
}
var firstName = this.firstName;
var lastName = this.lastName;
if (firstName && lastName) {
return trim(firstName + ' ' + lastName)
}
}

共享的代碼庫讓我們能快速完成新的目的地的實現,他們之間的相似性帶給我們一致性的實現而且維護上也讓我們減少了不少頭疼的地方。

儘管如此,一個新的問題開始發生並蔓延。共享庫代碼改變後的測試和部署會影響所有的目的地。

這開始讓我們需要大量時間精力來維護它。修改或者優化代碼庫,我們得先測試和部署幾十個服務,這其中會帶來巨大的風險。時間緊迫的時候,工程師只會在某個特定的目的地去更新特定版本的共享庫代碼。

緊接着,這些共享庫的版本開始在不同的目標代碼庫中發生分歧。微服務起初帶給我們的種種好處,在我們給每一個目的地都做了定製實現後開始反轉。

最終,所有的微服務都在使用不同版本的共享庫——我們本可以用自動化地發佈最新的修改。但在此時,不僅僅是開發團隊在開發中受阻,我們還在其他方面遇到了微服務的弊端。

這額外的問題就是每一個服務都有一個明確的負載模式。一些服務每天僅處理寥寥幾個請求,但有的服務每秒就要處理上千個請求。

對於處理事件較少的目的地,當負載出現意外峯值時,運維必須手動伸縮服務以滿足需求。(編者注,肯定有解決方案,但原作者突出的還是複雜度和成本。)

當我們實現了自動伸縮的實現,每個服務都具有所需CPU和內存資源的明顯混合,這讓我們的自動伸縮配置與其說是科學的,不如說更具有藝術性(其實就是蒙的)。

目的地的數量極速增長,團隊以每個月三個(目的地)的速度增長着,這意味着更多的repo,更多的隊列,更多的服務。

我們的微服務架構的運維成本也是線性地增長着。因此,我們決定退後一步,重新考慮整個流程。

 

深挖微服務以及隊列

這時列表上第一件事就是如何鞏固當前超過140個服務到一個服務中,管理所有服務的帶來的各種成本成了團隊巨大的技術債務。運維工程師幾乎無眠,因爲隨時出現的流量峯值必須讓工程師隨時上線處理。

儘管如此,當時把項目變成單一服務的架構是一個巨大的挑戰。要讓每一個目的地擁有一個分離的隊列,每一個 worker進程需要檢查檢查每一隊列是否運行,這種給目的地服務增加一層複雜的實現讓我們感到了不適。

這是我們「離心機」的主要靈感來源,「離心機」將替換我們所有的個體隊列,並負責將事件發送到一個單體服務。

譯者注:「離心機」其實就是Segment 製作的一個事件分發系統。 相關地址

 

搬到一個單體Repo

所以我們開始把所有的目的地代碼合併到了一個repo,這意味着所有的依賴和測試都在一個單一的repo 裏頭了,我們知道我們要面對的,會是一團糟。

120個依賴,我們都提交了一個特定的版本讓每一個目的地都兼容。當我們搬完了目的地,我們開始檢查每一個對應的代碼是否都是用的最新的依賴。我們保證每一個目的地在最新的依賴版本下,都能正確運行。

這些改變中,我們再也不用跟蹤依賴的版本了。所有目的地都使用同一版本,這顯著地減小了codebase 的代碼複雜度。維護目的地變得快捷而且風險也變小了。

另一方面我們也需要測試能簡單快速地運行起來,之前我們得出的結論之一就是:「不去修改共享庫文件主要的阻礙就是得把測試都跑一次。」

幸運的是,目的地測試都有着相似的架構。他們都有基礎的單元測試來驗證我們的自定義轉換邏輯是否正確,而且也能驗證HTTP 的返回是否符合我們的期望值。

回想起我們的出新是分離每一個目的地的codebase 到各自的repo 並且分離各自測試的問題。

儘管如此,現在看來這個想法是一個虛假的優勢。HTTP 請求的發送仍然以某種頻率失敗着。因爲目的地分離到各自的repo,所以大家也沒有動力去處理這類失敗的請求。

這也讓我們走進了某種令人沮喪的惡性循環。本應只需幾個小時的小改動常常要花上我們幾天甚至一週的時間。

 

構建一個彈性測試套件

給目的地發送的HTTP 請求失敗是我們主要的失敗測試原因,過期憑證等無關的問題不應該使測試失敗。

我們從中也發現一些目的地的請求會比其他目的地慢不少。一些目的地的測試得花上5 分鐘才能跑完,我們的測試套件要花上一小時時間才能全部跑完。

爲了解決這個問題,我們製作了一個「Traffic Recorder」,「Traffic Recorder」是一個基於yakbak 實現的工具,用於記錄並且保存一些請求。

無論何時一個測試在他第一次跑的時候,對應的請求都會被保存到一個文件裏。後來的測試跑的時候,就會複用裏頭的返回結果。

同時這個請求結果也會進入repo,以便在測試中也是一致的。這樣一來,我們的測試就不再依賴於網絡HTTP請求,爲了接下來的單一repo 鋪好了路。

記得第一次整合「Traffic Recorder」後,我們嘗試跑一個整體的測試,完成 140+ 目的地的項目整體測試只需幾毫秒。這在過去,一個目的地的測試就得花上幾分鐘,這快得像魔術一般。

 

爲何單體應用可行

只要每個目的地都被整合到一個repo,那麼他就能作爲一個單一的服務運行。所有目的地都在一個服務中,開發團隊的效率顯著提高。我們不因爲修改了共享庫而部署140+ 個服務,一個工程師可以一分鐘內重新完成部署。

速度是肉眼可見地被提升了,在我們的微服務架構時期,我們做了32個共享庫的優化。再變成單體之後我們做了46個,過去6個月的優化甚至多過2016年整年。

這個改變也讓我們的運維工程師大爲受益,每一個目的地都在一個服務中,我們可以很好進行服務的伸縮。巨大的進程池也能輕鬆地吸收峯值流量,所以我們也不用爲小的服務突然出現的流量擔驚受怕了。

 

壞處

儘管改變成單體應用給我們帶來巨大的好處,儘管如此,以下是壞處:

1. 故障隔離很難,所有東西都在一個單體應用運行的時候,如果一個目的地的bug 導致了服務的崩潰,那麼這個目的地會讓所有的其他的目的地一起崩潰(因爲是一個服務)。

我們有全面的自動化測試,但是測試只能幫你一部分。我們現在在研究一種更加魯棒的方法,來讓一個服務的崩潰不會影響整個單體應用。

2. 內存緩存的效果變低效了。之前一個服務對應一個目的地,我們的低流量目的地只有少量的進程,這意味着他的內存緩存可以讓很多的數據都在熱緩存中。

現在緩存都分散給了3000+個進程所以緩存命中率大大降低。最後,我們也只能在運維優化的前提下接受了這一結果。

3. 更新共享庫代碼的版本可能會讓幾個目的地崩潰。當把項目整合的到一起的時候,我們解決過之前的依賴問題,這意味着每個目的地都能用最新版本的共享庫代碼。

但是接下來的共享庫代碼更新意味着我們可能還需要修改一些目的地的代碼。在我們看來這個還是值得的,因爲自動化測試環節的優化,我們可以更快的發現新的依賴版本的問題。

 

結論

我們起初的微服務架構是符合當時的情況的,也解決了當時的性能問題還有目的地之間孤立實現。

儘管如此,我們沒有準備好服務激增的改變準備。當需要批量更新時,我們缺乏適當的工具來測試和部署微服務。結果就是,我們的研發效率因此出現了滑坡。

轉向單體結構使我們能夠擺脫運維問題,同時顯着提高開發人員的工作效率。我們並沒有輕易地進行這種轉變,直到確信它能夠發揮作用。

1. 我們需要靠譜的測試套件來讓所有東西都放到一個repo。沒有它,我們可能最終還是又把它拆分出去。頻繁的失敗測試在過去損害了我們的生產力,我們不希望再次發生這種情況。

2. 我們接受一些單體架構的固有的壞處而且確保我們能最後得到一個好的結果。我們對這個犧牲是感到滿意的。

在單體應用和微服務之間做決定的時候,有些不同的因素是我們考慮的。在我們基礎設施的某些部分,微服務運行得很好。但我們的服務器端,這種架構也是真實地傷害了生產力和性能的完美示例。

但到頭來,我們最終的解決方案是單體應用。

End

微服務、高併發、JVM調優、面試專欄等20大進階架構師專題資料私信“資料”領取

《Java學習、面試;文檔、視頻資源免費獲取》

 

微服務,再見

 

 

微服務,再見

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章