★微服務系列
微服務1:微服務及其演進史
微服務2:微服務全景架構
微服務3:微服務拆分策略
微服務4:服務註冊與發現
微服務5:服務註冊與發現(實踐篇)
微服務6:通訊之閘道器
微服務7:通訊之RPC
微服務8:通訊之RPC實踐篇(附原始碼)
微服務9:服務治理來保證高可用
1 微服務帶來的挑戰
在第2篇《微服務2:微服務全景架構 》中,我們曾經分析過微服務化後所面臨的挑戰,有過如下的結論:
1.1 分散式固有複雜性
微服務架構是基於分散式的系統,而構建分散式系統必然會帶來額外的開銷。
效能: 分散式系統是跨程式、跨網路的呼叫,受網路延遲和頻寬的影響。
可靠性: 由於高度依賴於網路狀況,任何一次的遠端呼叫都有可能失敗,隨著服務的增多還會出現更多的潛在故障點。因此,如何提高系統的可靠性、降低因網路引起的故障率,是系統構建的一大挑戰。
分散式通訊: 分散式通訊大大增加了功能實現的複雜度,並且伴隨著定位難、除錯難等問題。
資料一致性: 需要保證分散式系統的資料強一致性,即在 C(一致性)A(可用性)P(分割槽容錯性) 三者之間做出權衡。這塊可以參考我的這篇《分散式事務》。
1.2 服務的依賴管理和測試
在單體應用中,通常使用整合測試來驗證依賴是否正常。而在微服務架構中,服務數量眾多,每個服務都是獨立的業務單元,服務主要通過介面進行互動,如何保證它的正常,是測試面臨的主要挑戰。所以單元測試和單個服務鏈路的可用性非常重要。
1.3 有效的配置版本管理
在單體系統中,配置可以寫在yaml檔案,分散式系統中需要統一進行配置管理,同一個服務在不同的場景下對配置的值要求還可能不一樣,所以需要引入配置的版本管理、環境管理。
1.4 自動化的部署流程
在微服務架構中,每個服務都獨立部署,交付週期短且頻率高,人工部署已經無法適應業務的快速變化。有效地構建自動化部署體系,配合服務網格、容器技術,是微服務面臨的另一個挑戰。
1.5 對於DevOps更高的要求
在微服務架構的實施過程中,開發人員和運維人員的角色發生了變化,開發者也將承擔起整個服務的生命週期的責任,包括部署、鏈路追蹤、監控;因此,按需調整組織架構、構建全功能的團隊,也是一個不小的挑戰。
1.6 更高運維成本
運維主要包括配置、部署、監控與告警和日誌收集四大方面。微服務架構中,每個服務都需要獨立地配置、部署、監控和收集日誌,成本呈指數級增長。服務化粒度越細,運維成本越高。
2 迫切的治理需求
正是因為有這些弊端,所以對微服務來說,有了更迫切的服務治理需求,以彌補弊端產生的問題。
可以看看下面的這張圖,這是一個典型的微服架構,他包含4層的Load Balance,7層的GateWay,計算服務,儲存服務,及其他的一些中介軟體系統。
實際上,但凡有需要微服務化的系統,都是具備一定規模了。一般會有很多模組構成,相應的部署節點也會非常多,這樣故障的概率就會大幅增加,比如磁碟故障、網路故障,機器當機,觸發一些核心bug或者是執行環境漂移等。
本質上也是微服務細粒度拆分後提升了出問題的概率,正如上面說的,分散式系統有它固有的複雜性,相比於單體服務錯誤會顯著的增多,需要高可用方案來保證複雜通訊鏈路的健壯性。
3 如何進行服務可用性治理
我們有很多種方法對服務進行治理來保障服務的高可用。但總的來說有4類:
- 流量調控:方法主要是金絲雀釋出(灰度釋出)、ABTesting、流量染色。
- 請求高可用:方法主要有超時重試、快速重試以及負載均衡。
- 服務的自我保護:主要包括限流、熔斷和降級。
- 應對故障例項:主要分為異常點驅逐和主動健康檢查。
3.1 流量調控
3.1.1 金絲雀釋出、ABTesting
流量排程中典型的金絲雀場景,你可以先放行一部分流量到一個新的服務例項中,這個新的服務例項只有你的研發和測試團隊可以接入。可以在上面試用或者測試,直到你確認你的服務是健康的,沒有bug的,再把流量逐漸的遷移過去。
這個的好處是減少釋出新功能存在的風險,而且全程是無停服釋出,對使用者是透明無感知的,大大提高了可用性。
3.1.2 流量染色
流量染色也是一種典型的場景。如果你想讓不同的使用者群體(比如這邊的Group A、Group B、Group C)使用的功能也是不同的,那流量染色是一個不可缺少的功能。
它可以把符合某些特徵的使用者流量調控到對應的服務版本中。比如GroupA是學生群體,對應到V1版本,GroupB是老人群體,對應到V2版本。需要注意的是,如果是一條完整的鏈路,那鏈路上的各個服務包括資料儲存層都應該有不同的版本,這樣才能一一對應。
3.2 請求高可用
3.2.1 超時
假設你有兩個服務,服務A和服務B,服務A向服務B請求資料。但是B服務由於非常繁忙,在給定的時間週期內(紅色時間線)都無法響應。而這個紅色時間線是A服務固定的超時時間,如果這個時間之後還沒有等到B服務的響應,A服務就不等了,去執行其他的任務。
這個其實就是一個超時的基本概念。它的意義在於可以避免一些長時間的無意義的等待,因為這個時候下游可能是處於故障或者有請求堆積,短時間內可能是無法返回正常的結果的。
因此,服務A在超時之後,可以及時釋放自己的一些資源,比如執行緒或者是請求相關的其他資源。
在實踐中,超時時間的設定通常要比正常的請求時間稍微大一些(正常的返回時間可以根據平響進行分析),這樣可以避免請求還沒有來得及返回就觸發超時。當然這個超時時間也不能設定太大。如果太大的話,在服務B出現異常的時候,服務A不能夠及時的釋放資源,會導致請求堆積,降低自身服務的吞吐能力。
3.2.2 重試
跟上面一樣,A服務向B服務獲取資料,由於B服務非常繁忙,在給定的超時時間內無法獲得響應資料。於是A設定了重試機制,在超時時間結束之後,重新獲取一下B服務的資料,這時候B服務已經不忙碌了,很快就把正常資料響應給A服務。
可以看到,重試的意義在於可以提升一次請求的成功率。通常重試不僅可以配合超時,也可以配合一些其他種類的失敗。
比如B服務5xx錯誤了,但可能是有概率的錯誤,所以重試一次就可能獲取到想要的結果。當然重試也有一些注意事項,避免重試帶來其他災難。
- 重試儘量避開之前已經選擇過的失敗例項,因為這個時候再重試,大概率還是錯誤的,意義不是特別大。
- 其次重試的次數也不能太多,否則很容易對服務B造成數倍的壓力,導致服務B發生一些雪崩。
- 對於多次重試,我們通常可以配合一些類似於退避重試的策略來減緩對服務B的壓力。退避策略說明,演算法
3.2.3 快速重試(backup request)
同上面一樣,服務A向服務B發起一個正常的請求,服務B工作繁忙,A服務在給定的超時時間之前都無法獲得響應。按照之前的做法就是等超時時間達到的時候,再發起一次請求。
但是可以在超時時間到達之前做一次更智慧的處理,比如超時時間線的中間點,再請求一次服務B。可以看到,重試其實就是一次backup request。正是由於它在正常的超時之前就觸發了,所以我們叫它快速重試。
這次快速重試,剛好服務B可能已經緩過來。他就收到了這個請求,給服務A返回了這個結果。
服務A在正常的超時觸發之後,就是紅色這條線,他也會發起一次正常的標準重試,這個時候服務B也有可能會再給他返回一次資訊,快速重試的返回和正常返回,他們的時序是有可能不一定的。通常我們的處理方法是服務A先收到哪一個回覆,就以哪個為準。後面收到了就會被拋棄掉。
這邊需要注意的是,多一次重試會有一定的額外資源開銷,所以在使用的時候,需要注意快速重試和正常重試合理設定,避免總的重試次數過多導致服務可用性反受影響。
3.2.4 負載均衡
假設服務B有多個例項,對於服務A的請求,我們希望它是比較均勻的流向B服務的各個例項,這樣才能真正做到負載均衡,提供更穩定的服務。比較常用的一些策略比如隨機輪詢、RR順序輪詢等。
我們在實際的生產實踐中會有一些比較高階的負載均衡策略,比如說Least Conn、Least Request,他會觀測你後端叢集中連線數、請求數最少的例項進行分配。
還有如LA負載均衡策略,它可以動態計算後端的壓力,比如說,可以根據qps和延遲來算一個權重。那些qps很高,延遲又很低的後端實力,我們可以認為它是一個比較優秀的後端例項,處理能力比較強,我們會給它比較高的權重,反之就給一個稍微低一些的權重。
另外一個比較常用的負載均衡策略就是一致性雜湊。一致性雜湊指的是說保證相同來源的請求能夠落入相同的後端例項。這在一些後端例項有快取,或者是有一些類似的場景的話,可以大幅提升請求的效能。
3.3 服務的自我保護
3.3.1 限流
正常一個長期穩定執行的服務,他們的請求是正常波形狀且符合預期的,你可以觀察他的流量峰值、平均值來判斷服務真正的吞吐。
如果你的服務突然遇到持續性的、高頻率的、不符合預期的突發流量。你需要檢查一下服務是否有被錯誤呼叫、惡意攻擊,或者下游程式邏輯問題。參考我的這一篇,就是典型的下游瘋狂呼叫。
這種超出預期的呼叫經常會造成你的服務響應延遲,請求堆積,甚至服務雪崩。而雪崩會隨著呼叫鏈向上傳遞,導致整個服務鏈的崩潰,對我們的系統造成很大的隱患。
限流通常指的是上游服務(服務B)這一側,任何服務處理能力總是有限的,所以在超過他的處理能力之後,我們需要一些保護行為來避免服務過載。通常的做法就是讓服務B快速返回失敗來進行自我保護,否則大量請求在服務B堆積,在請求佇列裡阻塞,會造成大量資源損耗,也導致正常的請求無法被有效處理。
一些常見的限流方法,比如QPS限流、連線數限流,併發請求數限流等,都可以有效的對服務進行保護,下面列舉幾種常見的限流演算法。
-
時間窗:簡單易用
時間窗實現非常簡單,大概原理是我們有一個統計的時間窗,比1分鐘之內,我們只允許通過1000個請求。但是這種方法有一個比較大的缺陷,就是不太穩定。比如剛好在前10秒,就來了1000個請求,而後面50秒就不能夠再接受任何請求了,非常的不均勻。所以在要求嚴謹的生產環境中比較少使用。 -
漏桶演算法:定速流出
對於漏桶來說,由於它的出水口的速度是恆定的,也就是消化處理請求的速度是恆定的,所以它可以保證元件以恆定的速率來處理請求,這對一些對處理速度或者資源有嚴格要求的系統是非常實用的。 -
令牌桶:定速流入
令牌桶配了一個蓄水池,這個蓄水池通常如果請求比較少的話,那麼它一直往蓄水池裡面放水,就會導致這個蓄水池的容量會稍微多一些。那這樣的話,在接下來的一個瞬時的流量高峰,它可以允許系統經過比這個平均速率更高一些的請求高峰。所以它有一定的彈性,在實踐中也得到了非常廣泛的使用。
3.3.2 熔斷和降級
假設服務B非常繁忙,對於服務A正常的請求未能及時的返回結果,一直在Pending。而服務A在觸發超時時間之後,按照重試策略又發了一次請求,服務B依然沒有給返回。服務A經過多次重試,覺察到服務B有一些異常,他就自己做決策,認為服務B可能已經無法提供服務了,這個時候繼續發出重試請求,可能意義不太大。所以它主動發起熔斷(注意,是服務A發起的,因為B服務可能已經死了),就是一段時間內不再請求服務B。
A既然發起了熔斷,那他總得返回對應的資訊給使用者,不能讓使用者或者更下游的服務一直等待。
這時候的處理方式就是fall back 到預設的處理資訊,比如跳到一個預設的函式,指定返回預設設定的靜態資訊。或者對返回值進行降級,比如說是之前請求成功的一些資訊,或者一些快取的舊值,這樣比什麼都不返回好很多。
筆者這邊的做法是制定一個特定的返回結構(包含狀態碼、錯誤資訊、flag),帶有特定資訊標誌,讓前端可以識別出是正常的返回還是熔斷自動返回。
3.4 例項故障後的離群檢測
3.4.1 異常點驅逐
當叢集中的某個服務例項發生故障的時候,其實我們最優先的做法是先進行驅逐,然後再檢查問題,處理問題並恢復故障。所以,能否快速的對異常例項進行驅逐,對提高系統的可用性很重要。
下面的是ServiceMesh中Istio的異常驅逐配置,表示每秒鐘掃描一次上游主機,連續失敗 2 次返回 5xx 錯誤碼的所有主機會被移出負載均衡連線池 3 分鐘,並且上游被離群的主機在叢集中佔比不應該超過10%。
outlierDetection:
consecutiveErrors: 2
interval: 1s
baseEjectionTime: 3m
maxEjectionPercent: 10
說明:
驅逐: 一段時間內出現多次失敗,遮蔽該例項一段時間
恢復: 遮蔽時間之後,再嘗試請求該例項