坦白說也是機緣巧合,在碩士生階段進入分散式系統領域學習。無論是大規模存儲或計算,其核心也是運用分散式技術利用並行性來解決數據密集型應用的需求。最近開始在啃這本 "《Designing Data Intensive Applications》" 大部頭,作者 "Martin Kleppmann" 在分 ...
坦白說也是機緣巧合,在碩士生階段進入分散式系統領域學習。無論是大規模存儲或計算,其核心也是運用分散式技術利用並行性來解決數據密集型應用的需求。最近開始在啃這本《Designing Data-Intensive Applications》大部頭,作者Martin Kleppmann在分散式數據系統領域有著很深的功底,併在這本書中完整的梳理各類紛繁複雜設計背後的技術邏輯,不同架構之間的妥協與超越,很值得開發人員與架構設計者閱讀。
很可惜的是國內目前並沒有對應的中文版本,這個系列算是一個讀書感悟,同時也夾帶私貨,闡述一些自己的理解與看法,拋磚引玉,希望大家多交流學習。這本書共有12個章節,接下來我會一個章節更新一篇讀書筆記。(囧rz,感覺自己又開了一個坑)同時也希望國內的出版社可以儘快引入版權,我也想要參與翻譯工作啊(,,• ₃ •,,) !!
1.數據密集應用
作為一個開發者來說,目前絕大多數應用程式都是數據密集型的,而不是計算密集型的。CPU的計算能力不再成為這些應用程式的限制因素,而更加亟待解決的問題是海量的數據、數據結構之間的複雜性,應用的性能。
先看看我們經常打交道的數據系統:
- 存儲數據,以便它們或其他應用程式稍後再找到它(資料庫)
- 記住昂貴操作的結果,以加快讀取速度。(緩存)
- 允許用戶按關鍵字搜索數據或通過各種方式過濾數據(搜索索引)
- 將消息發送到另一個進程,非同步處理(流處理)
- 周期性地壓縮大量的累積數據(批處理)
而很多時候,我們所謂應用程式的絕大工作就是將這些數據系統進行組合,然後添加我們的運行邏輯,但是如何更加合理的整合這些數據系統,對我們來說仍然是一個值得學習和思考的問題。而數據系統也在慢慢變得越來越相似,不同的數據系統也在各自學習彼此的優點。如Redis這樣的緩存系統可以支持數據落地,很多時候的應用場合我們可以替代傳統的RDBMS。而Kafka這樣的數據隊列也可以支持數據落地來存儲消息。更加深刻的理解這些數據系統,來更好的權衡架構設計,是一門很精深的課題。
上圖是一個典型的由多種數據系統構成的應用程式,隨著數據量和數據邏輯的複雜,就成為了一個數據密集型的應用。
2.設計數據密集型應用的三原則
- 可靠性
具有容錯性(面對硬體或軟體故障,甚至是人為錯誤),系統仍應繼續正常工作(在期望的性能水平上執行正確的功能)。 - 可擴展性
隨著系統的增長(在數據量、流量或複雜度),應該有合理的方法來處理這種增長。 - 可維護性
隨著時間的推移,許多不同的人將致力改善數據系統(既保持當前的行為,並使系統適應新的環境),他們都應該能夠卓有成效地工作。
顯然,這三個原則不單單是數據密集型應用應當遵循的原則,在絕大多數軟體系統中同樣是很重要的問題,接下來我們一一梳理一下。
(1)可靠性
硬體故障
硬碟崩潰,記憶體出現故障時,電網停電,有人拔了網線,幾乎硬體故障在數據中心總是不間斷的出現。
解決方案:- 在軟體與硬體層面考慮冗餘,來確保硬體的故障不會演變為系統的故障。
人為的錯誤
人是很不可靠,從駕駛技術的演變就可以看出來,人為的疏失會帶來巨大的災難。而且,人經常犯錯。
解決方案:- 最小化錯誤機會的方式設計系統。例如,精心設計的抽象,API和管理界面可以很容易地做“正確的事情”,阻止“錯誤的事情”。
人們犯最多錯誤的地方和那些可能導致失敗的地方解耦。
全面測試,從單元測試到整個系統集成測試和手動測試。
允許快速和容易地從人為錯誤中恢復,以儘量減少在失敗的情況下的影響。例如,使其快速回滾更改配置,逐步推出新的代碼(所以任何意想不到的錯誤隻影響一小部分用戶),並提供工具來重新計算數據(如果原來舊的計算是不正確的)。
(2)可擴展性
即使一個系統今天工作可靠,但這並不意味著它將來一定會可靠地工作。一個常見原因是負載增加:也許系統已經從10000個併發用戶發展到100000個併發用戶,或者從100萬個增加到1000萬個。
“如果系統以特定的方式增長,我們應對增長的選擇是什麼?” “我們怎樣才能增加計算資源來處理額外的負載?”
- 描述負載
首先,我們需要簡潔地描述系統當前的負載,負載可以用幾個數字來描述,我們稱之為負載參數。
參數的選擇取決於系統的體繫結構,如: - 每秒對Web伺服器的請求
- 資料庫中的讀寫比
- 聊天室中的活躍用戶數量
緩存的命中率
描述性能
一旦描述了系統上的負載,就可以討論負載增加時發生的情況。可以從兩方面看:
1.增加負載參數並保持系統資源(CPU、記憶體、網路帶寬等)不變時,系統的性能如何受到影響?
2.當增加負載時,如果希望保持性能不變,需要增加多少資源?
所以我們需要有描述性能的尺子:
- 平均響應時間:給定n值的算術平均值,全部加起來,除以n。然而這不是一個很好的指標,因為它不告訴你有多少用戶真正體驗了延遲。
- 百分比響應時間:把響應時間列表,從最快到最慢排序,那麼中間值是中間點:例如,如果平均響應時間是200毫秒,那意味著一半請求在少於200毫秒時返回,而一半請求花費的時間比那個要長。
- 高百分比的響應時間:可以看看高百分位數:95th,99th,和99.9th百分位數是常見的(簡稱P95,P99,和p999),來參考響應時間的閾值。
負載情況與性能情況是很重要的,有時系統的瓶頸是由少數極端情況引起的。作者舉了一個Twitter的例子,我覺得很好,這裡詳細分享一下這個例子:
Twitter的故事
Twitter在2012年11月16日公佈的數據。
Twitter的兩個主要操作是:
- 發出Tweet
用戶可以發佈一個Tweet給他們的訂閱者。(平均4.6k請求/秒,峰值超過1.2萬的請求/秒)。 - 獲取Tweet
用戶可以查看他們關註者發佈Tweet。(約300K的請求/秒)。
Twitter在擴展性的挑戰主要不是由於Tweet的數量,而主要是在每個用戶都有很多訂閱者,每個用戶也有很多關註者。執行這兩種操作大致是兩種方法:
- 1、發佈一條推特,只需將新的推文插入到全球的推文集合中即可。當用戶請求他們關註者的Tweet時,可以查找他們所關註的所有人,並找到每個用戶的所有Tweet,並將它們合併(按時間排序)。在關係資料庫中,可以編寫如下查詢,例如:
java 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
如下圖所示:
- 2、為每個用戶訂閱的Tweet維護一個緩存,就像每個收件人的Twitter郵箱一樣。當用戶發佈一條推文時,請查找所有關註該用戶的人,並將新的Tweet推送到他們的緩存中。所以讀取Tweet列表是很划算的,因為它的結果提前計算好了。
如上圖所示的結構顯然更合適Tweet的發佈,因為發佈的Tweet的寫操作幾乎比讀的操作低兩個數量級,所以在這種情況下,最好是在寫時做更多的工作,而不是在讀時做更多的工作。但是方法2並不適用於有大量關註者的賬號,假設某人有3000W粉絲,一次發佈Tweet產生的寫操作可能是巨大的。所以目前在Twitter的Tweet系統中,Twitter將這兩種方法混合。大多數用戶的推文在發佈時仍然會被擴展到Tweet緩存之中,但只有少數用戶擁有大量的關註者(即名人)。用戶可以跟蹤的任何名人的Tweet,並單獨讀取並與用戶的Tweet緩存中進行合併。這種混合方法能夠始終如一地提供良好的性能。
(這個例子很精煉的描述了架構設計的妥協與精妙,依據業務特點,最大化的優化了數據系統的性能。很佩服Twitter的工程師在架構設計上的功力。同時也很好奇如微博,微信是不是也是採用類似的架構進行設計。)
- 怎麼擴展
放大(垂直縮放,移動到更強大的機器)和縮放(橫向縮放,在多台更小的機器上分配負載)之間的二選一。實際上,好的架構通常涉及到一種實用的混合方法:例如,使用幾個功能強大的機器仍然比大量的小型虛擬機更簡單、更便宜。無節制的分散式會給系統混入複雜度,這是軟體工程中危險的地方,雖然在多台機器上分發無狀態服務相當簡單,但將有狀態數據系統從單個節點轉移到分散式安裝程式會帶來許多額外的複雜性。
沒有這樣的東西,一個通用的,一個適合所有的應用的可伸縮的架構。(寫的真好)
(3)可維護性
這部分教導了一些構建可維護系統的方法。軟體的大部分成本不是在最初的開發中,而是在持續的維護中修複bug、保持系統運行、使其適應新業務、添加新特性。
可操作性
讓操作運維團隊保持系統運行的順利。簡單
讓新工程師很容易理解系統,通過儘可能地從系統中刪除儘可能多的複雜性。可進化性
讓工程師很容易在將來對系統進行更改,以適應需求變化時的意料之外的用例。也被稱為可擴展性、可修改性、可塑性。
(維護別人留下的爛攤子真的是很痛苦的事情,文檔,註釋真的是重中之重!!!)