很多人第一次碰到大型系統問題,會先問:
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.
