Designing Data-Intensive Applications:大型資料系統真正難的是什麼

大型資料系統最難的,通常不是決定「資料庫要選哪一家」,而是釐清可靠性、可擴充性、可維護性、一致性、效能與成本之間的取捨。 Martin Kleppmann 的《Designing Data-Intensive Applications》之所以重要,就是把資料模型、儲存引擎、複寫、分區、交易、分散式系統、批次與串流處理放在同一張地圖上,幫助工程團隊理解:每一種技術都有適用條件,也都有代價。

很多人第一次碰到大型系統問題,會先問:

SQL 還是 NoSQL?

PostgreSQL 夠不夠?

要不要用 Redis?

資料量變大是不是一定要分庫分表?

是不是改成微服務就比較能擴充?

這些問題都合理,但如果只從工具名稱開始,很容易錯過困難的部分。

《Designing Data-Intensive Applications》最有價值的地方,是先把工作負載、資料關係、故障模式、延遲要求與一致性需求看清楚,再決定工具,而不是替所有情境指定同一個「最好」的資料庫。

這也是資料密集型系統(Data-Intensive System)和單純「資料很多」不一樣的地方。

什麼叫資料密集型系統?

有些應用的主要難題在計算,例如大量影像運算或科學模擬;另一些應用麻煩的是資料。

資料怎麼存?

怎麼查?

怎麼在多台機器之間保持可接受的一致?

某一台機器壞掉時怎麼繼續服務?

需求改變後,舊資料怎麼和新版本共存?

這就是資料密集型應用常見的核心問題。

Kleppmann 把這類系統的品質大致拉回三個基礎:可靠性(Reliability)、可擴充性(Scalability)與可維護性(Maintainability)。

系統不只要「今天能跑」,還要在流量增加、硬體故障、軟體更新與人員交接之後,仍然能被理解與維護。

可靠性:局部出錯時,整體仍要能工作

大型系統一定會遇到故障。

硬碟會壞。

網路會斷。

伺服器會重啟。

程式會有 Bug。

操作人員也可能下錯指令。

所以可靠性的前提,是接受個別元件一定可能出問題,再設計讓局部故障不要直接變成整體失敗。

這個觀念對一般網站也很重要。

例如課程網站的付款流程,如果第三方金流暫時失敗,系統至少要能保留清楚狀態,而不是讓使用者付款成功、網站卻完全不知道。

可靠系統通常會更重視可追蹤性、重試策略、備份、故障切換與資料完整性。

可擴充性:先知道負載增加後哪裡會出問題

可擴充性常被簡化成「這個系統能不能撐十萬人」。

但更有用的問法是:當負載增加時,哪一個資源會最先成為瓶頸?

負載可以有很多形式:

每秒請求數增加。

同時在線人數增加。

每個使用者產生的資料量增加。

搜尋與分析查詢變複雜。

寫入速度突然提高。

不同負載,對架構的要求完全不同。

所以擴充之前應該先描述工作負載,而不是直接說「我們需要更大的架構」。

如果現在的瓶頸只是某一個慢查詢,先加索引可能比拆微服務有效得多。

可維護性:系統最終是交給人長期修改的

資料系統不會只活一天。

功能會增加,商業規則會變,資料格式也會演進。

因此可維護性不只是程式碼乾不乾淨,還包括營運是否容易、系統是否過度複雜,以及未來修改是否能控制風險。

這也是為什麼資料架構不能只追求「現在跑得最快」。

一個極度客製、只有原作者看得懂的系統,即使短期效能很好,長期成本可能很高。

如果你正在處理已經變慢、變亂的既有系統,可以延伸閱讀 系統愈做愈慢、愈改愈亂?架構最佳化應該先從哪裡開始。

資料模型:不是 SQL 與 NoSQL 誰比較先進

資料模型會直接影響你怎麼思考問題。

關聯式模型適合處理結構清楚、關係豐富、需要 Join 與完整性約束的資料。

文件模型常適合一對多、整包讀寫、資料結構比較自然聚合在一起的情境。

圖形模型則適合大量節點與關係之間的遍歷,例如社交關係、推薦或網路拓樸。

重點在於資料關係與查詢方式是否和模型吻合,和哪一種技術比較新沒有直接關係。

如果模型選錯,後面的應用程式就會一直用額外邏輯補救。

儲存引擎:同樣叫資料庫,底層取捨可能完全不同

資料寫進去之後,系統到底怎麼存,也會影響效能特性。

B-tree 類型的結構常見於傳統關聯式資料庫索引,讀取延遲相對可預期,也能兼顧範圍查詢。

日誌結構合併樹(Log-Structured Merge Tree,LSM Tree)則偏向把寫入轉成順序寫入,再透過背景合併整理資料,通常能提供很高的寫入吞吐量。

分析型工作又是另一回事。

如果查詢經常掃描大量資料、只需要其中幾個欄位,欄式儲存(Column-Oriented Storage)可能比逐列儲存更有效率。

這就是為什麼「同一個資料庫處理所有事情」不一定永遠合理。

複寫:多一份資料,不代表事情就只會更安全

複寫(Replication)是把相同資料保留在多個節點上,用來提升可用性、讀取能力或跨地區延遲。

看起來很直覺:多存幾份,不就好了?

困難的是資料變更。

如果一筆資料剛剛寫進主節點,其他副本還沒同步完成,使用者立刻從另一個副本讀取,就可能看到舊值。

如果多個地區都能寫入,衝突又該怎麼解決?

同步複寫通常能提供較強的耐久性保證,但會增加寫入等待;非同步複寫降低延遲,故障切換時卻可能面臨尚未同步完成的資料。

這些選擇沒有單一最好答案,關鍵是業務可以接受哪一種失敗與延遲。

分區:資料太大時,不是切開就結束了

當資料量大到一台機器不夠,常會進入分區(Partitioning),也就是把資料切到不同節點。

問題很快就會變多。

按照範圍分區,範圍查詢可能很方便,但如果新資料都集中在最新區間,某一台機器可能特別忙。

按照雜湊(Hash)分配,負載通常比較平均,卻可能犧牲原本的排序與範圍查詢能力。

即使雜湊很平均,也還是可能出現熱門鍵(Hot Key)。

例如某位名人突然被大量讀取,同一個鍵仍可能把單一分區壓垮。

所以設計分區時,必須同時理解查詢模式、熱門資料與重新平衡成本,不能只追求表面上的平均分配。

交易:ACID 不是有打勾就代表所有併發問題都消失

交易(Transaction)最大的價值,是把一組操作包成具有明確安全保證的單位,減少應用程式自己處理部分失敗與競爭條件的負擔。

但光知道資料庫「支援 ACID」還不夠。

會影響行為的是隔離層級(Isolation Level)。

不同隔離層級允許的異常不同。

像遺失更新(Lost Update)、寫入偏差(Write Skew)、幻讀(Phantom Read)等問題,常常只有在併發狀況下才會出現。

例如兩位使用者同時搶最後一個名額。

兩個請求都先讀到「剩 1 個」,接著都判定可以購買。如果沒有正確的交易與鎖定策略,就可能賣出兩個名額。

這種問題在低流量測試時完全看不出來,一到正式環境才發生。

一致性:關鍵在於使用者最後看到什麼

分散式系統裡,網路延遲與節點故障是日常條件。

因此一致性(Consistency)不是抽象理論,而會直接影響使用體驗。

使用者剛改完姓名,重新整理後卻看到舊名字,是讀己之寫(Read-Your-Writes)沒有被滿足。

使用者先看到新狀態,下一次又看到舊狀態,則可能違反單調讀取(Monotonic Reads)的期待。

對社群動態來說,短暫延遲可能可以接受。

對付款、庫存、權限或課程存取資格來說,錯誤的一致性假設就可能造成實際損失。

所以「一致性要多強」應該由業務規則決定,不是架構師單獨決定。

批次與串流:差別不只是快與慢

批次處理(Batch Processing)通常處理一批有邊界的資料,例如每天凌晨整理前一天報表。

串流處理(Stream Processing)則持續處理源源不絕的事件,例如付款、登入、點擊或感測器資料。

兩者都常用來建立衍生資料(Derived Data)。

例如搜尋索引、統計報表、快取與推薦結果,很多都是根據系統紀錄加工出來的衍生狀態。

這個區分很重要。

如果你能清楚知道哪一份是系統紀錄(System of Record)、哪些只是可以重建的衍生結果,架構通常更容易演進。

變更資料擷取:讓資料變化成為事件流

變更資料擷取(Change Data Capture,CDC)會把資料庫裡的新增、修改與刪除轉成事件,提供其他系統訂閱。

例如會員資料更新後,可以同步更新搜尋索引、分析平台或其他服務。

這比每個系統都直接輪詢資料庫更容易控制。

但一旦事件變多,就要面對順序、重送、重複處理與失敗恢復。

所謂「恰好一次(Exactly Once)」也不是一句設定就能得到,實務上常要靠交易輸出、去重與冪等操作(Idempotent Operation)一起設計。

分散式系統最麻煩的地方:你很難知道對方到底是慢,還是真的壞了

同一台機器裡,一個函式沒有回傳,問題通常比較容易判斷。

跨機器之後就不同。

你送出一個請求沒有收到回應,可能是:

封包沒送到。

對方收到但還沒處理。

對方已經處理完成,只是回應遺失。

網路暫時變慢。

節點真的故障。

這也是分散式系統為什麼比單機系統難。

逾時(Timeout)只能告訴你「等太久」,不能直接證明對方已經死掉。

所以重試如果沒有做好冪等性,甚至可能把同一筆付款重做兩次。

大型系統不是越分散越好

理解這些問題後,很容易得到相反的誤解:那是不是越早做分散式系統越專業?

不是。

分散式系統會引入網路故障、資料一致性、協調、監控、部署與維運等新的複雜度。

如果一台資料庫就能承受目前負載,把問題拆到十台機器不一定是進步。

架構要追求的是符合需求,而且保留合理的成長路徑。

如果你正在做第一版產品,可以延伸閱讀 從 App 點子到第一個可用版本:哪些事情要先驗證,哪些不要太早做。

一個課程網站,什麼時候會開始碰到這些問題?

小型課程網站一開始通常不需要複雜分散式架構。

但隨著功能增加,資料責任會慢慢變重要。

例如:

付款完成後,會員權限要不要立刻開通?

訂閱取消時,存取資格何時失效?

學員進度寫入失敗,可以重試嗎?

電子報、CRM、金流與網站之間,哪一份資料才是最後依據?

統計數據延遲五分鐘可以接受,但付款狀態可以嗎?

這些都已經是資料系統設計問題。

規模不一定要很大,才需要把資料責任想清楚。

判斷資料架構時,可以先問這七個問題

一、這份資料最自然的模型是關聯式、文件式,還是圖形關係?

二、主要工作負載是讀多、寫多、分析,還是混合?

三、資料要不要複寫?能接受多少延遲與資料遺失風險?

四、如果要分區,熱門鍵與重新平衡怎麼處理?

五、哪些操作必須在同一個交易裡完成?隔離層級真的足夠嗎?

六、哪些資料是原始紀錄,哪些只是可以重新計算的衍生資料?

七、節點、網路或第三方服務失敗時,系統會怎麼恢復?

這七題沒有唯一答案。

重要的是團隊知道自己選了什麼,也知道為什麼。

DDIA 最值得學的是如何做取捨

《Designing Data-Intensive Applications》橫跨資料庫、儲存、複寫、交易、分散式系統、批次與串流,內容非常廣。

但它一直反覆回到同一個思考方式:

不要因為某項技術流行就選它。

先說清楚需求與限制。

再理解不同工具提供什麼保證、犧牲什麼。

最後選擇最適合目前情境的方案。

大型資料系統最困難的地方,從來不只是讓資料存得下。

更難的是在故障、併發、延遲、成長與需求改變時,仍然讓資料保持可理解、可恢復,而且系統可以繼續演進。

常見問題

SQL 和 NoSQL 哪一種比較適合大型系統?

沒有單一答案。應先看資料關係、查詢模式、交易需求、一致性要求與擴充方式。大型系統也常同時使用不同資料模型處理不同工作負載。

資料量變大後一定要分庫分表嗎?

不一定。先確認目前瓶頸。如果索引、查詢、快取或硬體升級就能解決問題,過早分區反而會增加維護與一致性成本。

資料有複本就代表不會遺失嗎?

不一定。同步或非同步複寫、故障切換方式與備份策略都會影響耐久性。多副本可以提高容錯能力,但不能取代清楚的恢復設計。

ACID 交易是不是就能避免所有資料錯誤?

不是。還要理解實際隔離層級以及併發時可能出現的異常。部分問題只有在特定交易設計、鎖定或序列化保證下才能避免。

小型網站需要學分散式系統嗎?

不需要一開始就做複雜分散式架構,但理解資料一致性、交易、故障與系統紀錄仍然很有價值。這能幫助你知道哪些問題目前可以簡化,哪些不能靠運氣。

參考資料

Martin Kleppmann. Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems.
Martin Kleppmann and Chris Riccomini. Designing Data-Intensive Applications, Second Edition.
O’Reilly Media. “Designing Data-Intensive Applications.”
O’Reilly Media. “Reliable, Scalable, and Maintainable Applications.”
Martin Kleppmann. “Designing Data-Intensive Applications.” MartinKleppmann.com.

元氣站長
元氣站長

經營網路事業,賺取持續收入!
網路賺錢修煉,成就理想人生!