第一章:可靠性、可伸縮性和可維護性
現在多數系統都是資料密集型(data-intensive)的,而非計算密集型(compute-intensive)
資料密集型應用通常由標準組件構建而成,標準組件提供了很多通用的功能;例如,許多應用程式都需要:
- 儲存資料,以便自己或其他應用程式之後能再次找到 (資料庫,即 databases)
- 記住開銷昂貴操作的結果,加快讀取速度(快取,即 caches)
- 允許使用者按關鍵字搜尋資料,或以各種方式對資料進行過濾(搜尋索引,即 search indexes)
- 向其他程序傳送訊息,進行非同步處理(流處理,即 stream processing)
- 定期處理累積的大批次資料(批處理,即 batch processing)
關於資料系統的思考
近些年來,出現了許多新的資料儲存工具與資料處理工具。它們針對不同應用場景進行最佳化,因此不再適合生硬地歸入傳統類別【1】。類別之間的界限變得越來越模糊,例如:資料儲存可以被當成訊息佇列用(Redis),訊息佇列則帶有類似資料庫的持久保證(Apache Kafka)。
其次,越來越多的應用程式有著各種嚴格而廣泛的要求,單個工具不足以滿足所有的資料處理和儲存需求。取而代之的是,總體工作被拆分成一系列能被單個工具高效完成的任務,並透過應用程式碼將它們縫合起來。
例如,如果將快取(應用管理的快取層,Memcached 或同類產品)和全文搜尋(全文搜尋伺服器,例如 Elasticsearch 或 Solr)功能從主資料庫剝離出來,那麼使快取 / 索引與主資料庫保持同步通常是應用程式碼的責任。圖 1-1給出了這種架構可能的樣子(細節將在後面的章節中詳細介紹)。
可靠性
人們對可靠軟體的典型期望包括:
- 應用程式表現出使用者所期望的功能。
- 允許使用者犯錯,允許使用者以出乎意料的方式使用軟體。
- 在預期的負載和資料量下,效能滿足要求。
- 系統能防止未經授權的訪問和濫用。
如果所有這些在一起意味著 “正確工作”,那麼可以把可靠性粗略理解為 “即使出現問題,也能繼續正確工作”。
- 造成錯誤的原因叫做 故障(fault)
- **故障(fault)**不同於 失效(failure) - 故障為部分狀態偏離標準,失效則為整個系統掛掉,因此最好設計容錯機制,以防因故障而失效。
- 我們可以透過故意引發故障來確保容錯機制不斷執行並接受考驗,從而提高故障自然發生時系統能正確處理的信心。範例:Netflix 公司的 Chaos Monkey【4】
- 能預料並應對故障的系統特性可稱為 **容錯(fault-tolerant)**或 韌性(resilient)
硬體故障**(hardware faults)**
- 為了減少系統的故障率,第一反應通常都是增加單個硬體的冗餘度,例如:磁碟可以組建 RAID,伺服器可能有雙路電源和熱插拔 CPU,資料中心可能有電池和柴油發電機作為後備電源,某個元件掛掉時冗餘元件可以立刻接管。這種方法雖然不能完全防止由硬體問題導致的系統失效,但它簡單易懂,通常也足以讓機器不間斷執行很多年。
- **但是隨著資料量和應用計算需求的增加,越來越多的應用開始大量使用機器,這會相應地增加硬體故障率。**此外,在類似亞馬遜 AWS的一些雲服務平臺上,虛擬機器例項不可用卻沒有任何警告也是很常見的,因為雲平臺的設計就是優先考慮 靈活性(flexibility) 和 彈性(elasticity),而不是單機可靠性。
- 如果在硬體冗餘的基礎上進一步引入軟體容錯機制,那麼系統在容忍整個(單臺)機器故障的道路上就更進一步了。這樣的系統也有運維上的便利,例如:如果需要重啟機器(例如應用作業系統安全補丁),單伺服器系統就需要計劃停機。而允許機器失效的系統則可以一次修復一個節點,無需整個系統停機。
軟體錯誤
是內部的系統性錯誤(systematic error),例子包括:
- 接受特定的錯誤輸入,便導致所有應用伺服器例項崩潰的 BUG。例如 2012 年 6 月 30 日的閏秒,由於 Linux 核心中的一個錯誤,許多應用同時掛掉了。
- 失控程序會用盡一些共享資源,包括 CPU 時間、記憶體、磁碟空間或網路頻寬。
- 系統依賴的服務變慢,沒有響應,或者開始返回錯誤的響應。
- 級聯故障,一個元件中的小故障觸發另一個元件中的故障,進而觸發更多的故障。
雖然軟體中的系統性故障沒有速效藥,但我們還是有很多小辦法,例如:
- 仔細考慮系統中的假設和互動
- 徹底的測試
- 程序隔離
- 允許程序崩潰並重啟
- 測量、監控並分析生產環境中的系統行為
如果系統能夠提供一些保證(例如在一個訊息佇列中,進入與發出的訊息數量相等),那麼系統就可以在執行時不斷自檢,並在出現 **差異(discrepancy)**時警報
人為錯誤
人為錯誤是導致服務中斷的首要原因,但還是有些方法可以增加系統可靠度:
- 以最小犯錯機會的方式設計系統,讓做對事情更容易,讓做錯事情更困難。
- 將人們最容易犯錯的地方與可能導致失效的地方 解耦(decouple)。特別是提供一個功能齊全的非生產環境沙箱(sandbox),使人們可以在不影響真實使用者的情況下,使用真實資料安全地探索和實驗。
- 在各個層次進行徹底的測試,從單元測試、全系統整合測試到手動測試。自動化測試特別適合用來覆蓋正常情況中少見的邊緣場景(corner case)。
- 允許從人為錯誤中簡單快速地恢復,以最大限度地減少失效情況帶來的影響。 例如,快速回滾配置變更,分批發布新程式碼(以便任何意外錯誤只影響一小部分使用者),並提供資料重算工具(以備舊的計算出錯)。
- 配置詳細和明確的監控,比如效能指標和錯誤率。 在其他工程學科中這指的是 遙測(telemetry)(一旦火箭離開了地面,遙測技術對於跟蹤發生的事情和理解失敗是至關重要的)。監控可以向我們發出預警訊號,並允許我們檢查是否有任何地方違反了假設和約束。當出現問題時,指標資料對於問題診斷是非常寶貴的。
- 良好的管理實踐與充分的培訓 —— 一個複雜而重要的方面,但超出了本書的範圍。
可伸縮性
**可伸縮性(Scalability)**是用來描述系統應對負載增長能力的術語。
服務**降級(degradation)**的一個常見原因是負載增加
描述負載
負載可以用一些稱為 **負載引數(load parameters)**的數字來描述。引數的最佳選擇取決於系統架構,它可能是每秒向 Web 伺服器發出的請求、資料庫中的讀寫比率、聊天室中同時活躍的使用者數量、快取命中率或其他東西。除此之外,也許平均情況對你很重要,也許你的瓶頸是少數極端場景。
描述效能
當負載增加時會發生什麼,可以從兩個角度來看
- 增加負載引數並保持系統資源(CPU、記憶體、網路頻寬等)不變時,系統性能將受到什麼影響?
- 增加負載引數並希望保持效能不變時,需要增加多少系統資源?
對於 Hadoop 這樣的批處理系統,通常關心的是 吞吐量(throughput),即每秒可以處理的記錄數量,或者在特定規模資料集上執行作業的總時間。
對於線上系統,通常更重要的是服務的 響應時間(response time),即客戶端傳送請求到接收響應之間的時間。
就算是相同需求,響應時間也會略有不同,因此需要將響應時間視為一個可以測量的數值 分佈(distribution),而不是單個數值。
如果想知道**典型(typical)**響應時間,使用中位數會比平均值更好。為了弄清楚異常值有多糟糕,可以看更高的百分位點,例如95%、99%、99.9%。
響應時間的高百分位點(也稱為 尾部延遲,即 tail latencies)非常重要,因為它們直接影響使用者的服務體驗。這是因為請求響應最慢的客戶往往也是資料最多的客戶,也可以說是最有價值的客戶 —— 因為他們掏錢了
另一方面,最佳化第 99.99 百分位點(一萬個請求中最慢的一個)被認為太昂貴了,不能為亞馬遜的目標帶來足夠好處。減小高百分位點處的響應時間相當困難,因為它很容易受到隨機事件的影響,這超出了控制範圍,而且效益也很小。
百分位點通常用於 服務級別目標(SLO, service level objectives) 和 服務級別協議(SLA, service level agreements),即定義服務預期效能和可用性的合約。 SLA 可能會宣告,如果服務響應時間的中位數小於 200 毫秒,且 99.9 百分位點低於 1 秒,則認為服務工作正常(如果響應時間更長,就認為服務不達標)。這些指標為客戶設定了期望值,並允許客戶在 SLA 未達標的情況下要求退款。
排隊延遲(queueing delay) 通常佔了高百分位點處響應時間的很大一部分。由於伺服器只能並行處理少量的事務(如受其 CPU 核數的限制),所以只要有少量緩慢的請求就能阻礙後續請求的處理,這種效應有時被稱為 頭部阻塞(head-of-line blocking) 。
應對負載的方法
- 縱向伸縮(scaling up,也稱為垂直伸縮,即 vertical scaling,轉向更強大的機器)
- 橫向伸縮(scaling out,也稱為水平伸縮,即 horizontal scaling,將負載分佈到多臺小機器上)
跨多臺機器分配負載也稱為 “無共享(shared-nothing)” 架構。可以在單臺機器上執行的系統通常更簡單,但高階機器可能非常貴,所以非常密集的負載通常無法避免地需要橫向伸縮。現實世界中的優秀架構需要將這兩種方法務實地結合,因為使用幾臺足夠強大的機器可能比使用大量的小型虛擬機器更簡單也更便宜。
- 有些系統是 彈性(elastic)的,這意味著可以在檢測到負載增加時自動增加計算資源
- 而其他系統則是手動伸縮(人工分析容量並決定向系統新增更多的機器)
如果負載 極難預測(highly unpredictable),則彈性系統可能很有用,但手動伸縮系統更簡單,並且意外操作可能會更少(請參閱 “分割槽再平衡”)。
跨多臺機器部署 無狀態服務(stateless services) 非常簡單,但將帶狀態的資料系統從單節點變為分散式配置則可能引入許多額外複雜度。出於這個原因,常識告訴我們應該將資料庫放在單個節點上(縱向伸縮),直到伸縮成本或可用性需求迫使其改為分散式。
一個良好適配應用的可伸縮架構,是圍繞著 假設(assumption) 建立的:哪些操作是常見的?哪些操作是罕見的?這就是所謂負載引數。如果假設最終是錯誤的,那麼為伸縮所做的工程投入就白費了,最糟糕的是適得其反。在早期創業公司或非正式產品中,通常支援產品快速迭代的能力,要比可伸縮至未來的假想負載要重要的多。
可維護性
避免自己的系統變成遺留(legacy) 系統的三個設計原則:
一、可操作性(Operability)
便於運維團隊保持系統平穩執行。
一個優秀運維團隊的典型職責如下(或者更多):
- 監控系統的執行狀況,並在服務狀態不佳時快速恢復服務。
- 跟蹤問題的原因,例如系統故障或效能下降。
- 及時更新軟體和平臺,比如安全補丁。
- 瞭解系統間的相互作用,以便在異常變更造成損失前進行規避。
- 預測未來的問題,並在問題出現之前加以解決(例如,容量規劃)。
- 建立部署、配置、管理方面的良好實踐,編寫相應工具。
- 執行複雜的維護任務,例如將應用程式從一個平臺遷移到另一個平臺。
- 當配置變更時,維持系統的安全性。
- 定義工作流程,使運維操作可預測,並保持生產環境穩定。
- 鐵打的營盤流水的兵,維持組織對系統的瞭解。
資料系統可以透過各種方式使日常任務更輕鬆:
- 透過良好的監控,提供對系統內部狀態和執行時行為的 可見性(visibility)。
- 為自動化提供良好支援,將系統與標準化工具相整合。
- 避免依賴單臺機器(在整個系統繼續不間斷執行的情況下允許機器停機維護)。
- 提供良好的文件和易於理解的操作模型(“如果做 X,會發生 Y”)。
- 提供良好的預設行為,但需要時也允許管理員自由覆蓋預設值。
- 有條件時進行自我修復,但需要時也允許管理員手動控制系統狀態。
- 行為可預測,最大限度減少意外。
二、簡單性(Simplicity)
從系統中消除儘可能多的 複雜度(complexity),使新工程師也能輕鬆理解系統(注意這和使用者介面的簡單性不一樣)。
一個陷入複雜泥潭的軟體專案有時被描述為 爛泥潭(a big ball of mud)
**複雜度(complexity)**有各種可能的症狀,例如:狀態空間激增、模組間緊密耦合、糾結的依賴關係、不一致的命名和術語、解決效能問題的 Hack、需要繞開的特例等等。
簡化系統並不一定意味著減少功能;它也可以意味著消除 額外的(accidental) 的複雜度。 Moseley 和 Marks把 額外複雜度 定義為:由具體實現中湧現,而非(從使用者視角看,系統所解決的)問題本身固有的複雜度。消除額外複雜度 的最好工具之一是 抽象(abstraction)
三、可演化性(evolvability)
使工程師在未來能輕鬆地對系統進行更改,當需求變化時為新應用場景做適配。也稱為 可擴充套件性(extensibility)、**可修改性(modifiability)**或 可塑性(plasticity)。
敏捷(agile) 工作模式為適應變化提供了一個框架。敏捷社群還開發了對在頻繁變化的環境中開發軟體很有幫助的技術工具和模式,如 **測試驅動開發(TDD, test-driven development)**和 重構(refactoring)。
修改資料系統並使其適應不斷變化需求的容易程度,是與 簡單性 和 抽象性 密切相關的:簡單易懂的系統通常比複雜系統更容易修改。但由於這是一個非常重要的概念,我們將用一個不同的詞來指代資料系統層面的敏捷性: 可演化性(evolvability)