1. 程式人生 > >如何設計高可用的微服務架構

如何設計高可用的微服務架構

   要點

           動態的環境和分散式的系統,比如微服務,它們出現故障的機率更大。

           發生故障的服務應該被隔離開來,實現優雅的服務降級,提升使用者體驗。

           70%的故障都是因為程式碼變更引起的,所以有時候回退程式碼並不算是什麼壞事。

           如果發生故障,就要讓它們快速而獨立的發生。一個團隊無法控制他們服務的依賴項。

           快取、隔板、迴路斷路器和速率限定器這些架構模式有助於構建可靠的微服務。

 

微服務架構通過定義良好的邊界讓失效隔離成為可能,但每一個分散式系統都存在同樣的問題——網路、硬體或應用程式層面都有可能出現故障。因為服務之間存在依賴關係,所以任何一個元件出現了問題都會影響到元件依賴者。為了最小化區域性故障所帶來的影響,我們需要構建具有容錯能力的服務,可以優雅地應對某些型別的故障。

 

這篇文章基於RisingStack的Node.js諮詢和開發經驗,介紹構建高可用微服務系統的常用技術和架構模式。

 

如果你不熟悉這篇文章所介紹的模式,並不代表你現在所做的就是錯的,畢竟構建一個可靠的系統需要付出額外的代價。

 

微服務架構的風險

 

微服務架構將業務邏輯分散到了各個微服務當中,微服務間通過網路層進行通訊。網路通訊帶來了額外的延遲和複雜性,需要多個物理元件和邏輯元件共同協作。分散式系統的額外複雜性增加了出現網路故障的機率。

 

微服務架構相比單體架構最大的優勢之一在於,不同的團隊可以獨立地設計、開發和部署他們的服務。他們可以完全掌控自己的微服務生命週期。當然,這也意味著他們無法控制服務依賴項,因為依賴項的控制權掌握在其他團隊手中。在採用微服務架構時,我們要時刻銘記,釋出、配置等方面的問題可能會導致服務提供者出現短暫的不可用。

 

優雅的服務降級

 

通過微服務架構可以實現失效隔離,也就是說,在元件發生故障時可以實現優雅的服務降級。例如,在圖片共享應用發生故障時,使用者可能無法上傳新的圖片,但他們仍然可以瀏覽、編輯和分享已有的圖片。

 

images/fx6zEBWwNz6btCKz64BifrfTmwaHr6WZ.png

 

圖:理論上的微服務失效隔離

 

在大多數情況下,實現這種優雅的服務降級是很困難的,因為在分散式系統裡,應用之間相互依賴,為了應對臨時的故障,需要應用到一些失效備援方案(稍後會提到)。

 

images/j6RisGjmFFGd8FzZJSyyk3t3xh2ktCDx.png

 

圖:相互依賴的服務,在沒有失效備援方案的情況下就會全部失效。

 

變更管理

 

Google的網站可靠性團隊發現,70%的故障都是由系統變更引起的。更改服務、部署新程式碼、變更配置,這些都有可能引入新的缺陷或造成服務失效。

 

在微服務架構裡,服務之間是相互依賴的。所以我們要最小化出現故障的機率,限制故障所造成的負面影響。我們需要良好的變更管理策略和自動回滾機制。

 

例如,在部署新程式碼時,或者在對配置做出變更時,要先在一小部分服務例項上進行,然後監控它們,一旦發現關鍵性度量指標出現異常,馬上自動回滾。

 

images/X5zcEKxpB8wEsRyzQxX8hhZxk6AaGKE5.png

 

圖:變更管理——回退部署

 

另一個解決方案就是執行兩套生產環境。在部署的時候只部署到其中一個生產環境,只有在確認這個環境沒問題了之後才能將負載均衡器指向這個環境。這種部署方式被稱為藍綠部署或者紅黑部署。

 

回退程式碼並不是件壞事。你總不可能一邊把有問題的程式碼留在生產環境裡,一邊想著到底發生了什麼問題。所以,在必要的時候回退程式碼,越快越好。

 

健康監測和負載均衡

 

服務例項總是因為各種原因(故障、部署或自動伸縮)經歷著啟動、重啟、停止這樣的過程。這個過程會讓服務暫時或永久地不可用,為了避免出現問題,負載均衡器需要忽略出現問題的服務例項,因為它們已經不具備為使用者或其他子系統提供服務的能力。

 

應用的健康狀態可以通過外部的觀察來獲得,比如通過不斷重複地呼叫/health端點來得知應用的狀態,或者讓應用報告自己的狀態。服務發現機制會持續地收集服務例項的健康資訊,負載均衡器應該被配置成只將流量路由給健康的服務例項。

 

自愈

 

自愈能力能夠讓應用在發生故障時進行自我恢復。如果一個應用能夠通過一系列步驟從一個故障狀態中恢復,那麼就可以說它具備了自愈能力。在大多數情況下,這是通過一個外部系統來實現的。這個系統監控服務例項的健康狀態,如果服務長時間處於不健康狀態,系統就會將它重啟。在大多數時候自愈能力是很有用的,不過如果持續不斷的重啟應用也會造成一些問題。在應用超載或資料庫連接出現超時的時候通常會發生這種情況。

 

要實現高階的自愈方案會比較麻煩,比如在發生資料庫連線超時的情況下,你需要在應用程式裡新增額外的邏輯,讓外部系統知道此時不需要重啟服務例項。

 

失效備援快取

 

服務總會因各種原因發生失效,比如網路問題等。不過,大部分這樣的錯誤都是臨時性的,而系統的自愈能力和高階負載均衡特性可以讓應用例項在出現這些問題時仍然能夠提供服務能力。失效備援快取在這個時候就可以派上用場,它可以為應用程式提供必要的資料。

 

失效備援快取通常會使用兩個不同的過期時間,一個是短期時間,表示正常情況下的快取過期時間,另一個是長期時間,表示在發生故障期間的快取過期時間。

 

images/dZX4dDpDHmwZyGYpSCPsQRXpDDJdJzrB.png

 

圖:失效備援快取

 

不過需要注意的是,失效備援快取的資料可能是已經過期的資料,所以要確保這對於你的應用程式來說是可接受的。

 

可以通過HTTP的標準響應頭來設定快取或失效備援快取。例如,通過max-age指定資源的最長過期時間,通過stale-if-error指定在發生故障時快取的有效時間。

 

現代的CDN和負載均衡器提供了各種各樣的快取和失效備援機制,你也可以為自己的公司建立適合自己使用的快取解決方案。

 

重試

 

在某些情況下,我們無法快取資料,或者我們想要更新快取內容但更新失敗。在這個時候,我們可以進行重試,因為我們認為相關資源稍後會重新恢復過來,或者負載均衡器會將請求傳送給正常的例項。

 

在應用程式裡新增重試邏輯的時候要十分小心,因為大量的重試操作會讓事情變得更糟糕,甚至導致應用程式無法從故障中恢復。

 

在分散式系統中,一個微服務系統可能會觸發多個請求或重試操作,從而發生級聯效應。為了降低重試帶來的影響,應該要限制重試的次數,可以使用指數退避(exponential backoff)演算法來逐步增加重試之間的延遲,直到達到重試的上限。

 

重試是由客戶端(比如瀏覽器、其他微服務,等等)發起的,而客戶端並不會知道之前請求處理是否成功,所以在進行重試時要注意處理冪等性問題。例如,在重試一個購買操作時就不應該重複計費。可以為每個事務使用唯一的冪等性鍵,這樣有助於解決冪等性問題。

 

速率限定和負載倒注器(Shedder)

 

速率限定規定了應用程式在一個時間視窗內能夠接收或處理的請求個數。通過速率限定,你可以在流量高峰期過濾掉一些使用者請求或發出請求的微服務,確保你的應用程式不會出現過載。

 

你也可以因此限制低優先順序的流量,將更多的資源優先用在處理更關鍵的事務上。

 

images/FfCSBWAw6KfJKFCC7SYKB4MzxBfBFQMD.png

 

圖:速率限定器限制流量高峰

 

另一種速率限定器叫作併發請求限定器(concurrent request limiter),它在某些情況下會很有用。比如,你不希望某些端點被多次呼叫,但同時又想為所有的流量提供服務。

 

使用負載倒注器可以確保總是存在足夠的資源來處理關鍵性的事務。它為高優先順序的請求保留了一些資源,這些資源不會被用在低優先順序的事務上。負載倒注器基於整個系統的狀態來決定如何保留資源,而不是基於請求桶大小。負載倒注器有助於系統在發生故障時進行恢復,因為在發生故障的時候,它們仍能保證系統核心功能正常執行。Stripe的文章裡詳細介紹了速率限定器和負載倒注器。

 

快速而獨立地失效

 

在微服務架構裡,如果服務發生失效,我們就要讓它們快速而獨立發生。我們可以應用隔板(bulkhead)模式在服務層面對問題進行隔離。稍後會對隔板模式進行更多的介紹。

 

如果服務元件發生失效,那麼就要讓它儘快失效,因為我們不想浪費太多時間等它發生超時。沒有什麼比一個被掛起的請求和無響應的介面更讓人感到沮喪的了,它不僅浪費了資源,也給使用者體驗造成影響。在服務生態系統裡,服務之間相互呼叫,我們要防止掛起的請求操作造成雪崩效應。

 

你可能首先會想到為每個服務呼叫定義二級超時時間,但問題是,你無法確切地知道多長的超時時間才是最合適的,因為有時候網路故障等問題只會影響到一兩個操作。很顯然,如果是這種情況,那麼就不應該因為少數的請求過時就拒絕其他請求。

 

可以說,在微服務架構裡通過使用超時來實現快速失效機制是一種反模式,所以應該儘量避免這麼做。相反,我們可以使用迴路斷路器(circuit breaker)模式,它基於成功操作和失敗操作的統計結果來決定服務是否已經失效。

 

隔板

 

在造船業,隔板被用於將船隔成多個部分,如果船體發生洩漏,發生洩漏的部分就可以被單獨封閉。

 

隔板的概念也被應用在軟體開發裡,用於分隔資源。

 

通過應用隔板模式,我們可以防止有限的資源被耗盡。例如,如果我們有兩種針對資料庫的操作,我們可以使用連個連線池,而不是一個。這樣一來,如果有些操作超時或者過度使用了連線池,就不會對另一個連線池上的操作造成影響。

 

迴路斷路器

 

為了限定操作的時長,我們可以為操作定義超時時間。超時機制可以防止出現長時間的掛起操作,保證系統可以正常響應。不過,在微服務架構裡使用固定的超時機制是一種反模式,因為我們的環境是高度動態的,所以幾乎不可能為每一種情況定義正確的限定時間。

 

我們可以使用迴路斷路器,而不是使用固定的超時機制。迴路斷路器的名字源於真實世界的電子元件,因為它們的行為極其相似。它們可以保護資源,有助於進行系統恢復。在分散式系統裡,它們能夠起到很大的作用,特別是當重複性的故障造成雪崩效應進而威脅到整個系統的時候。

 

如果某種型別的錯誤在一定時間段內多次發生,迴路斷路器就會斷開。斷開的迴路斷路器會阻止後續的請求,就像電子元件裡的斷路器一樣。迴路斷路器會在一段時間之後關閉,給底層的服務更多的空間進行恢復。

 

要記住,並不是所有的錯誤都需要觸發迴路斷路器。例如,你可能想要忽略客戶端的一些問題,比如那些包含4xx響應碼的請求,但同時想要保留5xx的伺服器端錯誤。有些迴路斷路器會出現半開閉狀態。這個時候,服務會發送一個請求來檢測系統的可用性,同時拒絕其他的請求。如果檢測系統可用性的請求成功返回,那麼就會關閉斷路器,繼續處理後續的請求。否則的話,它就保持開啟狀態。

 

images/8z3AnBzi76QhJsaxAe6achQcptNaNnRe.png

 

圖:迴路斷路器

 

做好故障測試

 

我們應該持續不斷地針對各種常見問題對我們的系統進行測試,確保我們的服務能夠應對各種故障。

 

例如,我們可以使用一個外部服務來識別一組服務例項,然後隨機地終止其中的一個例項。這樣就知道該如何應對單個例項故障,當然,也可以關閉整組服務來模擬雲服務中斷。

 

Netflix的ChaosMonkey是一個非常受歡迎的彈性測試工具。

 

總結

 

實現和執行可靠的服務並不是一件簡單的事情。你需要付出很多的努力,你的公司也因此要付出很高的代價。

 

可靠性可以分為多種層次,也涉及到多個方面的內容,你要找到適合自己團隊的解決方案。你應該把可靠性作為業務決策的一個考量因素,併為它分配足夠的預算和時間。