回顧 AppWorks School System Design 的課

Joel Zhong
AppWorks School
Published in
11 min readMay 27, 2022

前情提要

當初會知道這門課是因為主管提到有 AppWorks School 要開系統設計的課, 問我有沒有興趣, 這引起我的好奇心, 畢竟我知道從 AppWorks School 出來的學生通常都有一定的水準, 而我已經工作近 10 年的時間, 也不禁問自己到底系統設計是什麼? 什麼才是正確的設計? 這不是我平常工作就一直在面對的事嗎? 我是抱著好奇的心來參加這門訓練的

這系列的課程主要由 Arthur 老師先跟大家來探討一些經典案例, 像是短網址、Facebook Newsfeed、Yelp、Uber 等等, 看他們是怎麼做的, 然後隔週再穿插來自同學的分享以及請來業界的前輩來分享他們自己公司的案例

理解系統需求及目標是什麼

當我們看到一個需求背後的商業價值之後, 更重要的是要意識到背後可能會發生什麼問題, 因此我們必須要問需求端針對系統有沒有額外的限制要求

舉例來說:設計一個短網址系統, 功能有

  • 給定一個 URL, 產生一個短 URL 能連結到相同網站
  • 客製化短網址名
  • 預設或客製化過期時間

其實上述的些列表這叫『功能需求』

但我們要更關心的是藏在背後的『非功能需求』, 像是

  • 服務必須極高可用性: 那我們是不是就要開始定義 SLA、SLO, Failure rate 要多少? 要如何設計才能盡可能達到要求?
  • Create 與 Redirect 時間必須足夠短: 也就是 Response time 要夠低, 甚至是要多低? 要採取哪一種快取策略或工具?
  • 如何阻止被濫用或攻擊: 那我們就要想哪些方法或工具可以解決這個問題? 要不要額外追加網路限速器? 又或是最外層要不要再一層 CDN?

關於非功能需求的描述可以看 Wiki 的解釋, 其中吸引我目光的是『情感因素』也是非功能需求, 能夠描述的系統行為我們稱『功能需求』之外, 能夠描述的系統特性或是限制要求我們稱『非功能需求』, 又或者是只要是需求端說得出來的功能都叫『功能需求』, 其他的就歸類在『非功能需求』

設計前要對系統擴展的估算有一定的敏銳度

知道了非功能需求之後,就要開始思考系統限制、承載量以及成本, 這就涉及到面對擴展性所相關的數字 要有一定的認知, 這些數字會影響我們對於成本的估算; 影響決策, 還是以短網址系統為例

假設我們得到了相關數字:

  • 每月寫入 500M 新 URL
  • 保存期限 5 年
  • 一組 URL 500 Byte
  • 寫與讀的比率為 1:100

接著就要再推導出

  • 讀的 RPS 是多少?
  • 需保存的 URL 有幾筆?
  • 需要多少硬碟?
  • 快取的記憶體容量要多大?

Traffic:

  • Request per second for write = 500M / 30天 ≈ 192
  • Request per second for read = 500M * 100 / 30天 ≈ 19K/s

Storage:

  • URL 數量: 500M * 12 * 5 = 30 billion
  • 500M * 12 * 5 * 0.5KB = 15000GB = 15TB

Memory:

  • 假設將每日最熱的 20% 的 URL 存入 Memory
  • 19K * 86400 * 0.2 * 0.5KB ≈ 165GB

提出高層次(大方向)的解決方案

在設計短網址系統的時候, 我們就從最簡單的架構開始, 假設不管任何需求, 我們的系統設計就是很簡單就是Client -> Server -> DB

在這種情形下雖然是符合功能需求, 但可能會馬上會意識到問題非常多, 比方說:

  • Server 無法應付單點錯誤
  • 沒有快取策略, 可能回應速度慢, 最後拖垮系統
  • 沒有 Auto scaling 策略, 當流量增大的時候, 系統最後也無法扛下流量而崩潰
  • 資料庫可能也面臨龐大的查詢跟寫入而崩潰
  • 資料庫也面有單點錯誤的問題

於是系統架構上我們演進為這個模樣

這邊也可以在延伸幾個議題:

(1) Server 跟 Database 要用垂直擴展(Scaling up)? 還是平行擴展(Scaling out)?

(2) Load balancer 要想的點有可能有

  • 要用 Network layer(L4) 還是 Application layer(L7)?
  • 需不需要 Reverse proxy?

(3) Database 我們也加入了 Read replica, 讓 Application 可以讀寫分離, 仍然有要再考量的地方

  • DB 能一直垂直擴展嗎?
  • 如果資料量達到數十億筆, Table 會不會因為資料量不斷累積造成查詢效能降低?
  • 每筆資料都很小, 資料間沒有關聯, 要用 RDBMS 還是 NoSQL?
  • 如果 DB 要做水平擴展就會使用 DB Partition 或著 Sharding, 那這樣做需要額外注意什麼? 如果 DB 還是撐不住, 是不是要引入 Consistent hashing 機制等等

(4) Cache 我們也加入了 Read replica, 讓 Application 一樣可以讀寫分離

  • 但看到剛剛估算快取記憶體需要 165GB , 我們要用單台這麼大的 Cacahe server 嗎? 是不是要考慮比方說 Redis cluster mode 而非一般的 Master/Slave
  • Cache 以 Redis 為例, cache eviction 要採用什麼機制, 用 LRU? 還是 LFU? 短網址系統又適合哪種?

這些是不是要再依據估算再做延伸的設計以及相關的配套措施來滿足系統上的需求?

針對系統的關鍵部分做更細部的設計

Schema

Encoding

接著就要思考

  • Key 的合適長度?
  • Encode 出來的的短網址撞 Key 如何處理?

假設我們的 Key 採用 Base62:[A-Za-z0-9] 作為儲存基礎來設計長度

  • 長度 6: 62⁶ = ~56.8 Billion (5.68e10)
  • 長度 7: 62⁷ = ~3.5 Trillion (3.5e12)

上次我們小組討論這題的時候, 我們當下決定了 長度=7 的方案, 理由是長度只多一個 char, 資料量應該不會因此有數倍增長, 但是卻大幅減少碰撞機率, 這對效能來說完全是大加分

產生方式可能有兩種:

  1. Random 7 碼 base62 code
  2. 原網址 -> MD5 -> base62 code -> 取前面 7 碼

撞 Key 的解法 1:

使用 DB + putIfAbsent(INSERT-IF-NOT-EXIST), 非常直覺就是不存在就新增, 已存在就不做任何動作

撞 Key 的解法 2:

Encoding as a Service 其實縮網址並產生對應 key 的當下如果很耗效能, 我們可以考慮為它建立一組 Microservice 也有獨立的 DB, 專門做 Encoding 的事, 我們暫把它稱之為 KGS(Key Generation Service), 因為當下討論在興奮了, 所以再畫一個小架構圖

撞 Key 的解法 3:

  • RDBMS + auto increment counter -> 看起來真的非常無腦
  • MongoDB + auto generate uuid(Object id) -> 這看起來也很無腦

共通點就是自動產生出來的值再做 base62 encode, 幾乎不會有撞 key 的問題

技術選型

每一個技術選擇都會伴隨著優劣勢, 沒有絕對的好跟不好; 也沒有所謂萬解, 因為萬解可能意味著成本高, 這時萬解的缺點就突顯出來了, 每個技術也會隨著時間推移, 這樣技術或解法可能也變得不合適, 比方說剛剛提到的 解法1解法3 的好處是成本低, 但未來會不會因為跟原本的 Application server 高度綁在一起且資料成長量高到影響到效能, 那或許 解法2 是不錯的解法, 也呼應我剛剛的說法 解法2 看似萬解, 但成本高, 這就會變成他的劣勢了

不同公司、不同團隊、不同規模以及有限資源下, 都會影響著當時候的系統設計, 我們必須認知手邊能掌握的資源到底有多少? 因為們在設計的當下必須一直跟 時間 <-> 人力 <-> 錢 這三個東西不斷拉扯

程式架構也是系統的一環

或許前面都說得非常理想, 但更多時候是一開始的系統都是非常簡陋的, 甚至一台 Linux Server 就開始做正式環境的維運, 會有系統設計的通常就是已經開始在賺錢的系統, 或是早期就有大量資源投入的系統, 當然不代表一開始就不設計系統, 而是需要預想未來可能發生的狀況, 在一開始的時候, 就需要針對系統設計一定的彈性好讓系統可以因應面對巨大資料量跟請求量的時候, 可以快速應變

舉個例: 縱使是一台 Linux 也一定不要讓資料庫跟應用程式放在同一台 Linux 伺服器上(單點錯誤)

剛剛提到不同公司、不同團隊、不同規模以及有限資源下, 都會影響著當時候的系統設計, 通常早期都會以功能為主, 到了一定的使用量除了系統乘載量之外, 另外一個讓大部分公司付出巨大成本的其實是系統迭代的過程中產生的可維護性及擴充性

因應流量成長, 系統需要演進, 但程式碼沒有做好抽象隔離分層, 造成整個功能可能需要重寫, 比方說原本是 Query db 是 row based, 之後要換 column based 因為實作上可能有巨大的不一樣, 沒有做好程式碼封裝或是模組化就可能需要重寫,

當我們的程式碼裡頭的技術細節非常複雜的時候, 我們能不能持續保持程式碼的簡潔以及易讀性, 我認為讓系統架構可以持續演化的關鍵就是程式碼模組化, 模組化可牽涉到物件導向設計(OOD), 可牽涉到領域驅動開發(DDD) 甚至檔案目錄的結構

以下 Pseudocode 為 Client 傳來短網址再轉換成原始網址, 並且紀錄請求用戶的資訊以利統計的使用案例

class URLShortToLongUseCase {  readonly loggingRepository: ILoggingRepository;     
readonly urlService: IURLShorteningService;
constructor(loggingRepository: ILoggingRepository,
urlService: IURLShorteningService) {

this.loggingRepository = loggingRepository;
this.urlService = urlService;
}

public async exec(request: Request): string {
await this.loggingRepository.logGuest(request);
return this.urlService.getOriginalURL(request.url);
}
}
class URLShorteningService implements IURLShorteningService {
public async getOriginalURL(shortUrl: string);
public async createShortenedURL(account: IAccount, options: Option);
public async deleteShortenedURL(account: IAccount, url: IRegisteredURL, options: Option);
}

感想:

在開始上第一堂課的時候就感覺大家都很強, 感覺大家也是帶著自己的疑惑來學習, 系統設計是一個資訊量非常龐大腦力活動, 當然其實寫程式也是拉, 其實給 Arthur 老師上完第一堂稍微有點小震撼, 原來我不知道的還這麼多, 敏銳度這麼低

這次的分享以第一堂為主讓大家知道系統設計所需要的思考方向是什麼, 如果要把這系列的課程全部都寫出來, 我想其知識量跟 ITHome 鐵人賽有得比了, 在這次的學習中讓我認知系統設計不能只有思考大方向的部分, 其實更重要的是系統關鍵部分, 比方說短網址系統的關鍵部分是Encoding, 因此需要著重在這點去特別設計以及抉擇, 關鍵的部分通常都有對應的工程方法或是演算法可以解決, 只差在如何運用它並把它融入在系統上

上完這一系列課程的感受是, 我好像有慢慢培養所謂系統面臨效能瓶頸的時候, 要如何解決的那種敏銳度, 以及知道每一個細節每一個環節都可以有優化的地方, 我也學習到它似乎是有一套思考框架可以學習的, 然後再來是資料結構跟演算法還是非常重要的, 畢竟它就是軟體的基礎之一, 很多工程方法也是建立在這之上

--

--