文章目錄
- 一. 數據密集型程序的特點以及遇到的問題
- 二. 可靠性 : 即使出現問題,也能繼續正確工作
- 1 硬件故障
- 2. 軟件錯誤
- 3. 人為錯誤
- 二. 可伸縮性
- 1. 描述負載與推特的例子
- 2. 描述性能-延遲和響應時間
- 3. 應對負載的方法
- 四. 可維護性
- 1. 可操作性:人生苦短,關愛運維
- 2. 簡單性:管理復雜度
- 3. 可演化性:擁抱變化
本文討論了軟件系統的可靠性,可伸縮性和可維護性。
可靠性(Reliability) 指高可用:及時發生故障(人為、bug、硬件服務)也能提供服務。一般高可用通過主備的概念去實現;
可伸縮性(Scalability) 負載增加的情況下
也有保持性能的策略
,一般進行橫向拓展;可維護性(Maintainability):這里強調抽象降低復雜度,并易于修改和適應新的應用場景。
?
一. 數據密集型程序的特點以及遇到的問題
現今很多應用程序都是 數據密集型(data-intensive) 的,而非 計算密集型(compute-intensive) 的。因此 CPU 很少成為這類應用的瓶頸,更大的問題(是內存)通常來自數據量、數據復雜性、以及數據的變更速度。
?
標準組件提供了應用的能力
數據密集型應用通常由標準組件構建而成,標準組件提供了很多通用的功能。例如,許多應用程序都需要:
存儲數據
,以便自己或其他應用程序之后能再次找到 (數據庫,即 databases)- 加快讀取速度(緩存,即 caches)
- 允許用戶按關鍵字搜索數據,或以各種方式對數據進行過濾(搜索索引,即 search indexes)
- 向其他進程發送消息,進行異步處理(流處理,即 stream processing)
- 定期處理累積的大批量數據(批處理,即 batch processing)
?
使用多個組件的數據系統架構
應用代碼將不同功能的工具縫合起來來一起服務,服務的接口想客戶端隱藏這些實現細節。這個復合系統會有特定的保證:例如:緩存在寫入時會作廢或更新,以便外部客戶端獲取一致的結果。
應用架構的樣子:
?
設計數據系統或服務時會遇到如下通用的問題:
- 當系統出問題時,如何確保數據的正確性和完整性?
- 當部分系統退化降級時,如何為客戶提供始終如一的良好性能?
- 當負載增加時,如何擴容應對?
- 什么樣的 API 才是好的 API?
?
這里我們討論大多數系統中遇到的共性問題:
可靠性(Reliability):
可伸縮性(Scalability):面對數據、流量導致的內存增長,復雜性導致的可伸縮性變差有合理的辦法應對系統的增長(數據量、流量、復雜性)。
可維護性(Maintainability):
?
二. 可靠性 : 即使出現問題,也能繼續正確工作
人們對可靠軟件的典型期望包括
允許用戶犯錯、系統能防止未經授權的訪問和濫用、在預期的負載和數據量下,性能滿足要求。
1 硬件故障
硬盤的 平均無故障時間(MTTF, mean time to failure) 約為 10 到 50 年。因此從數學期望上講,在擁有 10000 個磁盤的存儲集群上,平均每天會有 1 個磁盤出故障。
?
增加單個硬件的冗余度來減少系統故障率:
為了減少系統的故障率,第一反應通常都是增加單個硬件的冗余度,例如:磁盤可以組建 RAID,服務器可能有雙路電源和熱插拔 CPU,數據中心可能有電池和柴油發電機作為后備電源,某個組件掛掉時冗余組件可以立刻接管。這種方法雖然不能完全防止由硬件問題導致的系統失效,但它簡單易懂,通常也足以讓機器不間斷運行很多年。
?
多節點的高可用:
多節點的設計就是優先考慮 靈活性(flexibility) 和 彈性(elasticity),而不是單機可靠性。
?
2. 軟件錯誤
比起不相關的硬件故障往往可能造成更多的 系統失效
- 失控進程會用盡一些共享資源,包括 CPU 時間、內存、磁盤空間或網絡帶寬。
- 系統依賴的服務變慢,沒有響應,或者開始返回錯誤的響應。
- 級聯故障,一個組件中的小故障觸發另一個組件中的故障,進而觸發更多的故障
監控是有必要的
如果系統能夠提供一些保證(例如在一個消息隊列中,進入與發出的消息數量相等),那么系統就可以在運行時不斷自檢,并在出現 差異(discrepancy) 時報警。
?
3. 人為錯誤
盡管人類不可靠,但怎么做才能讓系統變得可靠?最好的系統會組合使用以下幾種辦法:
- 將人們最容易犯錯的地方與可能導致失效的地方 解耦(decouple)。比如提供一個功能齊全的非生產環境 沙箱(sandbox),使人們可以在不影響真實用戶的情況下,使用真實數據安全地探索和實驗。
- 確定測試邊界
- 快速回滾配置變更,分批發布新代碼
- 遙測(telemetry):配置詳細和明確的監控,比如性能指標和錯誤率。監控可以向我們發出預警信號,并允許我們檢查是否有任何地方違反了假設和約束。當出現問題時,指標數據對于問題診斷是非常寶貴的。
?
二. 可伸縮性
可伸縮性(Scalability) 是用來描述系統應對負載增長能力的術語。
系統今天能可靠運行,并不意味未來也能可靠運行。服務 降級(degradation) 的一個常見原因是負載增加,例如:系統負載已經從一萬個并發用戶增長到十萬個并發用戶,或者從一百萬增長到一千萬。
1. 描述負載與推特的例子
描述負載的方面,要注意最佳選擇取決于系統架構與業務的流量
- 可能是每秒向 Web 服務器發出的請求
- 數據庫中的讀寫比率
- 聊天室中同時活躍的用戶數量
- 緩存命中率或其他東西
為了使這個概念更加具體,我們描述推特的例子。推特的兩個主要業務是:
- 發布推文: 用戶可以向其粉絲發布新消息(平均 4.6k 請求 / 秒,峰值超過 12k 請求 / 秒)。
- 主頁時間線: 用戶可以查閱他們關注的人發布的推文(300k 請求 / 秒)。
這里的一個需求是:當博主發布新的作品時,給關注他的人發送推文的邏輯。
方法 | 說明 |
---|---|
方法1 | 發布推文時,只需將新推文插入全局推文集合即可。當一個用戶請求自己的主頁時間線時,首先查找他關注的所有人,查詢這些被關注用戶發布的推文并按時間順序合并。 |
方法2 | 維護消息隊列,當發布新的博文時,提前給每個關注者發送到主頁緩存中。所以讀取開銷小,因為寫入時已經做了這部分工作。 因為寫入時做更多的工作,寫出時做更少的工作,這樣查詢時會更快。 |
?
方法1,2的優劣
- 推特的第一個版本使用了方法 1,但系統很難跟上主頁時間線查詢的負載。所以公司轉向了方法 2,方法 2 的效果更好,因為發推頻率比查詢主頁時間線的頻率幾乎低了兩個數量級。
- 方法 2 的缺點是,發推現在需要大量的額外工作。平均來說,一條推文會發往約 75 個關注者,所以每秒 4.6k 的發推寫入,變成了對主頁時間線緩存每秒 345k 的寫入。但這個平均值隱藏了用戶粉絲數差異巨大這一現實,一些用戶有超過 3000 萬的粉絲,這意味著一條推文就可能會導致主頁時間線緩存的 3000 萬次寫入!及時完成這種操作是一個巨大的挑戰 —— 推特嘗試在 5 秒內向粉絲發送推文。
要根據負載來選擇方案,不能一概而論
- 這里需要考慮的是,因為每個用戶粉絲數的分布決定了
扇出負載
,而且需要在一定的時間內扇出,這里對系統的伸縮性(通過添加機器來分擔負載?)也提出了要求,所以方法1,2不能一概而論。- 雖然在查詢速度上慢,但方法1的扇出負載明顯比方法2小,所以找到用戶可接受的加載時間是需要權衡的。
?
最終的方案:
現在已經穩健地實現了方法 2,推特逐步轉向了兩種方法的混合。
- 大多數用戶發的推文會被扇出寫入其粉絲主頁時間線緩存中。
- 少數擁有海量粉絲的用戶(即名流)會被排除在外。當用戶讀取主頁時間線時,分別地獲取出該用戶所關注的每位名流的推文,再與用戶的主頁時間線緩存合并,如方法 1。
這樣就規避了這些名流帶來的巨大扇出負載。
?
2. 描述性能-延遲和響應時間
一旦系統的負載被描述好,就可以研究當負載增加會發生什么。
- 增加負載參數并保持系統資源(CPU、內存、網絡帶寬等)不變時,系統性能將受到什么影響?
- 增加負載參數并希望保持性能不變時,需要增加多少系統資源?
這兩個問題都需要性能數據,所以讓我們簡單地看一下如何描述系統性能。
- 對于 Hadoop 這樣的批處理系統,通常關心的是 吞吐量(throughput),即每秒可以處理的請求、或者在特定規模數據集上運行作業的總時間。
- 對于在線系統,通常更重要的是服務的 響應時間(response time),即客戶端發送請求到接收響應之間的時間。
?
- 響應時間:是客戶所看到的,除了實際處理請求的時間( 服務時間(service time) )之外,還包括網絡延遲和排隊延遲;
- 延遲:是某個請求等待處理的 持續時長,在此期間它處于 休眠(latent) 狀態,并等待服務。
響應時間的分布
- 負載不同時:大多數請求是相當快的,但偶爾會出現需要更長的時間的異常值。這也許是因為緩慢的請求
實質上開銷更大
,例如它們可能會處理更多的數據
。- 負載相同時:即使所有請求都花費相同時間的情況下,隨機的附加延遲也會導致結果變化,例如:1.
上下文切換
到后臺進程,2.網絡數據包丟失
與 3.TCP 重傳
,4.垃圾收集
暫停,5. 強制從磁盤讀取的頁面錯誤
,6. 服務器機架中的震動,還有很多其他原因。
圖展示了一個服務 100 次請求響應時間的均值與百分位數。
?
3. 應對負載的方法
適應某個級別負載的架構不太可能應付 10 倍于此的負載。如果你正在開發一個快速增長的服務,那么每次負載發生數量級的增長時,你可能都需要重新考慮架構
— 或者更頻繁。
應用系統的問題可能是讀取量、寫入量、要存儲的數據量、數據的復雜度、響應時間要求、訪問模式或者所有問題的大雜燴。
舉個例子,用于處理每秒十萬個請求(每個大小為 1 kB)的系統與用于處理每分鐘 3 個請求(每個大小為 2GB)的系統看上去會非常不一樣,盡管兩個系統有同樣的數據吞吐量
。
?
一個常用的方法是:
跨多臺機器部署 無狀態服務(stateless services) 非常簡單,但將帶狀態的數據系統從單節點變為分布式配置則可能
引入許多額外復雜度
(分庫分表、冷熱分離)。出于這個原因,常識告訴我們應該將數據庫放在單個節點上(縱向伸縮),直到伸縮成本或可用性需求迫使其改為分布式
。
?
根據業務情況來模擬負載情況:
一個良好適配應用的可伸縮架構,是圍繞著 假設(assumption) 建立的:
- 哪些操作是常見的?哪些操作是罕見的?這就是所謂負載參數。
- 如果假設最終是錯誤的,那么為伸縮所做的工程投入就白費了,最糟糕的是適得其反。
?
四. 可維護性
眾所周知,軟件的大部分開銷并不在最初的開發階段,而是在持續的維護階段,包括修復漏洞、保持系統正常運行、調查失效、適配新的平臺、為新的場景進行修改、償還技術債和添加新的功能。
我們在設計之初就盡量考慮盡可能減少維護期間的痛苦,從而避免自己的軟件系統變成遺留系統。
為此,我們將特別關注軟件系統的三個設計原則:
- 可操作性(Operability): 便于運維團隊保持系統平穩運行。
- 簡單性(Simplicity):從系統中消除盡可能多的 復雜度(complexity),使新工程師也能輕松理解系統。
- 可演化性(evolvability): 使工程師在未來能輕松地對系統進行更改,當需求變化時為新應用場景做適配。也稱為 可擴展性(extensibility)、可修改性(modifiability) 或 可塑性(plasticity)。
1. 可操作性:人生苦短,關愛運維
盡管運維的某些方面可以,而且應該是自動化的,但在最初建立正確運作的自動化機制仍然取決于人。
一個優秀運維團隊的典型職責如下(或者更多):
- 監控系統的運行狀況,并在服務狀態不佳時
快速恢復
服務。跟蹤問題的原因
,例如系統故障或性能下降。- 了解
系統間的相互作用
,以便在異常變更造成損失前進行規避。預測未來
的問題,并在問題出現之前加以解決(例如,容量規劃)。- 建立
部署、配置、管理
方面的良好實踐,編寫相應工具。- 執行復雜的維護任務,例如將應用程序
從一個平臺遷移到另一個平臺
。- 定義工作流程,使運維操作可預測,并保持生產環境穩定。
- 鐵打的營盤流水的兵,
維持組織對系統的了解
。
數據系統可以通過各種方式使日常任務更輕松:
- 通過
良好的監控
,提供對系統內部狀態和運行時行為的 可見性(visibility)。- 為自動化提供良好支持,將系統
與標準化工具相集成
。避免依賴單臺機器
(在整個系統繼續不間斷運行的情況下允許機器停機維護)。- 提供良好的文檔和易于理解的
操作模型(“如果做 X,會發生 Y”)
。- 提供良好的
默認行為
,但需要時也允許管理員自由覆蓋默認值。- 有條件時進行
自我修復
(how),但需要時也允許管理員手動控制系統狀態。(系統)行為可預測
,最大限度減少意外。
?
2. 簡單性:管理復雜度
復雜度(complexity) 有各種可能的癥狀,例如:狀態空間激增、模塊間緊密耦合、糾結的依賴關系、不一致的命名和術語、解決性能問題的 Hack、需要繞開的特例等等。
降低復雜度能極大提高軟件可維護性
因為復雜度導致維護困難時,預算和時間安排通常會超支。在復雜的軟件中進行變更,引入錯誤的風險也更大:當開發人員難以理解系統時,隱藏的假設、無意的后果和意外的交互就更容易被忽略。 相反,降低復雜度能極大地提高軟件的可維護性,因此簡單性應該是構建系統的一個關鍵目標。
?
通過抽象來降低復雜度
用于消除 額外復雜度 的最好工具之一是 抽象(abstraction)。
一個好的抽象可以將大量實現細節隱藏在一個干凈,簡單易懂
的外觀下面。比起重復造很多輪子,重用抽象不僅更有效率
,而且有助于開發高質量的軟件。抽象組件的質量改進
將使所有使用它的應用受益。
?
3. 可演化性:擁抱變化
系統的需求處于常態的變化中,例如:你了解了新的事實、出現意想不到的應用場景、業務優先級發生變化、用戶要求新功能、新平臺取代舊平臺、法律或監管要求發生變化、系統增長迫使架構變化等。
在組織流程方面,敏捷(agile) 工作模式為適應變化提供了一個框架。敏捷社區還開發了對在頻繁變化的環境中開發軟件很有幫助的技術工具和模式,如 測試驅動開發(TDD, test-driven development) 和 重構(refactoring) 。
?
修改數據系統并使其適應不斷變化需求的容易程度,是與 簡單性 和 抽象性 密切相關的:簡單易懂的系統通常比復雜系統更容易修改。
?
一個應用必須滿足各種需求才稱得上有用。
- 一些 功能需求(functional requirements,即它應該做什么,比如允許以各種方式存儲,檢索,搜索和處理數據)
- 一些 非功能性需求(nonfunctional,即通用屬性,例如安全性、可靠性、合規性、可伸縮性、兼容性和可維護性)。在本章詳細討論了可靠性,可伸縮性和可維護性。
?