短網址系統設計


Posted by 小小碼農 on 2021-07-11

並非每一個系統、公司都要做系統設計,如果是流量小、資料量小或客群少的系統,不做的話效率可能還更高,因為設計系統架構需要投入時間與人力。

也同樣的,不必一開始就設計高性能、高可用、可擴展的架構,容易導致:

  • 系統初期過於複雜,難以上手與維護
  • 專案項目落地時間也可能會拉很長

因為架構設計主要並不是「防範」後期變化,因此適當的預測然後做好準備,剩下的就根據使用情況來做調整才是比較適合的。

基本上在做系統架構的設計時,有非常多要考慮的點,例如可用性、擴展性、伸縮性、安全性、前端性能優化、資料庫層優化...等。以下先從兩個比較主要的性能做切入,可用性與擴展性。

詳細系統架構圖

  • 可用性(Availability)

    • 系統「無中斷的執行其功能的能力」,簡單說系統掛掉的時間愈少越好
    • 既然「無中斷」是重點,困難點也是在「無中斷」上,畢竟軟硬體都不可能做到完全無中斷,硬體會老舊,軟體會有 bug 或越來越複雜等
    • 比較常見的解決方法,核心都是透過「冗餘」,簡單說預備好多的軟硬體,在快要或發生狀況時,無間斷地切換過去
  • 擴展性(Scalability)

    • 簡單說,當投入更多資源時,對技術也就提出了更多、更高的要求。能夠快速適應並最大程度降低對現有系統的影響

雖然直覺上覺得說,就不斷擴增硬體,購買更多機器或伺服器做增援就好,但實際情況就是資金有限、要求不同等各種情況,不必有殺雞焉用牛刀的情形也能做到麻雀雖小五臟俱全的優勢。

以下說明:

有個大原則就是「視需求切分」,這就是服務分離,例如會員跟留言還有商品相關的邏輯都分開,甚至可形成各自的資料庫,各自獨立。

雖然這樣改善很多,但軟體也跟硬體一樣,不可能無止盡切分下去,舉個簡單缺點,越複雜的東西就越難維護、追蹤,同樣道理,切太多,變更複雜,反而更難維護。

水平擴展(Horizontal Scaling)
與其一個人練到 100 等衝頂裝打王,還不如找 100 個 50 等的人一起開團打王,(毒雞湯請下:一個人可以走很快,一群人可以走很遠嘔嘔嘔)除了有狀況可以互相支援外,也大幅降低單體的壓力。

同理,可以增加更多的「雙胞胎伺服器」來分擔負擔較重的部分,這就是「水平擴展」。

不過問題又來了,你跟你的 49 個隊友都是 50 等、裝備也一樣(可能統一買的團服),但你們終究是不同人,名字也不一樣,這時候要怎麼分配誰負責輸出、巡邏、補血等各種事,而每一件事也都會在打王途中發生。

同理,增加了一台一模一樣的伺服器,但他有他的 IP 位址,這時候使用者的請求要分配到哪台伺服器?誰要負責這次的補血?誰要負責這次的輸出?

除了固定頻率的輪流外,最好的就是有人專門負責分配,而這個人就稱為 -- 負載平衡器,可以當作是打王團的小隊長。

負載平衡器( Load Balancer):
分配流量給伺服器們。負責將流量(或是請求),幫忙分配到背後的一群伺服器上。

之前的流程,我們這邊可以簡化一下,當瀏覽器得到 DNS 給的 IP 位址,其實這個 IP 位址就是負載平衡器的 IP 位址,就是把任務(請求)委託給小隊長,小隊長再分給底下的小弟去做。

這樣做的好處:

  1. 滾動更新(rolling update)
    當伺服器有新版本要發布的時候,可以在背景建置好新版本的伺服器。完成後負載平衡器再將流量切換過去,然後關閉舊版本的伺服器。整個過程不需要任何停機時間。

  2. 易於管理故障的伺服器
    小隊長(負載平衡器)很負責,會定期清點、檢查隊員的狀態,當有隊員下線太多天,聯絡不上時,就會把他踢出公會或團隊(從流量清單剔除),再加入新的隊員(伺服器)。

而以上操作,都是無痛,使用者完全不會發現。

不過這樣做有個很大的問題。

簡單說,以網站伺服器為例,伺服器是有狀態的(stateful)。當使用者連線後,伺服器會藉由建立 session 來紀錄使用者的相關狀態(購物車內放了什麼、登入與否等),如果每次連線被分配到不同的伺服器,這樣使用者的狀態就會四散,造成一直被要求登入或是購物車內容每次都不一樣等問題。

如何處理複數伺服器的狀態是擴展性的一大挑戰。

因此,接著要處理「有狀態的伺服器」如何處理水平擴展,以剛剛的 session 為例:
同樣也直覺想到,同步所有伺服器,或是相同使用者永遠都連線到相同伺服器,兩種方式。

  1. 同步所有伺服器
    成本。問題就是成本,系統規模大的時候可能就沒辦法,同步的成本太高了。
  2. 相同使用者永遠都連線到相同伺服器
    黏性會話(sticky session),指的就是這種行為,乍聽非常合理,但仔細想,如果極端一點,每個重度用戶都被分配到同個伺服器,那一樣是分配很不平均,而且伺服器掛掉的話,這些資料還是得重新分配到其他台伺服器。

很簡單,跳脫一點來看,我們讓伺服器成為「無狀態的伺服器」。
簡單說就是把狀態放在使用者(瀏覽器)自己保存或是把狀態放在資料庫,然後使用者只保存一組 ID,兩種方式。

  1. 把狀態放在使用者(瀏覽器)自己保存
    將狀態全部放在 HTTP Cookie 回傳給使用者,每次連線時,使用者將保存的狀態帶給伺服器,若有狀態需要修改,由伺服器處理完後再交還給使用者保管,避免使用者自行修改狀態,但這樣做的話,狀態太複雜時,cookie 的容量不大,會放不下,傳輸的流量也會不斷增長!

  2. 把狀態放在資料庫,然後使用者只保存一組 ID
    這是目前很常見的方式,狀態被抽離到獨立的資料庫,使用者只需要在 cookie 保存一組“ID”。每次連線時將 ID 帶給伺服器,伺服器就會根據 ID 去資料庫取回使用者狀態。

    這邊我們採用第二種做法。以保存 session 來說,在資料庫的選擇上,像登入狀態這種相對短暫,甚至可容許遺失的資料(volatile data),我們可以放在快取(cache)資料庫。例如 Redis,特點是結構簡單、速度快。

    而像購物車內的商品這種較持久的資料(persistent data),則可以考慮保存在 NoSQL 的資料庫,例如 MongoDB,特點是資料結構彈性、易擴展。

雖然伺服器擴展越來越多,但終究還是向資料庫存取資料,所以接著來談談資料庫本身的擴展...

資料庫的擴展性

主從模式(Master-slave)

我們採用類似伺服器的做法,幫資料庫增加很多雙胞胎資料庫,並複製當下的資料過去,不過問題在後續想要修改資料時,不可能同時連線到所有資料庫做操作,這時就需要「主從模式」。

同樣類似負載平衡器的思維,先在一群資料庫中選擇一個作為 master,剩下的作為 slave,資料的變動一律透過 master 完成,再同步到所有 slave(不一定即時,視情況設定)。

  • master -> 寫入
  • slave -> 讀取

也就是所謂的讀寫分離。

也因為每一次的寫入或是資料更動對資料庫來說都是負擔,因此這種「讀寫分離」的模式特別適合讀取頻率大於寫入模式的系統,ex:購物網站。

大多時候是提供商品給使用者瀏覽,只要加入更多專門讀取資料的 slave 就可以大幅減輕資料庫負擔。

單點失效(Single point of failure)與故障轉移(Failover)

在這之前我們談論的較多是關於擴展性的方面,這邊要來分析另一個面向 -- 可用性的部分。

之前提過,可用性現階段我們暫時可視為「系統故障的時間越少愈好」,因此我們要找到容易故障的地方,或是什麼情況下,系統表現會是異常的。

又要搬出打王遠征隊了,大家打王都知道要打點,球類比賽也知道要打對方團隊中的弱點,藉此崩潰整體,或是盔甲武士的關節處(關節無法被包覆鎧甲,不然無法做彎曲),或是阿基里斯的後腳跟(阿基里斯腱)

舉這麼多例,無非是要去想像自己最脆弱的點會是哪裡,想像一個請求進來後,沿途會經過哪些元件?那個地方發生異常,整體就會崩潰,我們稱呼我們稱呼這個部分為「單點失效」,也就是整個系統架構中的弱點。

故障不可避免,重點是要盡快修復這種錯誤狀態,作法類似於之前討論水平擴展,透過負載平衡器自動管理故障的伺服器(原本一台伺服器,壞了就沒了),同理,唯一不同是,今天故障的是 master 的話,就從 slave 中選出(promote)一個當作新的 master,整個自我修復的過程是自動的(不需人力介入),稱為故障轉移(Failover)。一個系統越能從各種錯誤狀態中自我修復,就擁有越高的可用性。

資料庫快取

資料庫的速度幾乎是影響整個系統效能最大的因素,雖然目前有讀寫分離跟複寫機制,但終究還有很多可改進的地方,一樣的道理,將「存取資料庫」這個行為視為一個高成本的行為,會加重資料庫的負擔,資料的同步需要成本,無論是主從或其它設計模式都有其限制和取捨,因此拉遠一點看,我們有沒有辦法做到不存取資料庫?沒有辦法?那或是只要存取幾次就好?

這個解答的關鍵就是快取(cache),將曾經查詢過的結果保存起來(ex:使用者用過的短網址,通常會再加上一個有效期限,過期後快取結果就消失,資料對即時性越要求,有效期限就越短),每當需要查詢資料的時候,先找找看有沒有先前的查詢結果。若有找到就直接回傳。找不到才需要連線到資料庫。

簡單說,你常找的東西我先幫你放在旁邊,這樣你要用的時候就可以直接取用,不用大費周章的跑樓下、跑很多地方去要、去買。

加上快取機制後,大幅減輕了資料庫的負擔,畢竟我們做到了大幅減低對資料庫的存取,故障機率也會降低,也對整個系統的效能有顯著提升,可用性大幅提升。

最後總結目前流程:

(垂直擴展)升級硬體,直至無法升級或效益減低 -> 前後端與服務分離(網頁伺服器與後端的各種應用程式伺服器) -> (水平擴展)網路伺服器或應用程式伺服器都可做,並交由負載平衡器管理 -> (抽離狀態)因應同步成本太大問題,決定將狀態抽離至資料庫 -> (資料庫的擴展)讀寫分離、主從模式、資料庫複寫 -> (提升可用性)單點失效與故障轉移 -> 為資料庫加上快取,降低存取的頻率

(中場休息)

簡略系統架構圖

最後還有一個東西:

  • 非同步任務的處理

同步與非同步

同步(Synchronous),簡單說就是任務開始後需要等,等到任務完成才可以離開,相反,非同步(Asynchronous)就是任務開始後不用等,中間可以去做其他事,等任務完成再進行後續處理。

關於網路的事,大部分都是非同步在處理,畢竟一等,等到都以為網頁當機了還在等。

先來簡單解釋一些名詞:

  • 訊息(Message)
    訊息是任務內容的描述

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

  • 生產者(Producer)
    負責建立訊息

  • 消費者(Consumer)
    工作者的統稱,負責「消化」佇列中的訊息,相當於廚房內場。

  • 工作者(worker)
    相當於廚房內場中的每個廚師。實際應用中,生產者常常是應用程式伺服器,將繁重的任務轉交給另外建置的工作者(worker)伺服器。這些伺服器作為消費者,專門負責處理佇列中的訊息。

流程大概是這樣:
生產者接收任務內容 -> 生產者根據任務內容建立訊息 -> 送出訊息到訊息佇列 -> 消費者消化(處理)儲列中的訊息

優點:

  • 非同步處理,優點顯而易見,生產者無需等待,訊息送出後馬上又可以去做其他事

  • 生產者、消費者互不影響,職責分開,任一方都可彈性擴展(ex:增加伺服器、分別採用不同技術)

缺點:

  • 系統複雜度提高,維護與追蹤都變困難

  • 訊息可能不會照發送順序完成,晚送出的 B 反而比 先送出的 A 先回來,如果對訊息順序有要求,就須注意

簡單說,非同步有很多優點,但缺點也同時變多,永遠設想最糟情況做打算。

短網址系統

短網址設計圖

左邊是查詢短網址用,右邊是申請短網址用。

簡單說,把又臭又長的網址,縮短成很短很短的網址,好用於分享或紀錄。

原理:

其實短網址並不是真的變成原本的網址,只是伺服器就由演算法縮短了原本的長網址,發給你短網址的同時,將原本的長網址儲存進資料庫,當你輸入這個短網址時,伺服器就會去資料庫幫你找對應的長網址,再幫你轉址到正確頁面。

優化方向:

Key Generations Service

預先產生一些隨機且唯一的 key,類似於 ID,然後存放到專門的 key 資料庫,當需要產生新的短網址時,就來這個資料庫拿取然後回傳,如此可以加快速度,也能避免短網址重複。

缺點:
雖然不重複,卻無法保證轉化後的短網址長度,如果是從 0 開始(xxxx.com/0....xxxx.com/20....xxxx.com/54642)慢慢累積,短網址的長度就不能保證一樣。

最後總結:
每一個決定都有取有捨,不一定哪一個特別好、特別適合,只是要針對遇到的情況做調整,軟硬體或是技術都是,這邊每一個項目都可以再細講下去,都是一個不小的領域,但這邊我們專注討論在架構設計,所以就不細究每一個項目,而是點到即可。

參考資料來源:
系統設計101—大型系統的演進(上)
教你怎麼實現縮短網址功能
短網址(short URL)系統的原理及其實現


#web-structure-design







Related Posts

淺談 React Fiber 及其對 lifecycles 造成的影響

淺談 React Fiber 及其對 lifecycles 造成的影響

轉職前端工程師之路 Day4

轉職前端工程師之路 Day4

[Day 06] - Vault Built-in help and Auth

[Day 06] - Vault Built-in help and Auth


Comments