可靠性
- 程序按照預期運行
- 有一定容錯能力(用戶使用方式錯誤)
- 在預期的負載和數據量下,系統性能足夠好
- 預防未授權訪問和濫用
錯誤 != 失敗,我們只能保證容忍某些特定錯誤,提供容錯性來盡量避免系統fail
可以特意觸發某種錯誤來檢測我們系統的容錯和error handling的能力(Netflix Chaos Monkey)
盡管我們更傾向于提高容錯性而不是完全避免錯誤,如果涉及到安全性相關的場景,需要做到完全防范(錯誤很可能無法挽救)
硬件錯誤
硬件冗余是最先想到的解決方案
- 硬盤Raid
- 后備電源
- 熱切換CPU
- 數據中心冗余
down機后通過啟動備份機器來保證可用性,對于現在的云服務,可以對不同單個機器進行有計劃的downtime維護來確保系統可用性。
軟件錯誤
硬件之間的錯誤一般沒有關聯性,一臺機器損壞不會導致另一臺機器損壞,大規模硬件同時損壞的概率很小(除了地震之類)。
但是系統性的錯誤就很難預測,可能會涉及不同的節點或者系統模塊,更容易導致系統fail,比如:
- 某些輸入可能引發軟件bug導致系統崩潰,例如Linux 內核的leap second bug
- 失控進程耗盡共享資源- CPU時間片,內存,硬盤或者帶寬
- 系統依賴的服務變的緩慢,無響應,或者返回不正確的響應
- failure升級,某個模塊中的小錯誤引發其他模塊錯誤,引起雪崩
這種錯誤可能長期休眠,在某個時刻突然爆發,只能通過一些方法減少這種錯誤:
- 仔細思考系統的交互和各種可能性
- 徹底測試
- 進程隔離
- 允許進程崩潰和重啟
- 測量,監控和分析production的系統行為
人為錯誤
人都會犯錯,很多時候系統錯誤是由于不正確的配置造成的,怎樣在無法避免人為錯誤的同事盡量提高系統的可靠性?有下面幾個方案:
- 設計系統的時候盡量減少犯錯機會,比如精心設計的抽象,API和管理界面可。但是如果接口有太多限制,人們就會想辦法繞過他們,所以需要在靈活性和限制性做一個平衡
- 將人們犯錯最多的地方和實際可能導致失敗進行解耦,提供完整功能的測試環境,并且使用真實數據讓人們來進行測試
- 進行全方位的測試,包括單元測試,集成測試和手動測試。自動測試也應該廣泛推廣,特別是一些正常情況下涉及不到的極端情況
- 允許簡單快速恢復來降低人為因素帶來的failure,比如快速回滾配置更改,逐步部署新代碼,并且提供工具來重新計算數據
- 設置詳細和清晰的監控,例如性能指標和錯誤率,這被稱為遙測,來追蹤發生的事情,更好理解故障。監控可以發出早期預警,并且讓我們去檢查是否有任何違規行為。
- 實施良好的管理實踐和培訓(復雜,這里不討論)
擴展性
系統現在的可靠不代表將來一定可靠,最常見的場景是用戶或者業務增長帶來的負載增長。擴展性不是一個簡單的概念,說某個系統可擴展而某個不可以,它用來描述系統應對負載上升的能力。我們通常需要考慮“如果系統以某種特定方式增長,我們的應對方案是什么?”
描述負載
負載可以用負載參數來描述,怎樣選取取決于系統的架構,舉幾個例子:
- web server的每秒請求數(QPS)
- 數據庫中讀寫比
- 同時活躍的用戶數量
- Cache的命中率
以2012 Twitter公布數據舉例, Twitter用戶的主要兩個操作為:
- 發推: 用戶可以向follower發布消息,平均4.6k/s,峰值12k/s
- 主頁時間線: 用戶可以瀏覽follow的人的消息,300k/s
僅僅處理12k/s的寫入并不難, Twitter的難點在于每個人有多個follower并且可以follow多個人,這個挑戰的解決方案有兩種:
- 發推只需要簡單把新tweet插入一個全局tweet集合中,當用戶請求首頁時間線時,查找所有他們follow的人,找到對應的tweet,并且按時間順序合并,在如下圖一樣的關系型數據庫中,可以編寫如下query:
SELECT tweets.*, users.* FROM tweets
JOIN users ON tweets.sender_id = users.id
JOIN follows ON follows.followee_id = users.id
WHERE follows.follower_id = current_user
-
為每個用戶的首頁時間線維護一個緩存,就像一個針對每個收件人的推文郵箱。當用戶發布tweet的時候,去查看follow這個用戶的所有用戶,在他們的cache里面插入這條消息。這個時候對首頁時間線的請求就非常"便宜",因為這個結果是提前計算好的。
image
Twitter最開始采用了方法1, 但是query的數量增加很痛苦,后來切換到了方法2,因為發布tweet的平均要比讀取首頁時間線的低兩個數量級。
方法2的缺點是發布一個tweet需要很多額外的工作。平均而言,一條tweet會發送給75個關注者,因此每秒4.6k的tweet會變成345k次對cache的寫入。但是這個平均值忽略了用戶關注者數量差異巨大這個事實,某些用戶擁有超過3000萬關注者,這樣就意味著一條tweet可能導致超過3000萬次對cache的寫入。
在Twitter的例子中,每個用戶關注者的分布是一個關于可擴展性的關鍵負載參數。
最終Twitter采用兩種方式的混合,大部分人還是采用cache的方式,對于名人則會單獨進行提取并且在刷新首頁時間線的時候進行合并。
描述性能
當負載增加的時候,我們可以從以下兩個方面查看性能:
- 負載參數增加但是系統資源(CPU,內存,帶寬等等)不變的情況下,系統性能如何?
- 負載參數增加的情況下,增加多少資源才能使系統性能不變
這兩個問題都需要對性能做一個量化,怎么去描述系統性能?
在一個批處理系統(比如Hadoop)中,我們通常關注吞吐量(throughput) - 每秒處理數據的數量, 或者說運行一個特定大小的數據集消耗的時間。
在一個web系統中,我們通常更關心系統的響應時間。如果系統處理多次請求,響應時間或許相差很大,我們需要專注響應的分布而不是單次時間。
響應時間的差異可能有多方面的原因(即使理論上時間應該差不多):
- 后臺處理中的上下文切換
- 丟包和TCP重傳
- GC 停頓
- 頁或者緩存錯誤需要從新從硬盤讀數據
- 機器架子的機械振動
我們常會看到平均響應時間(sum(time)/n), 但是這種方式并不是非常好,你無法知道多少用戶的延遲大概是這個數字。
一般來說采取百分比會更好一點,拿到所有的數據按照時間排序,取得其中的中位數,比如是200ms,這意味著半數用戶延遲超過了200ms,也意味著我們預期的響應時間是200ms。
如果想要找到異常值有多差,我們可以取更高的百分比,95%,99%,99.9%是常用的指標,比如95%的響應時間是1.5s, 意味著100個里面有5個用戶響應時間超過1.5s。
高百分比的(尾部延遲)響應時間十分重要,它直接影響用戶體驗,請求速度慢有可能是因為請求數據多,也就是說有可能是最有價值的用戶, Amazon可能會看到99.9%。
另一方面來說,過高百分比的優化可能會非常昂貴,需要進行收益和開銷的權衡。
百分比經常被用于服務等級目標(SLO)和服務等級協議(SLA),來定義服務預期的性能和可用性。比如SLA可以定義如下:
- 中位數響應時間小于200ms
- 99%響應時間小于1s
- 服務需要在99.9%時間內可用
- 如果滿足不了以上條件可以退錢...
(SLA = SLO + 后果, 可以簡單這么理解)
排隊延遲經常是響應時間非常慢的原因,服務器只能并行處理少量請求(比如收到CPU核心數量限制),因此只需要少量幾個緩慢的請求就可以阻塞后面的請求,這被稱為隊頭阻塞(head-of-line block),盡管請求被真正處理的時間很短,客戶端的響應時間也會很長,所以我們需要在客戶端進行測量響應時間。
應對負載
對于當前負載表現良好的系統不見得在10倍負載時還會表現良好,如果設計一個可能快速增長的服務,需要考慮不同維度的負載的增長。
人們常常討論垂直擴展和水平擴展,在多臺機器上分布負載也被稱為無共享架構。在做決定的時候也要綜合考慮,使用一臺或者幾臺強大的機器仍然可能要比使用很多性能很差的機器更為簡單和便宜。
一些系統具有彈性,可以根據負載自動添加計算資源,而有一些系統則需要手動水平擴展。在負載難以預測的情況下,彈性系統更好,但是人工擴展更為簡單并且更不容易出現意外。
在分布式系統中無狀態服務非常簡單直接,而有狀態服務會非常繁瑣。
分布式系統中需要應對的問題要依據場景來定,不同的系統需要應對的情況差別很大,比如:
- 大量數據讀
- 大量數據寫
- 大量數據存儲
- 數據復雜程度
- 響應時間需求
- 訪問模式
- 以上等問題的綜合
比如一個系統設計為可以每秒處理100,000個請求,每個請求都有1kb, 那么他和另外一個每分鐘處理3個請求,每個請求有2Gb完全不同, 盡管他們的數據輸入大小一致。
系統的水平擴展設計基于負載參數的假設,如果假設是錯誤的,最好情況是做無用功,最差情況是起到反作用。因此在開發初期,快速實現產品功能比對未來負載擴展的設計更為重要。
可維護性
大多數情況下,軟件的維護成本要高于開發成本:
- 修復bug
- 保持系統的運行
- 調查故障
- 適配到新的平臺
- 技術償債
- 現有功能修改
- 新功能開發
大部分人不喜歡老系統的維護工作,我們在設計系統的時候應該盡量減少維護時的痛苦,防止創造出所謂的"老系統",有以下3個設計準則:
- 可操控 - 使運維容易維護系統的正常運轉
- 簡單 - 讓新人容易理解這個系統,去除不必要的復雜度
- 可進化 - 能夠輕松對系統進行更改,根據需求變化進行調整。也被稱為可擴展性(extensibility), 可修改性(modifiability)或者可塑性(plasticity)
可操控: 讓運維更輕松
一些方面的運維工作可以被自動化,但是它還是需要很多人為干涉,好的運維有以下職責:
- 監控系統健康,在變差的情況下能夠快速恢復服務
- 記錄問題原因,比如系統故障或者性能變差
- 保證軟件和平臺與時俱進,包括安全模塊
- 理解并監控不同系統間的相互影響,在有問題的更改造成影響之前發現
- 預測未來的問題并且進行預防(比如 容量估算)
- 對于配置和部署有著豐富經驗和成熟的工具
- 能夠執行復雜的運維任務,比如將應用進行跨平臺移動
- 配置更改后確保系統的安全性
- 定義流程使得操作可預測并且保持production環境穩定
- 在人員流失的情況下保證公司對系統的了解
良好的可操作性可以讓日常工作變的簡單,讓團隊可以集中精力處理高價值的任務, 數據系統可以做很多事情:
- 提供可視化的系統運行行為監控
- 提供標準化工具進行自動化集成
- 避免對單個機器的依賴(允許機器單獨down機維護并且不影響業務)
- 提供完善的文檔和運維模式(如果做X, 會產生Y結果)
- 提供良好的默認行為,但是允許管理員在需要時覆蓋這些行為
- 可能的話提供自我恢復功能,但是允許管理員在需要的時候人為控制系統狀態
- 盡量保證行為可預測,減少"驚喜"
簡單化: 管理復雜度
小型項目可以有很簡單并且清晰的代碼,項目變大的時候就會變得十分復雜,很難理解,增加了維護成本。復雜度可能來自多個方面:
- 狀態空間爆炸
- 模塊間緊耦合
- 復雜的依賴掛你
- 不一致的命名和屬于
- 某些魔法操作
減少復雜度最好的工具之一是抽象,一個好的抽象可以用一個簡單清晰容易理解的外觀(facade)隱藏大量背后的實現,也可以廣泛用于不同的應用中。這不僅比重新實現相似的功能多次更為高效,而且也會使代碼質量變高,在抽象組件的改進可以使所有使用這個抽象的應用受益。
比如高級語言隱藏了機器語言,CPU寄存器,系統調用。SQL隱藏了復雜的磁盤和內存中的數據結構,客戶的并發請求以及崩潰后的恢復。
找到好的抽象是很難的,在分布式系統中我們也會把一些算法包裝到一些抽象中使得系統復雜度可控。
可進化: 改變更簡單
系統需求一直處于不斷變化中,敏捷開發提供了一個適應變化的框架,還有其涉及的一些工具和模式比如TDD和重構。
敏捷的討論大部分集中在小規模系統中,本書會探討怎樣在大的系統中提高敏捷度,可能包括不同的應用或者應用有著不同的特性,比如怎樣去重構Twitter組裝首頁時間線的架構。
可進化的系統通常和簡單性和抽象性密切相關,簡單和容易理解的系統修改起來更為輕松,我們用可進化性來代表系統的敏捷級別。