本章摘要: 系統性拆解可靠性(容忍硬體故障、軟體錯誤與人為失誤)、可擴展性(以負載參數和百分位數量化效能)及可維護性(可操作性、簡單性、可演化性)三大核心概念,建立後續章節所需的術語框架。
本章探討資料密集型應用程式最核心的三項非功能性需求:可靠性(Reliability)、可擴展性(Scalability) 與 可維護性(Maintainability)。這三個詞彙在業界被廣泛使用,但往往缺乏精確定義。作者在此章節中系統性地拆解每個概念,建立後續章節所需的思考框架。
Reliability(可靠性)#
可靠性的直覺定義是「即使出了問題,系統仍能正確運作」。具體而言,一個可靠的系統應該滿足:
- 執行使用者預期的功能
- 能容忍使用者的錯誤操作
- 在預期負載下維持足夠的效能
- 防止未經授權的存取與濫用
Fault 與 Failure 是不同的概念。 Fault 指的是系統中某個元件偏離規格,而 Failure 則是整個系統停止對外提供服務。設計容錯機制的目標,就是防止 fault 演變為 failure。
容錯 vs 預防故障#
在多數情況下,我們偏好容忍故障(tolerating faults)而非預防故障(preventing faults)。一個有趣的實踐是:刻意注入故障以測試容錯機制是否正常運作。Netflix 的 Chaos Monkey 就是這種做法的代表——隨機終止生產環境中的程序,確保系統能承受意外的元件失效。
不過也有例外。對於安全性問題,預防遠比容忍重要,因為一旦攻擊者取得敏感資料,事件便無法逆轉。
硬體故障#
硬體故障是最直覺的故障來源:硬碟損壞、記憶體故障、電力中斷、網路線被拔掉。硬碟的平均故障間隔時間(MTTF) 大約在 10 到 50 年之間,這意味著一個擁有 10,000 顆硬碟的儲存叢集,平均每天會有一顆硬碟損壞。
傳統的應對方式是對硬體元件進行冗餘設計:RAID 磁碟陣列、雙電源供應、熱插拔 CPU 等。但隨著雲端平台(如 AWS)的普及,虛擬機器隨時可能消失已成常態,因此現代系統逐漸轉向以軟體層面的容錯技術來因應整台機器的失效。這也帶來了營運上的好處:支援滾動升級(rolling upgrade),不需要為了套用系統修補程式而安排停機時間。
軟體錯誤#
相較於硬體故障的隨機性與獨立性,系統性軟體錯誤(systematic error) 更難預測,且往往會跨節點關聯,造成更大規模的故障。常見的軟體錯誤包括:
- 特定輸入導致所有應用伺服器實例同時崩潰(例如 2012 年閏秒事件導致 Linux 核心觸發 bug)
- 失控的程序耗盡共享資源(CPU、記憶體、磁碟空間、網路頻寬)
- 依賴的外部服務變慢、無回應或回傳損壞的資料
- 級聯故障(cascading failures):一個小故障觸發另一個故障,連鎖反應擴大影響範圍
應對軟體錯誤沒有銀彈,但有許多小措施能幫助:仔細審視系統假設與互動關係、徹底測試、程序隔離、允許程序崩潰後自動重啟、以及在生產環境持續監控與分析系統行為。
人為錯誤#
人是系統中最不可靠的元件。研究顯示,大型網路服務的停機事故中,配置錯誤(configuration error) 是最主要的原因,硬體故障僅佔 10-25%。
減少人為錯誤的策略包括:
| 策略 | 說明 |
|---|---|
| 設計不易出錯的介面 | 好的抽象層與 API 讓人容易做正確的事,但不能過度限制,否則人們會繞過它 |
| 提供沙箱環境 | 將容易犯錯的地方與可能造成故障的地方解耦,讓人可以用真實資料安全地實驗 |
| 全面的自動化測試 | 從單元測試到整合測試,特別是覆蓋正常操作中不易出現的邊界情況 |
| 快速回復機制 | 支援快速回滾設定變更、漸進式部署新版本、提供資料重算工具 |
| 完善的監控與遙測(telemetry) | 效能指標與錯誤率能提供早期預警,在問題發生時協助診斷 |
| 良好的管理實踐與培訓 | - |
Scalability(可擴展性)#
可擴展性討論的是:當系統的負載增加時,我們有什麼方法來應對。值得注意的是,可擴展性不是一個二元標籤——說「X 具有可擴展性」或「Y 無法擴展」是沒有意義的。正確的問法是:「如果系統以某種方式成長,我們有哪些選項來應對?」
描述負載(Describing Load)#
要討論擴展性,首先需要量化目前的負載。負載可以用幾個數字來描述,稱為負載參數(load parameters)。最佳的參數選擇取決於系統架構,可能是:
- Web 伺服器的每秒請求數
- 資料庫的讀寫比率
- 聊天室的同時活躍使用者數
- 快取命中率
Twitter 案例分析#
Twitter 的兩個核心操作(2012 年數據):
- 發推文:平均 4,600 次/秒,尖峰 12,000 次/秒
- 讀取主頁時間線:300,000 次/秒
處理每秒 12,000 次寫入本身並不困難,真正的挑戰來自 fan-out(扇出) ——每個使用者追蹤許多人,也被許多人追蹤。Twitter 嘗試了兩種架構:
方法一:寫入時簡單,讀取時計算。 發推文時只將推文寫入全域集合;讀取主頁時間線時,即時查詢使用者追蹤的所有人的推文並合併排序。

Figure 1-2: Twitter 主頁時間線的簡單關聯式架構
方法二:寫入時展開,讀取時直接取用。 為每個使用者維護一個主頁時間線快取。發推文時,查出所有追蹤者,將推文插入每個追蹤者的快取中。讀取時直接取用已計算好的結果。

Figure 1-3: Twitter 將推文投遞給追蹤者的資料管線
Twitter 最初採用方法一,但主頁時間線的讀取查詢負載過重,因此切換到方法二。由於發推文的頻率(4,600 次/秒)遠低於讀取時間線的頻率(300,000 次/秒),在寫入時多做一些工作是合理的取捨。
然而方法二的問題在於:部分使用者擁有超過 3,000 萬追蹤者,一則推文可能觸發 3,000 萬次寫入,且 Twitter 要求在 5 秒內完成投遞。最終 Twitter 採用了混合式架構:一般使用者用方法二(寫入時展開),名人用方法一(讀取時合併)。
Twitter 案例中,每位使用者的追蹤者數量分布(可能還需以發推頻率加權)是決定 fan-out 負載的關鍵負載參數。你的應用可能有完全不同的特性,但可以用相同的思維方式來推理其負載。
描述效能(Describing Performance)#
描述完負載後,下一步是探討負載增加時系統效能的變化。可以從兩個角度思考:
- 負載參數增加、系統資源不變時,效能如何受影響?
- 負載參數增加時,需要增加多少資源才能維持效能不變?
對於批次處理系統(如 Hadoop),關注的指標是 throughput(吞吐量)——每秒處理的記錄數,或在特定大小資料集上完成作業的總時間。對於線上系統,更重要的是 response time(回應時間)。
Latency 與 Response Time 並不相同。 Response time 是客戶端觀察到的完整時間,包含服務處理時間、網路延遲和排隊延遲。Latency 特指請求等待被處理的時間——即請求處於「潛伏」狀態的持續時間。
百分位數(Percentiles)#
回應時間不是一個固定值,而是一個分布。即使是相同的請求,每次的回應時間都會略有不同,原因包括:context switch、TCP 重傳、垃圾回收暫停、page fault 等等。
平均值(mean) 不是描述「典型」回應時間的好指標,因為它無法告訴你多少使用者實際經歷了那個延遲。更好的做法是使用百分位數(percentiles):
| 百分位數 | 說明 |
|---|---|
| p50(中位數) | 一半的請求在此時間內完成 |
| p95 | 95% 的請求在此時間內完成 |
| p99 | 99% 的請求在此時間內完成 |
| p999 | 99.9% 的請求在此時間內完成 |

Figure 1-4: 回應時間的平均值與百分位數示意圖
高百分位數的回應時間又稱為尾端延遲(tail latencies),它們直接影響使用者體驗。Amazon 以 p999 來定義內部服務的回應時間要求,因為回應最慢的客戶往往是帳戶資料最多的客戶——也就是最有價值的客戶。Amazon 的研究也顯示,回應時間增加 100ms 會使銷售額下降 1%。
百分位數也常用於定義 SLO(服務層級目標) 和 SLA(服務層級協議)。例如:中位數回應時間低於 200ms、p99 低於 1 秒,且服務可用率至少 99.9%。
尾端延遲放大效應#
在微服務架構中,服務一個終端使用者請求往往需要呼叫多個後端服務。即使每個後端服務只有少數請求較慢,但當一個使用者請求需要多次後端呼叫時,遇到至少一個慢速呼叫的機率大幅上升。這種效應稱為 tail latency amplification(尾端延遲放大)。

Figure 1-5: 尾端延遲放大效應:單一慢速後端請求拖慢整體回應
計算百分位數時,不可以對百分位數做平均。例如,不能將多台機器的 p99 取平均後得到整體的 p99。正確的做法是合併各機器的直方圖(histogram)後再計算。可用的高效演算法包括 forward decay、t-digest 和 HdrHistogram。
應對負載的方法(Approaches for Coping with Load)#
應對負載增長的方法通常分為兩類:
- 垂直擴展(scaling up / vertical scaling):換用更強大的機器
- 水平擴展(scaling out / horizontal scaling):將負載分散到多台較小的機器上,也稱為 shared-nothing 架構
實務上,好的架構通常是兩者的務實混合。使用數台性能較強的機器,往往比大量小型虛擬機器來得簡單且划算。
有些系統是 elastic(彈性的),能在偵測到負載增加時自動擴展資源;其他系統則依賴人工擴展。彈性系統適合負載高度不可預測的場景,但手動擴展的系統更簡單且較少意外。
將無狀態服務分散到多台機器相對容易,但將有狀態的資料系統從單節點遷移到分散式架構會引入大量額外的複雜性。因此過去的常見做法是:盡可能讓資料庫留在單一節點上(垂直擴展),直到成本或高可用性需求迫使你進行分散化。
不存在通用的、一體適用的可擴展架構。系統的架構取決於其負載參數的假設——哪些操作頻繁、哪些罕見。如果這些假設錯誤,擴展的工程投入輕則浪費、重則適得其反。
Maintainability(可維護性)#
軟體的大部分成本不在於最初的開發,而在於持續的維護——修復 bug、維持系統運行、調查故障、適配新平台、修改以滿足新的使用案例、償還技術債務、新增功能。
為了減少維護的痛苦並避免建立「遺留系統(legacy system)」,作者提出三個設計原則:
Operability(可操作性):讓營運團隊的日子好過#
好的營運團隊負責的工作包括:監控系統健康、追蹤問題根因、維持軟體更新、容量規劃、部署管理、維護安全性、定義可預測的運維流程、以及保存組織知識。
系統可以透過以下方式提升可操作性:
- 提供良好的監控,讓系統的執行時期行為與內部狀態清晰可見
- 支援與標準工具的自動化整合
- 避免依賴單一機器(允許在系統持續運行的同時對個別機器進行維護)
- 提供良好的文件與清晰的運維模型(「如果我做 X,就會發生 Y」)
- 提供合理的預設行為,同時允許管理員在需要時覆蓋預設值
- 在適當的情況下自我修復,但也提供手動控制的能力
- 行為可預測,減少意外
Simplicity(簡單性):管理複雜度#
隨著專案成長,系統往往變得越來越複雜。複雜度的症狀包括:狀態空間爆炸、模組緊密耦合、糾纏的相依關係、命名與術語不一致、為效能問題而做的各種 hack 等。
複雜度使得維護成本提高、預算與時程超支,也大幅增加了修改時引入 bug 的風險。因此,簡單性應該是系統設計的核心目標。
簡化系統不等於減少功能。關鍵在於移除偶發複雜度(accidental complexity)——那些並非問題本身所固有,而是由實作方式所引入的複雜度。抽象(abstraction) 是移除偶發複雜度最強大的工具:好的抽象能將大量實作細節隱藏在乾淨、易懂的介面之後。
例如:高階程式語言是對機器碼的抽象,SQL 是對磁碟與記憶體資料結構的抽象。然而,找到好的抽象非常困難,尤其是在分散式系統領域。
Evolvability(可演化性):讓變更變得容易#
系統需求幾乎不可能永遠不變。新的使用案例浮現、業務優先順序改變、使用者要求新功能、法規要求變更、系統成長迫使架構調整。
Agile 方法論提供了適應變化的組織流程框架,以及 TDD、重構等技術實踐。但這些討論通常聚焦在較小的範圍(幾個原始碼檔案)。本書關注的是更大規模的資料系統層級的敏捷性——例如,如何將 Twitter 的主頁時間線架構從方法一「重構」為方法二?
系統修改的容易程度,與其簡單性和抽象品質密切相關。簡單且易理解的系統,通常比複雜的系統更容易修改。作者用 evolvability 一詞來指稱資料系統層級的這種適應能力。
小結#
本章建立了貫穿全書的三大核心概念框架:
- Reliability:即使面對硬體故障、軟體 bug 和人為錯誤,系統仍能正確運作。容錯技術能對終端使用者隱藏特定類型的故障。
- Scalability:維持良好效能的策略,需要先量化負載與效能指標。可透過垂直或水平擴展增加處理能力。
- Maintainability:透過良好的可操作性、簡單性與可演化性,讓工程與營運團隊能高效地維護與演進系統。
這三者沒有簡單的解決方案,但存在反覆出現的模式與技術。後續章節將深入探討用以達成這些目標的各種技術、架構與演算法。