系統設計五大心法:水平擴張、快取、非同步、避免單點故障、監控
2023年12月31日
要做好系統設計,或是在系統設計面試中有突出的表現,必須深入細節。雖然說面對不同的系統,會需要針對需求與特性,去做特別的調整,不過有許多心法是在各類系統都適用的。在這篇文章,我們會談 5 個系統設計心法,分別是水平擴張、快取、非同步、避免單點故障、監控
水平擴張 (Horizontal Scaling)
第一個要談的心法是水平擴張。要擴張一個系統通常有兩種方式,一種是垂直擴張 (Vertical Scaling),也就是去提升單體機器的能力,例如增加 CPU 的核心數,或是增加記憶體,或是把硬碟升級成 SSD。 但是單一硬體總是有其極限所在,因此一直升級到最後仍會遇到瓶頸。這也是為什麼多數時候,在系統設計時,會選擇水平擴張 (Horizontal Scaling)。
水平擴張做的事情,就是透過複製或拆分伺服器與資料庫,來分散系統負載的壓力。而要做到這件事,會透過均衡負載器 (load balancer) 來實現。當請求進到系統時,均衡負載器會將請求轉去給不同的伺服器處理。這也是幾乎在所有系統設計面試中,平衡負載器基本上一定會在你的系統設計圖當中。
常見的均衡負載方式包含隨機 (random) 分配給伺服器、輪詢 (round robin) 分配、最少連接 (least connection),以及一致性雜湊 (consistent hashing) 等各類方法。在實務執行上,通常也會加上權重 (weight),每種方式都有其要考量的點,推薦大家可以多深入去理解不同方法的取捨。
當談到水平擴張,就不能不提「狀態」這件事。有狀態會導致服務被受限,因為請求打過來,只能去找具有狀態的那個伺服器;這時如果具有狀態的伺服器的單點掛了,要處理就比較麻煩。這也是為什麼在水平擴張的系統設計中,會讓各個微服務無狀態。當你在設計系統,或是在面試時被面試官問到要不要有狀態時,你的雷達要記得響起來,然後說「不」。
當選擇無狀態的設計,某一台伺服器掛了,均衡負載器可以把請求導向其他台伺服器。在實務上,我們經常會使用心跳檢測 (heartbeats) 來判斷伺服器是否還正常運作。簡單來說,就是會定期發心跳請求給伺服器,如果伺服器在一定時間沒有回應,則會被判斷為出問題,這時就會把請求轉去備用的伺服器。這做法也適用在均衡負載起本身,來確保如果均衡負載器掛了,隨時有備用的均衡負載器可以接手。
快取 (cache)
所謂的快取是指將運算完的結果,存放在可以更快取得的地方。當使用快取時,請求資料時會比從主要儲存位置來得快,同時可以免去重複已經做過的運算。因此,快取可以達到兩個顯而易見的好處 — (i.) 加快響應速度 (ii.) 避免重複運算,藉此能提高資料源頭的乘載量
在實際的應用程式開發中,通常是會採用多層快取的策略,來大幅增加應用程式的併發量與響應速度。舉例來說,可以在資料庫前放一個快取,這樣伺服器可以直接跟快取拿資料,這會比跟資料庫拿還快,而且可以降低資料庫的負載;同時我們可以在 HTTP 層做快取,這樣客戶端要跟伺服器端請求時,可以透過快取來加速,同時減少伺服器的負載。甚至我們可以直接在客戶端 (例如瀏覽器) 快取,更進一步加速響應速度。
雖說快取好處很明顯,但也有必須注意的問題。在軟體工程界有個名言是「電腦科學只有兩件困難的事,一個是快取失效管理,另一個是命名 There are only two hard things in Computer Science: cache invalidation and naming things」。快取失效 (cache invalidation) 的意思是指判斷什麼時候不該繼續用快取,而是要再去源頭取資料,然後更新快取 (revalidate cache)。這個困難點在於,每個應用有不同的屬性,所以沒有一個通用的標準,需要工程師隨著應用的特性去做判斷。
除此之外,在系統中加入快取時,千萬不要把快取當成資料庫。在快取背後,務必要有一個最終的資料來源 (source of truth),因為你沒辦法保證負責快取的機器會不會故障或出問題,因此要有帶著「快取的資料可能會丟失」的角度思考,這時就會有意識要有一個最終的資料來源。
非同步 (async)
在系統與程式設計中,當發起某個請求或操作時,可以是同步 (synchronous) 也可以是非同步 (asynchronous) 進行。如果是同步的方式,該直到操作完成,操作會阻塞其他部分的進行。反之,如果選擇非同步的方式,就不需用等該操作完成,也可以避免阻塞的問題。
在討論非同步前,有兩個概念要先釐清,一個是編排 (orchestration),意即依賴某個中樞的指揮,來驅動整個系統的流程;另一個是協同 (choreography),意即定義好系統中各個部分的職責,但具體要怎麼運行,則由該部分自行負責。
如果選擇編排的方式,可能遇到的問題會是,當中樞節點出問題,需要有相對應的處理機制,不然會導致整個系統的運作出問題;而選擇用協同的形式,雖然可以有效把各部分與中樞解耦,但是需要有一個額外的系統來監控,確保每個部分的運作都是良好的。從系統的角度來看,除非是關鍵的請求,不然現在業界會更偏向協同的模式。
從協同的角度進一步討論,最常見的非同步模式,就是透過消息對列 (message queue),發送方 (producer) 只需要把消息 (message) 送往消息對列,讓接收方(consumer) 依序從消息對列中取出訊息並處理,透過這種方式達到非同步處理。此架構不僅能達到解耦,也可以輕易擴展,只需要在發送方、接收方分別增加機器,就可以擴展。
總的來說,想要讓系統解藕,同時能輕易擴展系統,使用消息對列進行非同步處理,是非常有效的方法。
避免單點故障
所謂的單點故障 (Single Point of Failure,簡稱 SPOF) 指的是系統中的某個單一節點失效時,導致整個系統無法運作進而崩潰。這對於系統來說,最直接的影響就是可用性 (availability) 會降低,而對任何系統來說,這都是不樂見的事情。也因此,在系統設計時,這個概念特別重要。
在往下講之前,先來了解一下可用性,以及一些關於可用性大家需要知道的概念。假如要用白話來理解,可用性是指系統可供使用的時間;通常我們會用 系統可供使用的時間 / 總時間
這個比例來衡量。舉例來說,業界常用的 99.99% 可用性,即是指一年只有 52 分鐘系統是不可用的 (365 天 x 24 小時 x 60 分鐘 x 0.0001 會是約 52 分鐘)。
在業界,大家常會看到 SLA (Service Level Agreement) 即是針對可用性的協議。而常聽人說的「幾個 9」則是在描述 SLA 上的比例。以上面的 99.99% 來說,會被說四個 9;如果是 99.999% 則會被說五個 9,往下以此類推。
在業界通常違反 SLA,都是會有相對應的賠償,舉例來說,假如你用某個 AWS 的服務,SLA 寫四個 9,結果該年 AWS 掛掉超過 52 分鐘,AWS 的合約上會有他們相對應要做的賠償。身為設計系統的人,你肯定不想系統掛掉超過 SLA 協議的規範,不然系統掛掉造成客戶的損失,也是需要自己來承擔。
要做到高可用,避免單點故障,就會是非常重要的。要避免單點故障,有幾個重要的概念,首先要避免系統中的模塊有過高的相互依賴,減少依賴就能避免「一個地方壞掉,造成整個系統崩掉」的狀況。
然而,假如有某些關鍵依賴,就是沒辦法不依賴,這時可以則務必要避免某個模組有熱點,意即被大量地請求,先前提的均衡負載就能來做到這件事。除此之外,有備案也是非常重要的。常聽人說的冗余 (redundancy) 即使如此,如果某個模塊掛掉了,備案可以馬上接替。
監控 (monitoring)
監控在真實世界的系統中,是非常重要的環節,其核心在於能預測並協助系統在問題發生前主動發出警告,讓問題發生時相關人員能立即處理。
監控要設計的好,需要確保故障的檢測、警告的發出,以及問題定位的容易程度都能被照顧到。設計的好的監控,將讓維運的人,能快速上手,並能低門檻地接手 oncall 的任務。
針對上面提的點,我們一個個來談,首先是故障的檢測,要能夠有效檢測故障,需要定義好明確的指標,該要追蹤的都有追蹤到。在這個環節如果有缺失,很可能會導致在問題發生時,系統沒有辦法立即發出警告,導致出問題了沒人知道。在準備系統設計面試時,推薦針對每個題目,都練習發想在該系統下,有什麼是一定要監控的指標。
接著是警告的發出,在設計監控時,要確保過多的警告噪音 (無效的警告)。因為噪音不僅會造成團隊的疲勞,還可能導致真正的問題被忽視。就像狼來了的故事,如果野狼一直沒出現,大家就會開始覺得警告是無效的,這樣等狼真的出現時還以為是沒事,那將錯過問題的黃金處理時間。
因此,在系統設計中,適當的警告閾值設計和過濾機制,都是是至關重要的,它們能協助減少錯誤警告發出。在實務上,通常會針對每次發出的無效警告,都進行審查,確保未來同樣的無效警告會被過濾掉。
最後是問題定位的容易程度。當監控系統發出警告說有系統錯誤,這時開發與維運人員,需要第一時間定位問題,並處理問題。有效定位出問題,是能夠協助加速處理問題的關鍵。要有效定位,就需要確保分級有做好,在實務上,會需要在應用層 (例如 QPS、可用性的監控)、系統層 (例如 CPU、RAM 的監控),以及基礎設施層 (例如機房與網絡的監控),都有監控。
舉例來說,如果在基礎設施層沒有監控,很可能應用層的監控發出警告,但因為問題不是出在應用層,所以應用層排查不出所以然,這會浪費很多不必要的時間。
總的來說,好的監控設計,應該要能有效檢測出錯誤,同時讓開發者與維運人員,能輕易定位出問題所在,協助開發者迅速發現線上程式碼的問題。此外,要避免發出不必要的噪音,從而提高報警系統的準確性。