我們為什要這樣設計資料系統?
在金融、電商、社交平台等產業裡,多數的應用系統屬於資料密集型 (data-intensive),常遇到的瓶頸在於資料量級、資料複雜度、及快速的變化性,而不是 CPU 的處理能力。
在非功能性需求方面,對大多數軟體系統來說都極為重要的三個面向有:可靠性 (Reliability)、可擴展性 (Scalability)、可維護性 (Maintainability)。
可靠性 (Reliability)#
系統出錯時仍能正常運作。
故障 (faults) 可能來自硬體故障、軟體錯誤、人為失誤:
- 硬體故障:比如硬碟壞軌、記憶體掛點、停電。解決方法通常是幫硬體元件添加冗餘 (redundancy),類似備援的概念。當一個元件陣亡,冗餘元件可以馬上取代它,比如資料中心 (datacenter) 可能規劃柴油發電機和電池作為備用電源。
- 軟體錯誤:硬體故障經常是相互獨立的,很少出現大量硬體同時故障。但軟體中的系統錯誤,常常因為節點之間有相互關聯,導致更多的系統故障。例如:一個 Linux kernel 裡的 bug 導致許多應用程式當機、一個程序 (process) 吃掉了許多共享資源如 CPU、memory、磁碟空間、網路寬頻。軟體系統的故障沒有快速解法,但我們能仔細審視系統架構間的互動、做全面性的測試、監測及分析正式環境的系統行為,像是檢查 message queue 中的訊息量是否有異常。
- 人為失誤:人無法做到萬無一失,難免會在維運系統時設錯 config。我們可以將容易出錯的地方隔離出來,使用真實資料在沙盒 (sandbox) 等獨立環境做探索和嘗試。此外,建立可以快速回滾 (roll back) 的機制、或是滾動發佈程式碼,都可以降低意外故障的影響。
可靠性有多重要?可以預測並優雅應對故障的系統稱為容錯 (fault-tolerant) 或彈性 (resilient) 的系統。對一個平凡 user 來說,如果他將所有與家人的照片和影片存在 APP,結果它資料庫壞了,那很令人崩潰的! 🤯
可擴展性 (Scalability)#
系統能處理增長的負載 (load)。
當負載增加,資源不變 (CPU、記憶體、網路寬頻),系統性能會有什麼影響?
當負載增加,若要保持系統性能不變,需要增加多少資源?
- 負載:依不同系統架構,可以用不同的負載參數 (load parameter) 來描述負載,例如:對 server 的每秒請求次數 (Requests per second)、資料庫的讀寫比例、購物網站的同時活躍用戶 (simultaneously active users)、快取命中率 (cache hit rate)。
- 性能: 指標如吞吐量 (Throughput) 是指單位時間內系統能處理的工作量,像是批次處理系統 Hadoop 每秒可以處理幾筆資料?或是處理特定大小的 dataset 需要多久時間?對於線上系統,服務的回應時間 (response time) 通常更重要,客戶端從發出請求到收到回應需要多久?
面對增長的負載,常見的應對方式有 垂直擴展 (scale up, vertical scaling) 即升級到更強大的機器上,像是將 Oracle 資料庫升級為更高階的 Exadata;以及 水平擴展 (scale out, horizontal scaling) 即將負載分散到多台小機器上,比如 MongoDB 使用分片 (sharding) 將資料分到不同節點。
可擴展的系統可以按需求增加更多處理能力,在高負載下維持系統的可靠性。事實上,並沒有所謂既通用、又適合所有應用的可擴展架構 (magic scaling sauce)。大規模運行的系統,它的架構通常是依照其特定應用的需求去量身打造,而要考慮的因素有很多,包括讀 / 寫量級、資料儲存量級、資料複雜度、回應速度要求、存取模式等等,目的都是讓系統在長大後依然不會被流量壓垮。
回應時間是客戶端所看到的,它包含了網路延遲 (network delay)、排隊延遲 (queueing delay)、和處理請求的實際服務時間 (service time)。
當請求處於等待服務的狀態,這段等待被處理的期間稱為延遲 (latency)。
我們可以用中位數 (p50) 來知道 user 一般需要等多久。如果觀察異常值,假設第 99 百分位數 (p99) 的回應時間是 1 秒,表示有 99% 的請求回應時間不到 1 秒、有 1% 超過 1 秒。
百分位數常用於服務水準協定 (SLA),定義了客戶期望的性能,例如一個正常運行的服務平均回應時間應該低於 200 毫秒並且 p99 在 1 秒內。
高百分位數回應時間 (尾延遲tail latencies) 會直接影響 user 對服務的體驗。
大部分原因來自排隊延遲,由於 server 並行處理能利有限 (比如 CPU core 數的限制),所以只要有幾個慢請求 (slow request) 就會讓後面的請求塞車,這有時稱為隊頭阻塞 (Head-of-line blocking)。
可維護性 (Maintainability)#
系統能長期被理解與修改。
即使一個系統很穩又能撐住流量,但如果缺乏可維護性,很快就會變成一堆技術債。設計系統時可以注意的方向包括可維運性、簡單性、可演化性:
可維運性:讓維運團隊更容易保持軟體系統穩定運作,比如利用視覺化的監控儀表板更方便觀察系統的健康狀態,並且能有效管理它。
簡單性:盡量降低系統複雜性,讓新接手的人更容易理解系統。複雜性可能來自 code 雜亂無章和重複、module 緊密耦合、混亂的相依性、不一致的命名和術語、為特例而做的取巧做法,這些都會讓一個軟體專案變成大泥球 (big ball of mud),它們在短期看似有效,但長久下來會使維護變得很困難,像是開發人員只做一個小改動,卻造成多處連鎖錯誤。
我們可以利用抽象 (abstraction) 來消除「意外的」複雜性,一個好的抽象設計,將大量實作細節隱藏在一個乾淨易懂的表面下。 例如:SQL,它隱藏了複雜的磁碟和記憶體資料結構、與來自其他客戶端的併發請求等。可演化性 (可延展性、可修改性、可塑性):讓開發人員在未來能輕鬆改進與調整系統。很少有永遠不變的系統,它可能會遇到各種新需求,像是擴充新功能、法規變動、新舊平台交替。與前面提到的簡單性相關,簡單易懂的系統通常更容易修改以適應不斷變化的需求。此外,敏捷 (Agile) 開發模式、測試驅動開發 (TDD) 和重構 (refactoring) 等方法,也能帶來幫助。
可維護性的本質是要讓工程、維運團隊朝無痛維運邁進。在做軟體設計時多加考慮,盡量最小化系統維護的痛苦,它也許不會讓系統立刻變好,但會影響到未來是否需要花更多成本去修補。
我覺得可靠性像是底線,系統必須維持基本的穩定運作;可擴展性像是長大的能力,可以承載更大的世界;而可維護性則是壽命,大家都希望它健康又長壽。在設計架構和寫程式時,經常提醒自己這些原則,才能讓系統變得更好。
Reply by Email
