系統設計101—大型系統的演進(下)

System Design 101, Part 2

YH Yu
後端新手村
11 min readJul 5, 2019

--

回顧一下上篇的系統架構:

以一個購物網站來說,這樣的架構已經撐得起一定流量了。但仍有幾個常見的議題,想要在本篇進一步討論。分別是:靜態資源的擴展性、非同步任務的處理、全文檢索的效能,以及非常重要的——安全性!

靜態資源的擴展性

靜態資源指的是像圖片、影片這類型的檔案。它們通常變動的頻率相對較低、需要更多傳輸流量,以及多位使用者會下載同一份檔案。

舉例來說,有位商家上架了一個熱門商品,並在商品介紹中加入了一堆宣傳照片。假設這個商品有上萬名使用者瀏覽,若每個人都來伺服器下載這些照片。可以想像,對系統將是一個非常可觀的負擔!

還記得先前介紹資料庫的擴展性時,我們引入了快取(cache)來降低重複查詢資料庫的成本。這邊也是類似的做法,只要有使用者請求過某張圖片,我們就加到快取,讓其它使用者可以重複利用而不用再從伺服器下載。

靜態資源的快取,已經有許多供應商提供現成的服務,稱為內容傳遞網路(CDN)。收費上通常都是用多少付多少,並且保證了非常高的可用性!

內容傳遞網路(Content Delivery Network, CDN)
是由一群數量龐大、遍佈全球的“快取伺服器”組成的分散式網路,根據請求的來源,選擇距離最近的伺服器來提供內容。當請求的資源不在快取時,會連線到來源(也就是我們的伺服器)獲取資源,並保存到CDN快取。

將靜態內容分離,並使用CDN來負責傳輸後,我們的架構如下:

Note: 這邊將CDN簡化成一個圖示,但它其實是一群遍佈全球的快取伺服器

使用者首先透過我們的伺服器,上傳資源到特定的儲存空間(常常也會採用雲端服務,例如AWS S3)。之後當有人需要下載這個資源,一律透過CDN而不是我們的伺服器。在接到下載需求時,若有快取過這個資源就直接提供,否則CDN便會從我們的儲存空間下載給使用者並且快取。

非同步任務

何謂同步(Synchronous)、非同步(Asynchronous)任務?簡單來說,同步任務就是任務開始後我們“要等”,直到任務完成後才能離開。而非同步任務則是我們“不用等”,期間我們可以先去做別的事,等任務完成後再進行後續處理。

以餐廳外帶為例子,同步的方式就像是一間沒有櫃檯的餐廳,客人直接和廚師點餐。在料理期間必須一直等著,直到餐點完成後才離開。如果餐點需要做很久,就會發現已經點餐的客人,會花上很多時間在等待。而且當廚師忙不過來時,還沒點餐的客人可能根本沒辦法點餐!

非同步的方式則像是有櫃檯專門負責幫客人點餐,客人送出一份“點菜單”後就離開,點菜單會在櫃台排隊,廚師根據它們烹飪餐點。在餐點完成後發出通知,客人可以隨時來拿。

可以想像,比較耗時的餐點就很適合用非同步的方式處理。即使在晚餐的尖峰時刻,也不會發生有客人沒辦法點餐的情況。廚房還可以偷偷安排更多的廚師維持來出餐速度而不會影響到客人的用餐體驗。

這些角色在系統設計中的名稱是:

  1. 點菜單稱為訊息(Message),為工作內容的描述
  2. 送出點菜單的客人稱為生產者(Producer),負責“建立”訊息
  3. 櫃檯,也就是點菜單排隊的地方,稱為訊息佇列(Message Queue)。佇列是一種“先進先出”的資料結構,意即訊息會依照到達的順序來處理。
  4. 提供料理的廚師稱為消費者(Consumer),負責“消化”佇列中的訊息

訊息佇列(Message Queue)
一種非同步的架構,生產者根據任務內容生成(建立)訊息,再放到訊息佇列中排隊,由消費者消耗(處理)排班中的訊息。

在實際應用中,生產者常常是應用程式伺服器,將繁重的任務“轉交”給另外建置的工作者(worker)伺服器。這些伺服器作為消費者,專門負責處理佇列中的任務。

採用這種架構的好處有:

  1. 非同步處理,生產者無需等待:
    耗時的任務,例如需要大量運算(e.g. 影音轉檔),或由多個步驟組成的服務(e.g. 商品下單)。生產者知道請求已經被接受(成功送進訊息佇列)後,就可以馬上進行其它的操作。
  2. 解耦,讓生產者、消費者互不影響:
    生產者和消費者可以各自彈性擴展,甚至採用不同技術來因應任務需求。例如NodeJS伺服器作為生產者,負責接受訂單,送到訊息佇列後,再由作為消費者的Java伺服器處理。假設訂單多到Java伺服器處理不過來的時候,只需要單方面擴展更多Java伺服器來加快處理速度。
  3. 平衡流量,維持尖峰時刻高可用性:
    即使實際的工作內容再繁重,生成訊息及加到佇列本身是很輕量的動作。尖峰時刻我們依然可以接受大量的請求,由消費者依序處理。而不會發生伺服器過度忙碌而沒有回應,導致損失訂單的情況。

導入訊息佇列,用非同步方式處理耗時的請求後,我們的系統架構如下:

既然有這麼多好處,乾脆都用非同步的方式處理所有請求?不盡然,還記得在系統設計中,一切的選擇都是取捨!現在來看看可能的缺點或限制:

  1. 系統複雜度提高
    原本我們以同步方式處理請求,整個過程相對單純。但現在被拆分成三個角色,就必須考慮到每個環節都有可能發生失敗。若不只一個訊息佇列,甚至彼此關聯。那麼複雜度會提升的更快,增加監控、追蹤的難度!
  2. 訊息可能不會照發送順序完成
    假設訊息佇列中,依序有A和B兩個請求,分別由兩個不同的消費者處理,結果有可能B的處理速度比較快,反而比A先做完。若對處理順序有要求(例如B會依賴A的結果),就需要特別留意。
  3. 同樣的訊息被重複處理
    通常訊息佇列會讓處理失敗的訊息重新入列,來保證每條訊息都有確實完成。但試想一個例子,有則訊息是要扣款,當某個消費者收到訊息且執行扣款後,卻在回報前掛掉。佇列以為訊息沒有完成就將它重新入列,導致又被發送到另一個消費者而再度扣款。像這樣不允許訊息被重複處理的場合,也需要額外設計。

現今主流的訊息佇列技術(e.g. RabbitMQ,Kafka)各有不同的優缺點。根據選擇的技術和實作方式,以上情況可能很簡單,也可能很複雜。重要的是,永遠都要設想到最壞的情況,依據我們的需求適當地應對。

全文檢索的效能

全文檢索(Full-text search),簡單來說就是針對整個資料庫中的文字,找出符合特定字詞的內容。例如使用者在購物網站的搜尋列中,用關鍵字尋找商品。這對一般的資料庫而言是很耗時的工作,而搜尋引擎就是專門用來處理這樣的需求。

搜尋引擎(Search Engine)
可視為一種特殊的資料庫,用非常“特化”的方式來儲存資料。為了加速全文檢索的效能,大多採用倒排索引(Inverted index)來保存資料。

什麼是倒排索引呢?假設有三篇文章,下圖左邊為一般關聯式資料庫的保存方式。若要搜尋所有包含“apple”關鍵字的文章,恐怕只能掃完整張表格才能得到結果(雖然這個例子中只有三篇文章,但若有上萬篇呢?)。

而右邊就是倒排索引的保存方式,所有文章被打散成一個個單字,像字典一樣排序好,並紀錄在哪裡出現過。由於單字就是索引,一樣是搜尋“apple”,可以很快速地知道在doc 1和doc 2有出現過。

除了核心技術的倒排索引外,各家搜尋引擎會再加上種種優化搜尋的功能。例如支援單複數、同義詞的近似搜尋,或是將多個關鍵字的順序也列入考慮等等。

加入搜尋引擎處理全文檢索後,我們的系統架構如下:

安全性(Security)

最後,隨著系統規模擴張,安全性也是非常重要的課題!由於這是非常專業的領域,我們會著重在大方向的觀念,以及實務上常見的做法。

首先要思考的,就是系統中的元件(component),例如網站伺服器、資料庫等等,哪些要“暴露在外”,也就是讓使用者可以接觸到的?

這類型的元件數量應該越少越好,理論上除了少數的進入點外,系統的其它部分對使用者來說都要是封閉的,而這些進入點需要加上額外的驗證機制。我們希望系統像是一座堡壘,而不是自由穿梭的園遊會!

要如何做到呢?最直接的方式就是將所有元件都放到一個私人網路(private network)中。只有同一個網路中的元件可以互相連線,就像在宿舍用內網玩對戰遊戲那樣。此外,我們還要幫每個元件設定好防火牆,讓它們只接受特定的連線方式(例如指定來源或連結阜),來進一步提升安全性。

至於哪些元件適合當作進入點開放給外部使用者?負載平衡器就是一個非常好的選擇。我們讓它(而且只有它)擁有對外公開的IP地址,使用者只能透過負載平衡器進入到我們的系統。

若整個系統是放在雲端(例如Amazon的AWS),這個封閉網路就稱為虛擬私人雲端(Virtual Private Cloud, VPC)。現在我們的架構如下:

Note: 我們假設靜態資源的儲存空間是AWS S3,雖然它不在VPC內,但可以透過AWS的設定去保護

使用者可以訪問DNS詢問IP地址、從CDN下載靜態資源,以及發送請求到負載平衡器。除了這三者外的所有元件,對使用者來說都是封閉的。

將系統封閉起來後,目前最需要保護的路徑,就剩下使用者到負載平衡器這一段。一般都會採用HTTPS(相對於HTTP更安全)的方式來保護傳輸過程。

為什麼不讓所有的連線都透過HTTPS就好了呢?因為透過HTTPS傳輸的運算成本比較高(需要加解密),所以只在VPC外採用,一旦通過了負載平衡器的驗證,進到內部後就可以簡單使用HTTP來提升傳輸效率。

總結

在上篇中,我們從一個簡單的架構開始,針對系統的擴展性和可用性,依序介紹了垂直擴展、服務分離、水平擴展(狀態抽離),以及資料庫的複寫、讀寫分離、故障轉移與快取。

在本篇中,我們把靜態資源交由CDN處理、讓比較耗時的請求,透過訊息佇列用非同步的方式來完成,以及導入搜尋引擎加速全文檢索的速度。最後,我們將架構中的大部分元件封閉在VPC中,只開放少數元件作為使用者的進入點,並採用HTTPS來保護傳輸過程。

到此,這個系統架構的“101”算是告一段落了。本文以一個廣度優先的方式,為系統設計做一個比較基礎的介紹。主要目的是希望可以建立一個綜觀全局的big picture,來作為後續深入各個部分的深度的基礎。

需要特別強調的是:

  1. 擴展資料庫的複雜度、成本遠比應用程式伺服器來得高。無論何時,都要將存取資料庫當作高成本的行為。能在伺服器處理的商業邏輯不要放到資料庫,非不得已不要使用預存程序(Stored Procedure)。頻繁的查詢也要透過快取來減輕資料庫的負擔。
  2. 設計系統時,預先考慮到將來的擴展性(例如伺服器的狀態是否容易抽離)是必要的。但系統架構的演進是一個迭代的過程,不用一開始就把系統架構規劃的太大。事實上大部分的系統,使用者的數量根本沒有機會到達想像中的規模…系統架構越大,複雜度、開發和經營的成本就越高。在初期能快速地推出產品,並從市場獲得反饋也是非常重要的。
  3. 系統設計中每個選擇都是在“取捨”。雖然本文中帶出了很多元件、技術,但是否適用於每種系統、哪些技術應該優先導入等等,其實都沒有一定。重要的是知道系統當下的瓶頸在哪裏、有什麼方法可以解決,導入的成本和風險等等。

What’s Next?

在對系統設計有一個基本的認識後,我們已經有能力可以看懂一些現實的案例。以AWS架構中心為例,它示範了各種常見應用(e.g. 電商、串流、數據分析)的系統設計。可以發現,整體的思考脈絡和本文都是很類似的。不外乎就是這個應用會有哪些需求、需要考慮的限制,可以用什麼技術(大部分本文都涵蓋到了)解決等等。除了AWS外,其它還有非常多的資源可以參考,例如這邊就有一份整理好的清單。

最後,本文若有錯誤也請不吝指正,這才是撰寫這篇文章最大的目的!

如果這篇文章對你有所幫助的話,歡迎拍手讓我知道,最多可以拍50下喔👏👏

--

--

YH Yu
後端新手村

Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better.