本文為 Design Data Intensive Applications 的書摘 + 個人心得。
其實今天的很多系統都是 數據密集型 應用系統,也就是 數據量大、複雜、且速度快,
有別 10 幾年前的 計算密集型,CPU 時脈才是系統的瓶頸。
現在的數據密集型應用系統都包含了以下功能:
- 儲存資料 (database)
- 加速讀取昂貴操作的結果 (cache)
- 讓 user 用 keyword 或其他方式檢索資料 (search index)
- 寄送訊息到另一個 process,用非同步的方式做某些事 (streaming processing)
- 定期的處理大量已累積的資料 (batch processing)
數據工程師的工作就是選擇合適的工具把上面這些搭建起來。
數據系統
至於什麼是合適的工具呢?現今我們常用的工具並沒有一個非常明確的 分類,
例如你可以用 Elasticsearch 做你的 database,也可以用 Redis 做你的 message queue,
雖然我們直覺都不會這樣幹啦!但只要用的合理,符合等會講的 Reliable, Scalable, Maintainable 原則,又何嘗不可。
下圖為組合多種組件的數據系統架構圖:
![figure_1-1](https://it-iron.s3-ap-northeast-1.amazonaws.com/2020/figure_1-1.png)
個人認為這些是現在數據系統的基本架構,這裡可以有很多討論的細節,每一個區塊都能獨立成一個系統架構,但基本概念是一致的,有應用程式就有 DB,想要 Request 回覆更快就用 cache,數據工程師現在更需要有能力成為一個數據系統的架構師。
當你在建立數據系統時,我們必須確保 3 個主要原則:
Reliable (可靠的)
系統應該只完成我們預期它會完成的事,就算發生錯誤也是如此。
Scalable (可擴充的)
系統能支持未來成長的流量。
Maintainable (可維護的)
系統能同時讓開發工程師或維運工程師正常使用,不會因為某些原因造成某方的生產力下降。
接下來的篇幅會針對這 3 個主要原則詳細說明。
Reliable (可靠的)
對系統來說,工作正確 是預期能完成下述幾項:
- 正確執行 user 預期的函式
- 能容忍 user 的小錯誤和軟體層級上能預想到的錯誤
- 對需求端的 use case 能有效率的執行,並產生可預期的資料
- user 有經過授權才能做事
莫非定律(Murphy’s Law):「凡是可能出錯的事就一定會出錯」
能應對錯誤的系統稱為 falut-toerant (容錯),這裡要跟 failure 做區隔,我稱它為 系統掛點,
這種掛點是會讓你半夜需要爬起來處理的嚴重系統問題,
沒有能完美應對所有錯誤情況的系統, 所以合理的是想像出各種可能會發生的錯誤而提前應對,進而避免累積的錯誤導致系統掛點。
在 ETtoday 我們在測試系統時,也常用手動 kill process 的方式來觀察系統是否可靠,觀察 Service 是否會重啟或者 Leader 有無被正常的選出來等等。
硬體錯誤
當你都在用雲端時,這件事對你是無感的,什麼硬碟壞軌、記憶體故障等等都不甘你的事。
但公司是私有雲時,這件事對軟體的可靠程度就很重要了,在建置系統時,最有效避免此錯誤的方式就是 redundant (冗餘) ,像硬碟的 RAID 設定就是如此,root 硬碟掛掉時,機器還是能正常運作,最棒的是準備 redundant 的機器,確保系統在修復期間是 zero downtime,服務不中斷。
軟體錯誤
上一點提到的硬體錯誤是屬於弱關連,一顆硬碟故障了不會造成另一顆硬碟也故障,
但軟體層面的錯誤關連性就比硬體錯誤強了,舉例來說:
- 某些不好的 input 會導致多個軟體掛掉,像 Leap second issues – June 30, 2012 會導致 Linux Kernal 發生 3 個可能的 issue。
- 某些執行中的 process 可能共用了系統資源,如 CPU, memory, 頻寬等等。
- A 軟體變慢導致同台機器的 B 軟體也變慢或回覆非預期的結果。
- 瀑布流系統掛點,一個小的錯誤可能會造成另一個軟體的錯誤,最終演變成災難。
避免方式就是把測試寫好、獨立執行資源、進程隔離、軟體掛掉自行重啟等等,另外也要做好機器與軟體的監控。
人為錯誤
事在人為,錯誤亦同。
好的系統應該要組合下述幾項:
- 盡可能的降低錯誤的機率。
- 有與生產環境相同的沙箱環境做測試、實驗。
- 能通過完整的測試,包含單元測試、整合測試、人工測試。
- 系統能簡單且快速從人為錯誤中恢復;例如能透過 DevOps 快速 rollback 前一版本、逐步釋出 (rollout) 新版本程式、有工具能重新計算舊的資料。
- 設置詳細且簡單的監控機制,諸如 performance 指標、錯誤率指標等等,監控機制能讓工程師們能及早因應系統錯誤做出反應。
系統沒有符合 Reliable 後面的東西都甭談了,有時候我們會因為成本或時間考量而犧牲一定的可靠性,
工程師需要培養 你在危機時刻依然會遵循的紀律原則
,在這個情況下,恪遵紀律原則是避免陷入危機的最好途徑。
Scalable (可擴充的)
數據系統現在 Reliable 不代表未來也是 Reliable,系統會進步,
這裡討論的 Scalble 是我們有什麼選項去應對未來的成長 和 如何因應額外的流量,
比如 10,000 concurrent user 進步到 100,000 concurrent user,或者未來要處理的資料比現在大的情況。
Load Parameter
就是描述系統的關鍵負載指標 (很多東西真的超難用中文講- -),有了這個指標後才能思考成長的策略 (load paremter 變 2 倍時怎辦),
load parameter 會依各系統不同而有不同的指標,例如 網路服務是 每秒查詢次數 (request per seconds) 、聊天室是 同時在線人數、資料庫是 每秒寫入、讀取的次數 等等。
書中使用 Twitter 在 2012 年 11 月的演講 來說明,Twitter 有 2 個主要的操作:
- 貼文
- 4.6k request/sec 或 平均 12k request/sec 每個貼文
- 首頁 timeline
- 300k request/sec
他們主要的挑戰是如何達成 fan-out ,就是名人貼文後,所有的追蹤者的 首頁 timeline 都要有這篇貼文,
fan-out 是從電機工程來的,用來描述一個邏輯閘的 output 到另一個閘當 input 的量,output 需要滿足且能導向所有連接的 input。
他們有 2 個方法來實現:
- 用 SQL 查詢
SELECT tweets.*, users.* FROM tweets
JOIN users ON tweets.sender_id = users.id JOIN follows ON follows.followee_id = users.id WHERE follows.follower_id = current_user
![figure_1-2](https://it-iron.s3-ap-northeast-1.amazonaws.com/2020/figure_1-2.png)
- 維護每個 user 的首頁 timeline chche
![](https://it-iron.s3-ap-northeast-1.amazonaws.com/2020/figure_1-3.png)
Twitter 一開始是使用方法 1,但在 300k reads/sec 的情況下方法 1 就越來越慢了,方法 2 好一點,讀取首頁 timeline 的事變的簡單了,瓶頸就在寫入首頁 timeline cache 這一端了,
若 user 的追蹤者少,首頁 timeline cache 很快就寫完,但當某個 user 的追蹤者爆炸多時,例如 3 千萬追蹤者,Twitter 若想在 x 秒內讓追蹤者看到這篇貼文就有難度了,
所以 Twitter 的關鍵 load parameter 就是 fan-out 的量。
前公司在設計新聞推薦系統時,是採用 fan-out on read 的概念,我們一樣要確保新發佈的新聞要送給符合的 user 看,但我們是在推薦行為發生時才做完計算並做快取,而不是先建立好每個人的新聞推薦列表,我們的 load parameter 是 QPS。
Performance
有了 load paremter 後再來就是要如何衡量 Performance (執行效率) 了,這裡通常會問 2 個簡單的問題:
- 當你增加 load paremeter 時若系統資源 (CPU, memory etc..) 保持不變,系統的哪些 performance 會被影響?
- 當你增加 load paremeter 時若 performance 保持不變,系統資源會如何被影響?
常用的的 performance 指標就是 response time,就是在送出 request 到接收 response 的時間差。
![figure_1-4](https://it-iron.s3-ap-northeast-1.amazonaws.com/2020/figure_1-4.png)
上圖為 response time 的示意圖,可以看到大部份的 response time 都很低,但有幾個 outlier 讓 response time 變超長,這發生原因可能又是一脫拉庫了,前一天講的各種錯誤情況、 TCP 連線滿載或機器的物理限制等等都有可能讓 outlier 的 response time 變長,
response time 對 user 的最直接影響就是使用觀感,尤其現在人越來越沒有耐心了。
通常在觀察 response time 時我們會用 percentile (百分位) 來衡量,而不會看 mean (平均),平均很容易受 outliar 值影響,常用的 percentile 是 P50, P90, P95, P99
,舉例來說 P50 2.75 ms
,代表有一半的 request 是在 2.75 ms 內回覆,
下圖是某公司 API reponse time 的 percentile 數字:
![figure_grafana](https://it-iron.s3-ap-northeast-1.amazonaws.com/2020/figure_grafana.png)
可以看到 P99 有一個 request 衝到 一秒,P50, P90 都很低。
依公司或系統的不同,追逐的 percentile 目標就不一樣,像 Amazon 看的是 P99.9,然後我們團隊是 P99 要低於 1 秒。
關於 response time 還有個很重要的一點是要做到 dependency 隔離,如下圖,只要有一個 request 的 response time 特別長,整體的 response 的時間就拉長了,前公司是用 Hystrix 來分離我們的 backend request。
![figure_1-5](https://it-iron.s3-ap-northeast-1.amazonaws.com/2020/figure_1-5.png)
Scaling up 和 Scaling out
Scalable 最重要的問題就是:當 load parameter 增加時如何保持好的 performance?
工程師常簡單的一分為二成 Scaling up (垂直擴充,加大機器) 和 Scaling out (水平擴充,都小機器),實務上來看,最好的方式依然是混合版,用有點強度的機器跑服務。
若能用一台 (Scaling up) 就能搞定的需求何必一開始就往 Sacling out 的方向去架構系統呢?
無狀態分散式運算是現在許多數據系統的標配,本系列之路的後續章節將會介紹這些技術,除了幫助工程師更方便的設計 Scaling out 外,更能簡單的達到 Maintainable (可維護的)。
Maintainable (可維護的)
工程師都喜歡開發新系統,而沒這麼喜歡維護原有系統,我曾聽過一種系統開發循環:
工程師快速的寫完新系統 -> 另一批工程師接手維護 -> 覺得技術債太多越來越難以維護 -> crash -> 規劃 v2.0 新系統 -> 工程師快速的寫完新系統
工程師最多的時間都是在維護原有系統,無論你喜不喜歡,當你接手時你就應該跟隨你自己的紀律,把 code 寫好、新功能寫好、測試寫好,保持系統的程式處在 Maintainable 狀態,讓未來的其他工程師不會想拿刀砍你。(但如果是 legacy 系統就算了,這真的該改版!)
再來 3 個軟體開發設計原則
前面提到的 Reliable, Scalable 或許沒這麼容易達成,但我們在 Maintainable 中有 3 個軟體開發設計原則我們可以放在心上。
Operability (可操作的):Making Life Easy for Operations
好的維運單位能保持系統執行的圓滑,以下列個幾點我覺得重要的點:
- 監測系統和能讓系統快速從不正常的狀態中恢復
- 設計好的部署、開發流程,還有好的設定檔管理等等
- 保持軟體更新狀態(尤其是 Security 相關的更新)
- 維護清楚的系統文件
Simplicity (簡單的):Managing Complexity
直接推薦看另一本書 Clean Code,工程師沒看過這本就慘了。
Evolvability (具有進化能力):Making Change Easy
書中針對數據系統的 Agile (敏捷) 能力給它了 evolvability 這個形容詞,其實就是要做到敏捷開發,
數據系統怎麼做到敏捷開發呢?我們可以試想稍早講的 Twitter 例子,要如何從 方法1 進化到 方法2,
這本書後面就是在講這些數據系統、工具背後使用的技術,讓大型數據系統更易於迭代,
例如用 document 的方式儲存 資料能讓你頻繁的變更 Schema。
總結
最概念性的東西講完了,數據系統要完全符合 reliable, scalable 和 maintainable 是條不簡單的路,
希望看完能把這 3 點放在心上,不管是自己寫或用別人的工具都能觀察學習囉!