微服務 - 容錯、限流與鏈路追蹤

撰文王咚咚

出品王咚咚編輯部

容錯性設計

從歷史的角度看,沒有任何一種 架構模式(Architecture) 可以 “遺世而獨立”,它們都存在着彼此關聯的 衍生發展 的生命脈絡。架構模式都孵化於特定的技術領域,借鑑並改進了相關的設計思想,並且與現存的其他架構模式相比,有着鮮明的特點區分。

微服務(Microservices) 誕生自分佈式技術的土壤,並且脫胎於 SOA(Service-Oriented Architecture,面向服務的架構) 的設計思想。從技術實現的角度看,微服務需要針對分佈式環境下的經典技術問題,給出 普遍適用的、易推廣的、低成本 的解決方案。在本文中,我們將延續上一期的話題,談談在微服務的架構設計中,如何解決分佈式環境下天然的 複雜性不確定性 並存的難題。

讓我們回顧經典——《Microservices: A Definition of This New Architectural Term(微服務:新技術架構的定義)》(作者 :Martin Fowler 、 James Lewis ),這篇文章發表於 2014 年,它被認爲是微服務架構開始走向流行的重要標誌。在文章中,作者明確指出,微服務需要滿足 “容錯性設計(design for failure)”。

Since services can fail at any time, it's important to be able to detect the failures quickly and, if possible, automatically restore service. (由於服務可能在任何時間出現故障,因此係統快速檢測故障的能力是至關重要的,最好能夠進行自動恢復)

......

Microservice applications put a lot of emphasis on real-time monitoring of the application, checking both architectural elements (how many requests per second is the database getting) and business relevant metrics (such as how many orders per minute are received) .(微服務系統非常重視對服務的實時監控,檢查架構性元素【如數據庫負載】和業務關聯【如每分鐘訂單量】指標)

—— 摘自 《Microservices: A Definition of This New Architectural Term》

對於上述的容錯性設計目標,我們可以分爲兩大方向:流量治理服務治理。前者表示針對流量(即服務間調用) 進行的監測與容錯處理。而後者表示針對服務進程(也包括硬件、網絡與中間件等基礎設施)進行的監控與故障恢復。

服務治理的內容強依賴於基礎設施層面的具體技術工具(如 Kubernetes、Prometheus 等),我們會在後續推送中爲大家介紹。在本文中,我們僅針對流量治理方向進行展開討論。

斷路器模式

在微服務體系下,獨立的服務模塊數量衆多,形成了一個複雜的鏈路調用關係網,如下圖所示:

複雜的調用鏈路示意圖

(來自 Sentinel 官方文檔)

假如調用鏈路中某個服務出現故障,例如【ServiceD】出現了請求超時的情況,就會使得【ServiceG】和【ServiceF】的接口處於阻塞等待狀態,進一步又會影響到【ServiceA】和【ServiceB】。如果程序無法針對此類情況進行主動干預,那麼一個微小的故障就可能隨着調用鏈路迅速蔓延,最終大面積影響系統功能的可用性。

這種現象被形象地稱作 雪崩效應(avalanche effect),比喻因爲系統機制設計缺陷,導致一個局部差錯誘發了系統大面積故障。

斷路器設計模式(circuit breaker design pattern)能夠優雅地應對此類問題。這種設計模式首次出現於 Michael T. Nygard的著作《Release It!:Design and Deploy Production-Ready Software(發佈 : 設計與部署生產級軟件,2007 年出版)》。顧名思義,這本書面向生產級(Production-Ready,個人不建議使用【生產就緒】這個翻譯)的軟件設計,生產級環境必然需要考慮各類突發問題。這與我們討論微服務架構 “容錯性設計” 的話題是相呼應的。

此後,在 2014 年,Martin Fowler 也發表了一篇技術文章《Circuit Breaker(斷路器)》,他在文章中相當簡潔地闡述了斷路器的概念,斷路器的工作流程如下圖所示:

斷路器的 “熔斷” 流程

(來自《Circuit Breaker》)

斷路器就是一個特殊的對象,它通過相應的代碼組織形式,代管了受保護的遠程調用服務。一旦斷路器偵測到此服務的調用失敗(例如:timeout)次數達到閾值,則會 “跳閘(trip)”,斷路器變爲“打開” 狀態,之後當遠程調用再次發生時,斷路器會直接爲客戶端(client)返回錯誤信息,而不會真的去訪問服務端(supplier)。這個過程也被形象地稱爲 “熔斷”

斷路器這個術語來自於電力領域,這個裝置可以在電流過載時切斷電路,以保護電路上的相關設備。另一個我們更熟知的裝置就是 “保險絲(fuse)”,它們的區別在於,斷路器可以(手動或自動)復位,以恢復電路的正常使用,而保險絲則不行。所以保險絲也被稱爲 “熔斷器”。

相應的,斷路器設計模式也有 “復位” 的機制。在熔斷髮生後,如果滿足預設的條件(如熔斷持續了一定時間),斷路器會放行一次調用,以探測當前的調用結果,如果成功,斷路器就會“復位”。

斷路器的 “復位” 流程

(來自《Circuit Breaker》)

艙壁隔離模式

艙壁隔離模式(bulkhead pattern)是另一個值得介紹的容錯設計模式。所謂 “艙壁隔離”,指的是在船體底部建立多個彼此隔離的艙室,一旦船體因碰撞導致破裂進水,海水只會灌滿部分艙室,不會向船內持續擴散,船舶仍可以保持航行能力。艙壁隔離模式的核心在於 “隔離” ,我們熟悉的容器技術也可以看作是艙壁隔離的一種實踐。

容器(鯨背上的集裝箱)之間相互隔離

具體到本文的 “流量治理” 話題,我們可以舉一個現實中的例子。由於服務間的每次遠程調用通常是基於獨立線程的,假如某次遠程調用發生了超時,那麼它將會較長時間佔用一個線程資源。在鏈路複雜(可以參考本文第一張圖),且高頻次的調用場景下,因爲一個下游服務的故障,就會佔用大量的線程資源,直至線程池飽和,勢必會影響到對其他的服務調用。

解決方案也容易想到,一般有兩種模式:

一是爲每個外部服務的遠程調用設置一個獨立的線程池,例如【服務 A】需要調用【服務 B】和【服務 C】的相關接口,則【服務 A】就需要維護兩個線程池。如此以來,即使【服務 B】的調用發生了超時錯誤,線程池中的線程達到了上限,也不會影響對【服務 C】的正常使用。

二是使用信號量(Semaphore)機制,信號量表示的是相關指標的計數。例如我們可以累計特定服務的併發線程數,按照上述預想的故障場景,【服務 B】的併發線程數會在短時間內超過閾值,這裏就可以直接進行控制了。在實際的工程中,上述相關算法都是在工具組件內部實現的。兩種方法雖然在性能表現上有一定差異,但在生產級資源環境裏,基本不會存在顯著差別。

線程池機制(左)與信號量機制(右)

(來自 Hystrix 官方文檔)

流量控制

前面的內容,我們是基於服務調用者(上游服務,或稱客戶端)的視角來解讀的,現在我們轉向服務提供者(下游服務)的視角,談談 ” 流量控制(限流)“ 的問題。

衆所周知,由於計算資源等各方面限制,服務接受併發請求的規模終究是有限的。所以我們需要一種限流的機制,在瞬時流量超過系統預估的處理能力時,主動做出干預,以保證系統整體的穩定與可用。

我們都熟悉壓力測試的相關指標,如 TPS(每秒事務數,Transactions per Second)、QPS(每秒查詢數,Query Per Second)以及 併發數 等等,這裏我們可以選擇 QPS 作爲限流的指標,當一個服務提供者的接口 QPS 達到 閾值 後,則進入限流的機制,對過量的請求直接進行關閉處理。

接下來,我們就需要考慮,如何統計實時的 QPS 呢?

所謂 “流量”,即是一條永無止息的河流(這也是“流” 字的含義),我們無法預料未來水流峯值將會多大,也不知將會何時到來,只有當水流流經我們眼前時,我們才能觀測得到。

這裏介紹一種滑動窗口算法(Sliding Window Algorithm),這種算法理解起來很簡單,“河流” 在我們目光所及的眼前這一小段,就是我們觀測的 “窗口”。如下圖所示,我們可以每分鐘統計一次(此分鐘內累計的)請求數(圖中是將請求的結果分類統計的),然後置入到隊列中去。隊列長度爲 10 個元素,也就是表示了最近 10 分鐘的統計結果。這個隊列就是我們的窗口,統計窗口內的瞬時平均值,就能得到 QPS。

滑動窗口算法

(來自 Hystrix 官方文檔)

滑動窗口算法可以幫助我們計算出一段時間內流量的動態的平均值

我們還可以更進一步,使用漏桶算法(Leaky bucket Algorithm)來讓高峯期的流量 “排隊” 通過。如下圖所示,“漏桶”本身也是一種隊列,入隊的流量請求可能速度較快,但出隊的速度(取決於實際的計算能力)卻是勻速的。出隊的請求會正常進行計算處理,而隊列中的請求就相當於在“排隊“了。

漏桶算法示意圖

漏桶算法的優勢在於,當流量激增的時候,可以允許一部分過量的請求進行”排隊等待 “,而不會直接進行” 快速失敗(Failfast)“處理。當然,排隊的請求數量也是有上限的,超出上限的請求仍會被” 拋棄 “。漏桶算法與消息隊列的思想很接近,能夠起到” 削峯填谷 “的功能。這種模式也被稱爲勻速器流量整形(Traffic Shaping)。

如果我們希望讓限流機制更精準,在分佈式的環境下,可能還需要有更多的考慮。例如,我們對某個服務的接口設置了一個限流閾值,但此時在多機器節點內的其他服務的負載狀況是未知的,那麼這個閾值就可能是不精確的。所以我們需要一種全局化的、集羣化的、分佈式限流機制

這裏簡要列舉一下解決思路,就是單獨記錄一份全局的流量統計數據。如下圖所示,我們建立了一個 Token Server 服務,用於判斷某條流量是否放行。系統內原有的微服務都有一個身份——Token Client,它們在發出任何的調用請求前,都需要向 Token Server 進行申請,如果 Token Server 認爲此時全局的流量沒有超量,則允許放行。

分佈式限流

(來自 Sentinel 官方文檔)

鏈路追蹤

在流量治理話題的最後,我們有必要介紹一下分佈式鏈路追蹤系統(Distributed Systems Tracing)。因爲服務間存在着複雜的調用關係,所以我們需要一套鏈路信息收集的機制,以供日常的運維使用,包括故障排查與數據分析等等。通過分佈式鏈路追蹤系統,我們能夠爲服務間連續跳轉的請求分配一個唯一 ID,並通過可視化界面進行查閱和分析。

鏈路追蹤屬於分佈式系統的可觀測性(Observability)能力,我們熟悉的服務健康狀態監控日誌聚合等機制也屬於這一範疇。

我們不得不提到谷歌2010 年發佈的論文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure(Dapper,一個大規模分佈式系統鏈路追蹤基礎設施)》。Spring Cloud Sleuth 在文檔中明確指出,其重要的部分概念直接來自於這篇論文 。除此以外,其他幾個主流工具如 Cat、Zipkin、Pinpoint、SkyWalking 都與這篇論文提供的設計思路關係密切。

span 與 trace(來自 Sleuth 官方文檔)

這裏有兩個重要概念,trace(追蹤)span(跨度)。如上圖所示,trace 表示一次完整的內部調用鏈路,即【SERVICE1】->【SERVICE2】->【SERVICE3、4】,最終響應回到【SERVICE1】。trace 內部關聯的每一次調用,都稱作一個 span,這些 span 之間是存在拓撲結構關係的。

本期與上一期推送的內容中,我們主要側重於介紹相關概念,幾乎沒有討論具體的解決方案的工具選型。這是因爲微服務架構的發展,從技術實現角度看,有幾次重要的” 形態 “轉變,例如從 Sping Cloud NetflixSpring Cloud Kubernetes,再到 istio,目前每種” 形態“ 都有不小的市場採用份額。微服務架構在持續演進,關於它的話題,我們還會繼續討論。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CB8Zd2zd04Q7Y1ye9cEskA