告別微服務,咱們重回單體架構!

做者:馬島web

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

本文版權歸做者全部安全

------------------------------------------------bash

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

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

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

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

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

而另外一頭的單體應用:「巨大無比且難以測試,並且服務只能做爲一個整理來伸縮(若是你要提升某一個服務的性能,只能把服務器總體提升)」模塊化

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

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


爲何微服務曾經可行?

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

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

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

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

{
  "type": "identify",
  "traits": {
    "name": "Alex Noonan",
    "email": "anoonan@segment.com",
    "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

End


掃描下方二維碼試讀

《從零開始帶你成爲JVM實戰高手》詳細目錄:


相關文章
相關標籤/搜索