高可用(二)

可用性的計算公式:

  1. %availability=(Total Elapsed Times-Sum of Inoperative Times)/ Total Elapsed Time  

可用性99.999%,意味着在一年365天,系統停止服務時間爲5分15秒

  1. total times= 365*24*36000=31536000
  2. inoperative times=31536000*0.001%=315.36

高可用方法論

擴展
擴展是最常見的提升系統可靠性的方法,系統的擴展可以避免單點故障,即一個節點出現了問題造成整個系統無法正常工作。換一個角度講,一個容易擴展的系統,能夠通過擴展來成倍的提升系統能力,輕鬆應對系統訪問量的提升。

一般地,擴展可以分爲垂直擴展和水平擴展:

垂直擴展:是在同一邏輯單元裏添加資源從而滿足系統處理能力上升的需求。比如,當機器內存不夠時,我們可以幫機器增加內存,或者數據存不下時,我們爲機器掛載新的磁盤。
垂直擴展能夠提升系統處理能力,但不能解決單點故障問題。
優點:擴展簡單。
缺點:擴展能力有限。
水平擴展:通過增加一個或多個邏輯單元,並使得它們像整體一樣的工作。
水平擴展,通過冗餘部署解決了單點故障,同時又提升了系統處理能力。
優點:擴展能力強。
缺點:增加系統複雜度,維護成本高,系統需要是無狀態的、可分佈式的。
可擴展性係數 scalability factor 通常用來衡量一個系統的擴展能力,當增加 1 單元的資源時,系統處理能力只增加了 0.95 單元,那麼可擴展性係數就是 95%。當系統在持續的擴展中,可擴展係數始終保持不變,我們就稱這種擴展是線性可擴展。

在實際應用中,水平擴展最常見:

通常我們在部署應用服務器的時候,都會部署多臺,然後使用 nginx 來做負載均衡,nginx 使用心跳機制來檢測服務器的正常與否,無響應的服務就從集羣中剔除。這樣的集羣中每臺服務器的角色是相同的,同時提供一樣的服務。
在數據庫的部署中,爲了防止單點故障,一般會使用一主多從,通常寫操作只發生在主庫。不同數據庫之間角色不同。當主機宕機時,一臺從庫可以自動切換爲主機提供服務。
隔離
隔離,是對什麼進行隔離呢?是對系統、業務所佔有的資源進行隔離,限制某個業務對資源的佔用數量,避免一個業務佔用整個系統資源,對其他業務造成影響。

隔離級別按粒度從小到大,可以分爲線程池隔離、進程隔離、模塊隔離、應用隔離、機房隔離。在數據庫的使用中,還經常用到讀寫分離。

線程池隔離:不同的業務使用不同的線程池,避免低優先級的任務阻塞高優先級的任務。或者高優先級的任務過多,導致低優先級任務永遠不會執行。
進程隔離:Linux 中有用於進程資源隔離的 Linux CGroup,通過物理限制的方式爲進程間資源控制提供了簡單的實現方式,爲 Linux Container 技術、虛擬化技術的發展奠定了技術基礎。在工作中的實際應用,可以看看這篇文章:日誌壓縮資源消耗優化: Linux CGroup 的使用。
模塊隔離、應用隔離:很多線上故障的發生源於代碼修改後,測試不到位導致。按照代碼或業務的易變程度來劃分模塊或應用,把變化較少的劃分到一個模塊或應用中,變化較多的劃分到另一個模塊或應用中。減少代碼修改影響的範圍,也就減少了測試的工作量,減少了故障出現的概率。
機房隔離:主要是爲了避免單個機房網絡問題或斷電吧。
讀寫分離:一方面,將對實時性要求不高的讀操作,放到 DB 從庫上執行,有利於減輕 DB 主庫的壓力。另一方面,將一些耗時離線業務 sql 放到 DB 從庫上執行,能夠減少慢 sql 對 DB 主庫的影響,保證線上業務的穩定可靠。
解耦
在軟件工程中,對象之間的耦合度就是對象之間的依賴性。對象之間的耦合越高,維護成本越高,因此對象的設計應使模塊之間的耦合度儘量小。在軟件架構設計中,模塊之間的解耦或者說鬆耦合有兩種,假設有兩個模塊A、B,A依賴B:

第一種是,模塊A和模塊B只通過接口交互,只要接口設計不變,那麼模塊B內部細節的變化不影響模塊A對模塊B服務能力的消費。
面向接口設計下真正實現了將接口契約的定義和接口的實現徹底分離,實現變化不影響到接口契約,自然不影響到基於接口的交互。
模塊A和B之間的鬆耦合,主要通過合理的模塊劃分、接口設計來完成。如果出現循環依賴,可以將模塊A、B共同依賴的部分移除到另一個模塊C中,將A、B之間的相互依賴,轉換爲A、B同時對C的依賴。
第二種是,將同步調用轉換成異步消息交互。
比如在買機票系統中,機票支付完成後需要通知出票系統出票、代金券系統發券。如果使用同步調用,那麼出票系統、代金券系統宕機是會影響到機票支付系統,如果另一個系統比如專車系統也想要在機票支付完成後向用戶推薦專車服務,那麼同步調用模式下機票支付系統就需要爲此而改動,容易影響核心支付業務的可靠性。
如果我們將同步調用替換成異步消息,機票支付系統發送機票支付成功的消息到消息中間件,出票系統、代金券系統從消息中間件訂閱消息。這樣一來,出票系統、代金券系統的宕機也就不會對機票支付系統造成任何影響了。專車系統想要知道機票支付完成這一事件,也只需要從消息中間件訂閱消息即可,機票支付系統完全不需要做任何改動。
異步消息解耦,適合那些信息流單向流動(類似發佈-訂閱這樣的),實時性要求不高的系統。常見的開源消息隊列框架有:Kafka、RabbitMQ、RocketMQ。
限流
爲什麼要做限流呢?舉一個生活中的例子,大家早上上班都要擠地鐵吧,地鐵站在早高峯的時候經常要限制客流,爲什麼呢?有人會覺得這是人爲添堵。真是這樣嗎?如果不執行客流控制,大家想想會是什麼場景呢?站臺到處都擠滿了乘客,就算你使出洪荒之力也不一定能順利上車,且非常容易引發肢體碰撞,造成衝突。有了客流控制之後,地鐵站才能變得秩序井然,大家才能安全上地鐵。

一個系統的處理能力是有上限的,當服務請求量超過處理能力,通常會引起排隊,造成響應時間迅速提升。如果對服務佔用的資源量沒有約束,還可能因爲系統資源佔用過多而宕機。因此,爲了保證系統在遭遇突發流量時,能夠正常運行,需要爲你的服務加上限流。

常見的限流算法有:漏桶、令牌桶、滑動窗口計數。

分類
按照計數範圍,可以分爲:單機限流、全侷限流。單機限流,一般是爲了應對突發流量,而全侷限流,通常是爲了給有限資源進行流量配額。

按照計數週期,可以分爲:QPS、併發(連接數)。

按照閾值設定方式的不同,可以分爲:固定閾值、動態閾值。

漏桶算法
下面這張圖,是漏桶的示意圖。漏桶算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以一定的速度出水,當水流入速度過大時,會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率。漏桶算法(Leaky Bucket)是網絡世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)時經常使用的一種算法,它的主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。

漏桶算法可以使用 Redis 隊列來實現,生產者發送消息前先檢查隊列長度是否超過閾值,超過閾值則丟棄消息,否則發送消息到 Redis 隊列中;消費者以固定速率從 Redis 隊列中取消息。Redis 隊列在這裏起到了一個緩衝池的作用,起到削峯填谷、流量整形的作用。

令牌桶算法
對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更爲適合。令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而如果請求需要被處理,則需要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。桶裏能夠存放令牌的最高數量,就是允許的突發傳輸量。

Guava 中的限流工具 RateLimiter,其原理就是令牌桶算法。

滑動窗口計數法
計數法是限流算法裏最容易理解的一種,該方法統計最近一段時間的請求量,如果超過一定的閾值,就開始限流。在 TCP 網絡協議中,也用到了滑動窗口來限制數據傳輸速率。

滑動窗口計數有兩個關鍵的因素:窗口時長、滾動時間間隔。滾動時間間隔一般等於上圖中的一個桶 bucket,窗口時長除以滾動時間間隔,就是一個窗口所包含的 bucket 數目。

滑動窗口計數算法的實現,可以查看這篇文章:降級熔斷框架 Hystrix 源碼解析:滑動窗口統計。

動態限流
一般情況下的限流,都需要我們手動設定限流閾值,不僅繁瑣,而且容易因系統的發佈升級而過時。爲此,我們考慮根據系統負載來動態決定是否限流,動態計算限流閾值。可以參考的系統負載參數有:Load、CPU、接口響應時間等。

詳細內容請看:基於系統負載的動態限流 dynamic-limiter。

降級
業務降級,是指犧牲非核心的業務功能,保證核心功能的穩定運行。簡單來說,要實現優雅的業務降級,需要將功能實現拆分到相對獨立的不同代碼單元,分優先級進行隔離。在後臺通過開關控制,降級部分非主流程的業務功能,減輕系統依賴和性能損耗,從而提升集羣的整體吞吐率。

降級的重點是:業務之間有優先級之分。降級的典型應用是:電商活動期間關閉非核心服務,保證核心買買買業務的正常運行。

業務降級通常需要通過開關工作,開關一般做成配置放在專門的配置系統,配置的修改最好能夠實時生效,畢竟要是還得修改代碼發佈那就太 low 了。開源的配置系統有阿里的diamond、攜程的Apollo、百度的disconf。

降級往往需要兜底方案的配合,比如系統不可用的時候,對用戶進行提示,安撫用戶。提示雖然不起眼,但是能夠有效的提升用戶體驗。

熔斷
談到熔斷,不得不提經典的電力系統中的保險絲,當負載過大,或者電路發生故障時,電流會不斷升高,爲防止升高的電流有可能損壞電路中的某些重要器件或貴重器件,燒燬電路甚至造成火災。保險絲會在電流異常升高到一定的高度和熱度的時候,自身熔斷切斷電流,從而起到保護電路安全運行的作用。

同樣,在分佈式系統中,如果調用的遠程服務或者資源由於某種原因無法使用時,沒有這種過載保護,就會導致請求阻塞在服務器上等待從而耗盡服務器資源。很多時候剛開始可能只是系統出現了局部的、小規模的故障,然而由於種種原因,故障影響的範圍越來越大,最終導致了全局性的後果。而這種過載保護就是大家俗稱的熔斷器(Circuit Breaker)。

下面這張圖,就是熔斷器的基本原理,包含三個狀態:

服務正常運行時的 Closed 狀態,當服務調用失敗量或失敗率達到閾值時,熔斷器進入 Open 狀態
在 Open 狀態,服務調用不會真正去請求外部資源,會快速失敗。
當進入 Open 狀態一段時間後,進入 Half-Open狀態,需要去嘗試調用幾次服務,檢查故障的服務是否恢復。如果成功則熔斷器關閉,如果失敗,則再次進入 Open 狀態。

目前比較流行的降級熔斷框架,是由 Netflix 開源的 Hystrix 框架。

發佈相關
模塊級自動化測試**
衆所周知,一個項目上線前需要經歷嚴格的測試過程,但是隨着業務不斷迭代、系統日益複雜,研發工程師、產品經理、測試工程師等都在測試過程中投入了大量精力,而一個個線上故障卻表明測試效果並不是那麼完美。究其原因,目前的測試工作主要存在兩方面問題:

測試範圍難以界定:隨着業務邏輯的不斷迭代、系統的不斷拆分與細化,精確評估項目改動的影響範圍變得越來越困難,從而很難梳理出覆蓋全面的測試點。
case驗證成本過高:驗證一個case需要構造測試場景,包括數據的準備和運行環境的準備,當case量較大或者存在一些涉及多個系統模塊且觸發條件複雜的case時,這一過程也將花費大量的時間。
解決上述問題可以使用模塊級自動化測試。具體方案是:針對某一模塊,收集模塊線上的輸入、輸出、運行時環境等信息,在離線測試環境通過數據mock模塊線上場景,回放收集的線上輸入,相同的輸入比較測試場景與線上收集的輸出作爲測試結果。

模塊級自動化測試通過簡化複雜系統中的不變因素(mock),將系統的測試邊界收攏到改動模塊,將複雜系統的整體測試轉化爲改動模塊的單元測試。主要適用於系統業務迴歸,對系統內部重構場景尤其適用。

具體如何收集線上數據呢?有兩種方法:

AOP:面向切面編程,動態地織入代碼,對原有代碼的侵入性較小。
埋點:很多公司都開發了一下基礎組件,可以在這些基礎組件中嵌入數據收集的代碼。
更多細節,可以查看下面參考文獻中的文章:Qunar 自動化測試框架 ARES。

灰度發佈 & 回滾
單點和發佈是系統高可用最大的敵人。一般在線上出現故障後,第一個要考慮的就是剛剛有沒有代碼發佈、配置發佈,如果有的話就先回滾。線上故障最重要的是快速恢復,如果等你細細看代碼找到問題,沒準兒半天就過去了。

爲了減少發佈引起問題的嚴重程度,通常會使用灰度發佈策略。灰度發佈是速度與安全性作爲妥協。他是發佈衆多保險的最後一道,而不是唯一的一道。在這篇文章來自 Google 的高可用架構理念與實踐裏提到:

做灰度發佈,如果是勻速的,說明沒有理解灰度發佈的意義。一般來說階段選擇上從 1% -> 10% -> 100% 的指數型增長。這個階段,是根據具體業務不同按維度去細分的。
這裏面的重點在於 1% 並不全是隨機選擇的,而是根據業務特點、數據特點選擇的一批有極強的代表性的實例,去做灰度發佈的小白鼠。甚至於每次發佈的 第一階段用戶(我們叫 Canary/金絲雀),根據每次發佈的特點不同,是人爲挑選的。

發佈之前必須制定詳細的回滾步驟,回滾是解決發佈引起的故障的最快的方法。

故障演練
爲什麼要做故障演練呢?就跟在測試業務功能時,不僅要測試正常的請求能否正確處理,也要測試異常的請求能否得到適當的處理一樣。站在全局的角度看,我們也希望保證某個機器或某個服務掛掉時,儘量不影響系統整體的可用性,技術上要靠無狀態服務、冗餘部署、降級等。實際中如何測試這樣的異常情況呢?

Netflix 開源了一個工具 Chaos Monkey,這是一套用來故意把服務器搞下線的軟件,可以用來測試系統的健壯性和恢復能力。

自動化運維-故障自愈
在文章阿里如何做到百萬量級硬件故障自愈裏,介紹瞭如何實現硬件故障預測、服務器自動下線、服務自愈以及集羣的自平衡重建,真正在影響業務之前實現硬件故障自動閉環策略,對於常見的硬件故障無需人工干預即可自動閉環解決。

事件系統
AWS 有一個 CloudTrail 系統,專門記錄重大活動事件,可以簡化安全性分析、資源更改跟蹤和問題排查工作。系統發佈、配置變更是引發故障的一大因素,微服務化的系統架構裏,有時某個底層系統的變更,引起反映、出現故障的往往是上層直接面對用戶的系統。有了事件系統,出現故障後,可以快速查看在故障時間點,相關聯繫統是否有變更,是否是引起故障的根本原因?

事件系統的出現,可以幫助應用開發者快速定位「底層系統變更引發上層系統異常」這一類故障的根本原因。

其他**
設置超時:請求對外接口的時候,需要設置合理的超時時間,避免外部接口掛掉時,阻塞整個系統。
失敗重試:失敗重試能夠提高成功率,但是也會造成響應時間變慢,服務提供方壓力倍增。具體要不要重試要根據具體情況決定:對響應時間有要求嗎?接口失敗率如何?重試會不會造成雪崩?

總結