當應用程式變成「資料處理中心」
從「算得快」到「把資料管好」——先認識五塊積木,再看見貫穿全書的三支柱
過去比「誰算得快」,現在比「誰把資料管得好」
你可能覺得電腦這麼快,還有什麼難得倒它?過去很多程式的瓶頸確實是 運算密集型 ——只要 CPU 夠猛就好。
但網路普及、資料爆炸之後,今天大多數應用更像 資料密集型 ——它不是計算機,而是一座「資料處理中心」。挑戰從「算得快」變成三道難題:
要存、要處理的資料「多不多」?從 MB 到 PB,規模前所未有。
資料「長什麼樣」?不只是表格,還有圖片、影片、社交關係,盤根錯節。
資料「跑得多快」?產生、更新、讀取飛快,系統得即時回應。
傳統餐廳拼的是廚師手藝(CPU);現代外送平台拼的是——同時管好幾百萬筆訂單、幾十萬份菜單、幾萬個外送員位置即時更新。後者要的不是「更快的廚師」,而是一整套協調資料的系統。
直接讀原文,旁邊就是白話
這是一本英文書。我們把它最關鍵的開場,左邊放 原文、右邊放白話中文,一句對一句——你既看得懂,也順手練了原文。
「網際網路做得太成功,多數人把它當成像太平洋一樣的自然資源——忘了它其實是人造的。」
弦外之音:好系統「理所當然」的背後,是大量刻意的設計。這正是本書要拆給你看的東西。
今天大多數應用是「資料密集型」,而不是「運算密集型」。
CPU 算力很少是瓶頸——真正的難題是資料的「量」、「複雜度」,還有它「變化的速度」。
而這種應用,通常是用一組標準的「建構塊」組裝出來的。
程式碼教學的「程式碼 ↔ 白話」並排,搬到一本雙語書上,剛好變成「英文原文 ↔ 白話中文」——同一個元件,用在書上甚至更貼。
五塊積木:資料系統的標準零件
幾乎每個資料密集型應用,都是把下面這五塊積木挑著組起來的。像玩樂高——每塊各司其職。
把資料持久存起來,之後(自己或別的程式)還找得回來。系統的「核心資產」。
把「算過、常用」的結果先存起來,下次直接拿,不必重算或回頭翻資料庫。
像書本目錄——靠關鍵字或條件快速找到資料,不必把整批掃過一遍。
把訊息丟進佇列,另一端慢慢非同步處理;發送方不必等,服務彼此解耦。
定期把一大批累積資料一次處理完,例如每天半夜跑報表、做分析。
Redis 既能當快取也能當訊息佇列;Kafka 既是訊息佇列又有資料庫般的持久性。所以選工具別只看「分類」,要看它實際提供什麼能力——靠的是抽象化,你只管用它的介面。
動手配配看:哪塊積木解哪個問題?
把上面的積木,拖到它最該負責的任務上。配完按「對答案」。
貫穿全書的「黃金三角」
把積木組起來,你就從「應用程式開發者」升級成「資料系統設計師」。而衡量一個好系統,整本書反覆回到三個字——點點看每一根支柱:
它們不是各自獨立的。為了可擴展性硬塞一堆分散式機制,可能讓系統更難懂,反而傷了可維護性——設計永遠在權衡。
小試身手
看懂這一格,你就抓到本章的骨架了。來兩題:
三支柱第一根。我們先搞懂一個最常被搞混的分別——「故障 fault」到底跟「失效 failure」差在哪?往下捲。
可靠性:讓「故障」不等於「失效」
三支柱第一根——零件總會壞,目標不是「永不出錯」,而是壞了一塊也撐得住
先分清楚:故障 ≠ 失效
這是整章最常被搞混、卻最關鍵的一對詞。用「煮飯」想就懂了:電鍋插頭鬆了,是電鍋這個「元件」出的小問題——這叫 故障 Fault; 結果全家沒飯吃,是整套「煮飯系統」對人停止服務——這才叫 失效 Failure。
系統「內部某個元件」偏離規格:硬碟壞、記憶體秀逗、程式有 bug。範疇是局部,工程師才看得到。
整個「系統對使用者」停止提供服務:網站打不開、訂單送不出。範疇是整體,使用者直接感受到。
預期故障一定會發生,事先準備應對策略,讓「故障」不要演變成「失效」。這就是可靠性的真正目標。
故障是「原因」,失效是「結果」。我們的目標不是「永不故障」——那幾乎不可能——而是設計出 容錯 機制,讓壞掉一塊也不至於整個垮掉。
直接讀原文,旁邊就是白話
這段是書裡對「故障 vs 失效」下定義的原句。左邊 原文、右邊白話中文,一句對一句——讀懂定義,順手練原文。
那些「會出錯的事」就叫故障(fault);能預先料到故障、又有辦法應付它的系統,就叫「容錯(fault-tolerant)」或「有彈性(resilient)」。
注意:故障(fault)跟失效(failure)不是同一回事。
故障,通常指系統「某一個元件」偏離了它該有的規格;而失效,是整個系統「對使用者」停止提供該有的服務。
你不可能把故障機率壓到零,所以最好的做法是——設計容錯機制,攔住故障,別讓它一路滾成失效。
而這整本書,就是要教你怎麼用「不可靠的零件」,組出「可靠的系統」。
「用不可靠的零件,組出可靠的系統。」——這不是雞湯,而是接下來三節(硬體、軟體、人為)一以貫之的設計哲學。
跑給你看:故障發生,使用者卻完全無感
下面有五位「演員」。按「下一步」,看主硬碟突然壞掉時,容錯機制怎麼悄悄把流量切到備援、讓使用者根本沒發現。
準備多份備份,一個壞了立刻替補——資料庫主從複製、雙電源供應器、RAID 磁碟陣列都是它的化身。 冗餘 聽起來像「多餘」,但在關鍵時刻能救命。
故障從哪來?三大來源
要防故障,得先知道它躲在哪。書把故障來源分成三類,性質完全不同,對策也不一樣。
硬碟壞、記憶體故障、停電、網路線鬆脫。通常隨機且彼此獨立——但資料中心若有上萬顆硬碟,「平均每天壞一顆」就是日常。
程式 bug、失控程序吃光資源、依賴的服務變慢、連鎖反應。它是「系統性錯誤」——特定條件才觸發,一觸發就同時打趴一大片相同程式碼。
設定檔貼錯、誤刪資料庫、部署出包。研究顯示人為失誤才是大型服務中斷的「頭號元凶」——硬體故障只佔 10–25%。
硬體壞了通常只影響一台;但一個 系統性錯誤 或一次配置失誤,可能瞬間放倒成百上千台。小心 級聯失效——一張骨牌倒,全部跟著倒。
動手配配看:哪種故障,配哪種對策?
三種故障來源各有最對症的防禦。把左邊的故障,拖到右邊它該用的對策上。配完按「對答案」。
Netflix 的「混沌猴子(Chaos Monkey)」會在正式環境隨機關掉伺服器。為什麼?因為很多容錯機制平時在「沉睡」,只有真故障才會被觸發—— 混沌工程 主動製造故障,才能確認它們真的有效。
從「單機備胎」到「整台機器壞了也不怕」
應對硬體故障的思維,正在經歷一場轉變。用披薩店想最直覺:
同一家店多買幾套設備——兩台烤箱、外送車備胎、兩位師傅。專注「讓單一台機器盡量不中斷」。機器一多,這招就不夠了。
把服務分散到多台機器。一整家分店停電關門,訂單自動轉給其他分店,客戶吃得到熱披薩、完全不知情。重點變成「讓整個系統即使有機器掛掉也能繼續運作」。
有了 軟體容錯,你可以分批更新——一次只重啟一小部分伺服器,其他照常服務。這叫 滾動升級(Rolling Upgrade), 像火車行進中一節節車廂輪流進站維護,整列車從不停下。
小試身手
抓到「故障 ≠ 失效」與「為何要容錯」這兩件事,這一節就通了。來兩題:
系統撐得住、不失效之後,下一個問題是——使用者暴增十倍,它還跑得動嗎?下一站我們談「可擴展性 Scalability」。往下捲。
可擴展性:從一個人用到一百萬人
先量出系統的「飯量」,再學會看「最慢的那群人」過得好不好——最後決定:把一台機器養肥,還是多找幾台分工
先別急著喊「可擴展」,先量出你的系統有多忙
很多人喜歡幫系統貼個標籤:「我們很可擴展」。但這句話其實沒什麼意義—— 可擴展性 不是一個是非題,而是一個問句:「如果負載這樣長大,我們有哪些應對的選項?」
要回答這個問句,第一步得先把系統現在的 負載 講清楚。就像醫生看病前要先量血壓、測心跳——我們用一組數字來描述系統的壓力,這組數字叫 負載參數。
每秒處理多少個請求?(requests per second)
讀取多還是寫入多?這就是讀寫比。
同時有多少人在線上活躍?(concurrent users)
資料命中率有多高?(cache hit rate)
一個交通監測系統,只看全天平均車流量會覺得很順暢;可是它在上下班尖峰時刻當機,影響就大了。所以除了平均負載,更要看 尖峰負載(Peak Load)——那才是真正考驗系統極限的時刻。
直接讀原文,旁邊就是白話
這是一本英文書。我們把它談「可擴展性/負載」最關鍵的幾句,左邊放 原文、右邊放白話中文,一句對一句——你既看得懂,也順手練了原文。
可擴展性,是我們用來描述「系統應對負載增加的能力」的詞。
它不是一個能往系統上一貼的單一維度標籤——光說「X 可擴展」或「Y 不可擴展」其實沒有意義。
真正在談可擴展性,是在問這種問題:「如果系統以某種方式長大,我們有哪些應對成長的選項?」
負載,可以用一組數字來描述,我們把這組數字叫做「負載參數」。
也許你在意的是平均情況;也許你的瓶頸,是被一小撮極端案例給主宰的。
下次有人說「我們要做可擴展的系統」,你可以接一句:「好——如果負載長大十倍,我們具體要怎麼辦?」這一問,才是設計真正開始的地方。
招牌案例:Twitter 的「扇出 Fan-out」
書中用 Twitter(2012 年的數據)當經典範例。它有兩個主要操作:發推文,和看首頁時間軸(你追蹤的人發的推文,依時間排好)。問題來了——這兩件事該怎麼設計?
最初 Twitter 用「讀取時即時聚合」:你一打開首頁,系統才現去查你追蹤的所有人、把他們的推文合併排序。發推文很輕鬆,但看首頁慢到爆——因為讀取量是寫入量的近百倍。於是改成下面這招:發推文時,就先把它「扇出」寫進每個追蹤者的首頁快取。按「下一步」看它怎麼跑。
把工作從「讀取那一刻」搬到「寫入那一刻」。代價是發推文變貴:平均一則推文要送給約 75 位追蹤者,4.6k 則/秒就變成 345k 次/秒的首頁快取寫入。
追蹤者數量分佈差異極大。有些名人超過 3,000 萬追蹤者——一則推文就可能引發超過 3,000 萬次寫入,而且 Twitter 還想在五秒內送達!所以後來改用「扇出混合策略」:一般人照樣扇出寫快取,名人則例外——他們的推文等你讀首頁時才現抓、現合併(回到方法一)。
「快」怎麼量?別再只看平均值
描述完負載,接著要描述效能。先分清楚兩個詞: 響應時間 是用戶從按下到拿到結果的「總時間」; 延遲 則是請求在系統裡「等待被處理」的那段時間。
那「平均響應時間」呢?它就像「平均身高」——會把極端值平均掉、騙過你。平均 200 毫秒看起來很快,可能藏著一群等 10 秒的倒霉用戶。所以高手改看 百分位數。
一半的請求比它快、一半比它慢。代表「典型用戶」的體驗,比平均值可靠多了。
反過來說,有 5% 的請求比這個時間還慢。開始看見那群不開心的人。
99% 都比它快,剩下 1% 更慢。這些高百分位數就是 尾部延遲。
因為最慢的請求,常常來自帳戶資料最多的用戶——也就是買最多、最有價值的大戶。亞馬遜發現:響應時間慢 100 毫秒,銷售額就少 1%。讓最慢的那群人不開心,就是讓錢包不開心。
更麻煩的是尾部延遲放大(Tail Latency Amplification):當一個頁面要同時呼叫好幾個後端服務來組裝,就算每個服務各自的 p99 都很漂亮,只要其中任何一個落入它那 1% 的慢,整個請求就被拖慢。服務越多,踩到至少一個慢請求的機率就越高。這就像點一份套餐,只要沙拉那攤忙了,你整桌都得等。
應對負載的兩條路:向上 vs 向外
負載長大、瓶頸出現,怎麼辦?書上說這常被講成一個二選一:
換一台更強的機器。像把一個廚師練成料理鐵人。簡單、改動少、見效快;但有物理上限、高階硬體貴、且是單點故障。
把負載分散到多台較小的機器。像開連鎖分店、找店長(負載平衡器)分流客人。彈性近乎無限、硬體便宜、單台壞了不全停;但管理複雜、要處理資料一致性、程式可能得重構。
向外擴展常採用 無共享架構(Shared-Nothing)——每台機器各管各的,靠網路溝通。但現實裡好的架構通常是務實的混搭:用幾台還算強的機器,往往比一大堆小虛擬機更簡單也更便宜。沒有所謂的「神奇擴展銀彈」(magic scaling sauce)。
動手分分看:下面每個情境,比較適合「向上」還是「向外」?把標籤拖過去,配完按「對答案」。
小試身手
抓到「負載、尾部延遲、擴展策略」這三件事,這一章的骨架就在你手上了。來兩題:
系統可靠又能擴展之後,最後一根支柱是——三年後接手的人,會詛咒你還是感謝你?我們去看軟體成本最大的那一塊:上線之後的維護。往下捲。
可維護性:讓程式碼成為團隊的資產
軟體最大的花費不在「寫出來」,而在「養下去」——用三大設計原則,讓系統老得慢一點、優雅一點
寫完不是結束,是帳單的開始
很多人以為程式碼寫完、上線就萬事大吉。本書開宗明義就戳破這個幻覺:絕大部分的軟體成本,不在最初的開發,而在它之後的 持續維護 ——修 bug、保持運作、調查故障、適應新平台、改成新需求、償還技術債、再加新功能。
選地、翻土、種下第一批種子,興高采烈。這只是最初的一筆投入。
之後每天澆水、除草、驅蟲、改建——長期、持續、才是真正的大工程。
雜草叢生、病蟲害肆虐,最後變成沒人想靠近的一團亂麻。
當一個系統因為缺乏文件、設計不良、技術過時,或歷經太多不同的人之手,變得難以理解、修改與維護,它就成了 遺留系統(Legacy System)。 書上一句經典:「每個遺留系統都以自己獨特的方式令人不快。」我們無法完全避免系統變老,卻能從第一天就用好設計,盡量減少未來的痛苦。
直接讀原文,旁邊就是白話
這一段是本章對「可維護性」最直球的定義。左邊原文、右邊白話,一句對一句——你既看得懂,也順手練了原文。
眾所周知,軟體的絕大部分成本,不在最初的開發,而在它之後的持續維護。
……修 bug、保持系統運作、調查故障、適應新平台、償還技術債,還有新增功能。
每個遺留系統都以自己獨特的方式令人不快,所以很難給出對付它們的通用建議。
但我們可以、也應該這樣設計軟體:讓維護時的痛苦降到最低,從而避免親手製造出遺留系統。
為此,我們會特別關注三個設計原則——可操作性、簡潔性、可演進性。
軟體想要「活得久、活得好」,就要把這三個原則從設計的第一天起就放進骨子裡:讓系統好顧、好懂、好改。下一格,點點看它們各是什麼。
招牌互動:可維護性的三大設計原則
可維護性不是一句口號,而是落在三根具體的支柱上。點每一根,看它到底在解什麼問題:
每台機車有 GPS、電量、健康儀表板,中央大螢幕一覽無遺(可視性);備用車充足,一台拋錨立刻補上,技師能一次只保養幾台而不中斷配送(這就是 滾動升級 Rolling Upgrade)。 這就是把系統設計得「好顧」。
複雜度有兩種——一種非有不可,一種純屬自找
三大原則裡,「簡潔性」最容易被誤會。它不是要你砍功能,而是要你分清楚兩種複雜度,然後把那種「自找的」清掉。
問題本身固有的困難,來自使用者需求。例如多國語言支付系統要處理各種貨幣與稅務規則——無論你怎麼寫都跑不掉。無法完全避免,但可用好設計來管理、簡化。
不是問題本身固有的,而是因實作方式不當才額外冒出來的負擔:命名混亂、模組緊密耦合、纏繞的依賴、為效能塞的「偏方」。本來可以避免——這正是我們要努力消除的。
書上把 偶然複雜度(accidental complexity) 定義得很乾脆:如果一個複雜度「不是軟體要解的問題本身固有的(從使用者角度看),而只是從實作冒出來的」,它就是偶然的。那要怎麼清掉它?
書上原話:「對抗偶然複雜度,我們最好的工具之一就是 抽象化(abstraction)。」 一個好的抽象,能把一大堆實作細節藏在乾淨好懂的介面後面——高階程式語言把機器碼藏起來、SQL 把磁碟上的資料結構藏起來。好抽象還能被重複使用,而且因為被很多地方用,它的品質會被打磨得更高。只是在分散式系統裡,要找到好抽象非常難——這正是全書持續追尋的目標。
動手配配看:哪個做法服務哪根支柱?
把下面這些常見做法,拖到它最主要在強化的那個原則上。配完按「對答案」。
可演進性不是靠拍腦袋。Twitter 之所以能從「讀時才組裝時間線」演進到「寫時預先算好時間線」,正是因為他們監控到了巨大的讀取負載壓力——數據反饋,才是改變方向的底氣。可操作性的「可視性」,反過來餵養了可演進性。
小試身手
把三根支柱串起來,你就抓到「可維護性」的精神了。來兩題:
可靠性、可擴展性、可維護性——三根支柱都立起來了。最後我們把它們接成一張圖,看它們如何互相拉扯、又如何彼此成全。往下捲。
大局:把三支柱接起來
它們從來不是各自獨立——而是一邊互相拉扯,一邊互相成全
三句話,收齊整章
你已經走過三根支柱。如果只能帶走三句話,就是這三句:
硬體會壞、軟體有 bug、人會犯錯。目標不是零故障,而是讓故障不要變成失效。
先學會用負載參數描述壓力,用百分位數衡量效能,再決定向上還是向外擴展。
軟體大部分成本在上線之後。靠可操作性、簡潔性、演進性三招,降低長期維護成本。
它們會互相拉扯
最容易被忽略的一點:三支柱不是獨立的清單,而是會彼此影響的力。改善一個,常常牽動另一個——有時相助,有時相剋。
想做到高可靠,往往需要更好的監控——而監控正是「可操作性(可維護性)」的一部分。把可維護性做好,可靠性也跟著受益。
為了追求可擴展性,硬塞一堆分散式機制,系統會變得更難理解,反而傷害可維護性。所以設計永遠是權衡,不是把每項都調到最大。
這也是為什麼,當有人問「你的系統可擴展嗎?」最好的回答不是「可以」,而是反問一句:「如果負載以某種方式成長,我們打算怎麼辦?」
用作者自己的結語收尾
這是第 1 章 Summary 的原文,左原文、右白話——剛好替整章畫上句點。
這一章,我們探索了思考資料密集型應用的一些根本方式。這些原則會帶著我們走完整本書。
可靠性,是指就算發生故障,系統仍能正確運作。
可擴展性,是指就算負載增加,也有辦法維持好效能。
可維護性有很多面向,但本質上,就是讓「之後要跟這套系統打交道的工程與營運團隊」日子好過一點。
看懂三支柱,你多了一項超能力
下次系統出狀況,你能更快定位:是「故障沒被容錯接住」(可靠性)、是「負載長大撐不住」(可擴展性),還是「沒人敢動這段程式碼」(可維護性)?分得清楚,就問得出精準的問題。最後兩題,驗收一下:
第 1 章,完。
你剛剛用「程式碼導讀課」的形式,讀完了一本書的第一章——同一套捲軸敘事、雙語並排、互動測驗,搬到書本上毫無違和。這就是「融合」想證明的事。
後面每一章,DDIA 都會一層一層往下挖:資料模型、儲存引擎、編碼與演化……都能用這同一套導讀形式攤開來。第 1 章只是把「地基」鋪好。
整頁沿用既有的 styles.css 與 main.js 設計系統,只把內容換成 DDIA 第 1 章。若你喜歡,這套作法可以接回主 app、套用到其他章節。