1. 可擴展系統的基本要素 1.1. 分散式系統在本質上就是複雜的,你必須考慮多種故障模式,並設計應對所有可能發生的情況的處理方式 1.2. 大規模應用程式需要協調大量的硬體和軟體組件,共同實現低延遲和高吞吐量的能力 1.3. 面臨的挑戰是將所有活動部件組合成一個應用程式來運行,使其既能滿足需求又不 ...
1. 可擴展系統的基本要素
1.1. 分散式系統在本質上就是複雜的,你必須考慮多種故障模式,並設計應對所有可能發生的情況的處理方式
1.2. 大規模應用程式需要協調大量的硬體和軟體組件,共同實現低延遲和高吞吐量的能力
1.3. 面臨的挑戰是將所有活動部件組合成一個應用程式來運行,使其既能滿足需求又不會耗費過多成本
- 1.3.1. 新的編程抽象、平臺模型和硬體讓你更容易構建具有更高的性能、更好的可擴展性和更大的彈性的更複雜的系統
2. 自動化
2.1. 在構建大型系統時,工程師是相當昂貴但必不可少的資源
2.2. 需要部署頻繁的更改來改善客戶體驗,並確保可靠和可擴展的操作
2.3. 在不停機的情況下每天有效地將數百個更改推送到已部署系統的能力是系統規模化的關鍵所在
2.4. 促進自動化的一組工具和實踐體現在DevOps文化中
-
2.4.1. DevOps包含一組面向從開發到部署各個級別過程的自動化實踐和工具
-
2.4.2. DevOps的核心是持續交付(CD)實踐,由用於代碼配置管理、自動化測試、部署和監控的複雜工具鏈提供支持
2.5. DevOps實踐對於成功的可擴展系統至關重要
-
2.5.1. 團隊有責任設計、開發和運營他們的微服務,微服務通過良好定義的介面與系統的其餘部分進行交互
-
2.5.2. 藉助自動化工具鏈,可以在微服務中獨立部署本地更改和新功能,同時不幹擾系統操作
-
2.5.3. 自動化減少了協調開銷,提高了生產力,縮短了發佈周期
-
2.5.4. 意味著你的工程投資將獲得更大的回報
3. 可觀測性
3.1. 你無法管理你無法衡量的東西
3.2. 由於有大量移動部件,所有部件都在可變負載條件下運行,容易出現不可預測的錯誤
3.3. 需要藉助測量系統提供的健康狀況和行為來觀測系統狀態
-
3.3.1. 提供基礎設施不斷生成的細粒度指標和日誌數據來捕獲系統當前狀態
-
3.3.2. 分析聚合實時指標並採取行動,對指示實際或未決故障的警報做出反應
3.4. 可觀測性的第一個基本要素是具有一個儀錶化系統,它不斷以指標和日誌條目的形式發出系統遙測數據
-
3.4.1. 可以來自操作系統、你在應用程式中使用的基礎平臺(例如,消息傳遞、資料庫)以及你部署的應用程式代碼
-
3.4.2. 指標表示資源利用率以及系統各部分提供的延遲、響應時間和吞吐量
3.5. 代碼檢測是強制性的,你可以使用開源框架或專有解決方案
- 3.5.1. 指標和日誌條目形成了基於時間序列的連續的數據流,表徵了你的應用程式隨時間的行為
3.6. 捕獲原始指標數據是可觀測性系統推斷並感知態勢的先決條件
-
3.6.1. 需要快速處理數據流,它才可能讓系統及時採取行動
-
3.6.2. 包括持續監控當前狀態、探索歷史數據以瞭解或診斷一些意外的系統行為,以及在超過閾值或發生故障時發送實時警報
3.7. Prometheus、Grafana和Graphite是目前廣泛使用的技術,它們提供了適用於可觀測性棧各個部分的開箱即用的解決方案
3.8. 可觀測性是可擴展分散式系統的必要組成部分
4. 部署平臺
4.1. 可擴展系統需要大規模、有彈性且可靠的計算和數據平臺
4.2. 可以使用專為操作設計的腳本語言自動調用配置
- 4.2.1. 基礎架構即代碼(IaC),也是DevOps的基本要素
4.3. 傳統上,虛擬機是應用程式的部署單元
-
4.3.1. 容器鏡像支持將應用程式代碼和依賴項打包到單個可部署單元中
-
4.3.2. 與虛擬機相比,容器消耗的資源更少,因此可以在單個虛擬機上打包多個容器,更有效地利用硬體資源
4.4. 容器通常與集群管理平臺(如Kubernetes或Apache Mesos)一起使用
-
4.4.1. 容器編排平臺為你提供API來控制容器的執行方式、時間和位置
-
4.4.2. 平臺允許你自動部署容器並支持使用自動縮放的不同系統負載,簡化集群中在多個節點部署多個容器的管理工作
5. 數據湖
5.1. 隨著時間的推移,你的系統將生成許多PB級或更多的數據
- 5.1.1. 數據中的大部分很少被你的用戶訪問
5.2. 管理、組織和存儲歷史數據存儲庫是數據倉庫、大數據和數據湖的領域範圍所在
- 5.2.1. 本質是以一種可以檢索、查詢和分析的形式來存儲歷史數據
5.3. 數據湖的特征是以異構格式存儲和編目數據,從原生blob到JSON再到關係資料庫提取數據
-
5.3.1. 利用Apache Hadoop、Amazon S3或Microsoft Azure Data Lake等低成本對象存儲
-
5.3.2. 靈活的查詢引擎支持數據的分析和轉換
-
5.3.3. 可以使用不同的存儲類別,以本質上更長的檢索時間換取更低的成本,繼而優化成本
6. 併發系統
6.1. 分散式系統包括多個獨立的代碼片段,它們在不同位置的多個處理節點上並行或併發地執行
6.2. 任何分散式系統都是併發系統,即使每個節點一次只處理一個事件也是如此
- 6.2.1. 在分散式系統中協調節點充滿了風險
6.3. 編寫軟體來併發地執行多個操作,有助於優化單個節點上的處理能力和資源利用率,提高本地和系統範圍的處理能力
6.4. 在過去的計算時代,每個CPU在任何時刻都只能執行一條機器指令
-
6.4.1. 程式試圖讀取文件或在網路上發送消息時,它必須與CPU外圍的硬體子系統(磁碟、網卡)進行交互
-
6.4.2. 從硬碟讀取數據大約需要10ms。在此期間,程式必須等待可供處理的數據
-
6.4.3. Linux等操作系統可以在單個CPU上運行多個程式的方式
-
6.4.4. 將軟體明確地構造成具有多個可以並行執行的活動,在其他任務等待I/O時,操作系統可以安排有工作要做的任務
6.5. 使用多核晶元,可以在每個內核上併發執行具有多個並行活動的軟體系統,最多可達到可用內核的數量
-
6.5.1. 每種編程語言都有自己的線程機制
-
6.5.2. 所有併發機制的底層語義都是相似的
-
6.5.3. 主流使用的主要線程模型只有幾個
6.6. 在過去50年裡,併發模型一直是電腦科學中研究和探索較多的主題
-
6.6.1. CSP(通信順序進程)模型構成了Go併發特性的基礎
-
6.6.1.1. 在Go中,併發的單位是goroutine,goroutine使用無緩衝或緩衝通道發送消息來進行通信
-
6.6.2. Erlang實現了併發的actor模型
-
6.6.2.1. actor是沒有共用狀態的輕量級進程,通過向其他actor發送非同步消息來進行通信
-
6.6.2.2. actor使用郵箱或隊列來緩衝消息,可以使用模式匹配來選擇要處理的消息
-
6.6.3. Node.js避開多線程,利用由事件迴圈管理的單線程非阻塞模型
-
6.6.3.1. 該模型適用於頻繁執行I/O請求的代碼
-
6.6.3.2. 如果你的代碼需要執行CPU密集型操作,例如對大型列表進行排序,那麼你只有一個線程
> 6.6.3.2.1. 這將阻止其他請求,直到排序完成
> 6.6.3.2.2. 這並非一種理想的情況
6.7. 在可擴展分散式系統的世界中,併發是無處不在的
6.8. 無論你使用的是C/C++中的pthreads庫,還是受CSP啟發的經典Go併發模型,需要避免的問題都是相同的
7. 線程
7.1. 預設情況下,每個軟體進程都有一個執行線程,即操作系統在安排進程執行時所管理的線程
7.2. 線程本質上是我們構建可擴展分散式系統時用於數據處理和資料庫平臺的組件
7.3. 在許多情況下,你可能不會顯式編寫多線程代碼
7.4. 許多平臺還通過配置參數來調整其併發能力,這意味著要調整系統性能,你需要瞭解更改各種線程和線程池設置的影響
7.5. 線程執行順序
-
7.5.1. 從程式開發者的角度來看,執行順序是不確定的(nondeterministic)
-
7.5.2. 不確定性(nondeterminism)這個概念是理解多線程代碼的基礎
-
7.5.3. 一旦調度程式允許一個線程在CPU上執行一段時間,它就可以在指定的時間段後中斷該線程,並安排另一個線程運行
-
7.5.3.1. 中斷稱為搶占
-
7.5.4. 調度程式根據調度演算法決定何時運行哪個線程,線程是獨立且非同步地運行,直到完成
-
7.5.5. 無論線程執行的順序如何(你無法控制),你的代碼都應該產生正確的結果
7.6. 線程的狀態
-
7.6.1. 多線程系統有一個系統調度程式來決定何時運行哪些線程
-
7.6.1.1. 執行最高優先順序的線程
7.7. 線程池
-
7.7.1. 許多多線程系統需要創建和管理一組執行相似任務的線程
-
7.7.2. 線程集合為線程池
-
7.7.2.1. 線程池包含多個工作線程,它們通常執行相似的任務,並以一個集合進行管理
-
7.7.3. 如果系統以不受約束的方式創建線程,最終會耗盡記憶體,導致系統崩潰
7.8. 同步屏障
-
7.8.1. CountDownLatch是一個簡單的同步屏障器
-
7.8.1.1. 它是一次性工具,初始化值無法重置
8. 線程引入的問題
8.1. 併發編程的基本問題是如何協調多個線程的執行,無論它們以何種順序執行,都會產生正確的結果
8.2. 鑒於線程可以不確定地被啟動和搶占,任何中等複雜的程式本質上都有無數種執行順序
- 8.2.1. 這些系統是不容易測試的
8.3. 所有併發程式都需要避免兩個基本問題,即競態條件和死鎖
8.4. 競態條件
-
8.4.1. 如果每個線程都只做自己的事情並且完全獨立,執行順序就不是問題了
-
8.4.2. 完全獨立的線程並不是大多數多線程系統的行為方式
-
8.4.3. 線程可以使用共用的數據結構來協調它們的工作併在線程之間傳遞狀態
-
8.4.4. 競態條件是隱秘的、狡猾的錯誤,因為它們通常很少見,而且很難被髮現,大多數時候結果都是正確的
-
8.4.4.1. 相同的代碼,偶爾會出現不同的結果
-
8.4.4.2. 關鍵是識別和保護臨界區
-
8.4.4.3. 臨界區是更新共用數據結構的一段代碼,如果它被多個線程訪問,則必須以原子方式執行
-
8.4.4.4. 你應該使臨界區代碼儘可能少,將序列化代碼減到最少
8.5. 死鎖
-
8.5.1. 如果我們不小心編寫過多限制不確定性的代碼,則又會導致程式停止運行,並且永遠不會繼續執行,術語稱其為死鎖
-
8.5.2. 當兩個或多個線程永遠被阻塞,沒有一個可以繼續執行時,就會發生死鎖
-
8.5.2.1. 當線程需要獨占共用資源集,以不同的順序獲取鎖時,就會發生這種情況
-
8.5.3. 死鎖,也稱為致命擁抱,會導致程式停止
-
8.5.4. 可以在軟體的阻塞操作上使用超時來實現
-
8.5.4.1. 在超時到期後,一個線程釋放臨界區並重試,讓其他被阻塞的線程有機會繼續執行
-
8.5.4.2. 阻塞線程會損害性能,設置超時值也不是精確的做法
-
8.5.5. 對於迴圈等待死鎖,可以在共用資源上施加資源分配協議來解決,這樣線程就不會總是以相同的順序請求資源了
9. 線程間的協調
9.1. 很多時候,我們需要不同角色的線程來協調它們的活動,繼而解決問題
9.2. 列印問題就是典型的生產者-消費者的例子
-
9.2.1. 與一切現實的資源一樣,緩衝區的容量也是有限的
-
9.2.2. 輪詢,或忙等待
-
9.2.3. 更好的解決方案是讓生產者和消費者阻塞,直到其期望的操作(分別為put或get)成功
-
9.2.4. 阻塞的線程不消耗資源,這是一個有效的解決方案
10. 線程安全集合
10.1. java.util包中的集合併不是線程安全的
- 10.1.1. 為了加快單線程程式的執行速度,該集合不是線程安全的
10.2. 在多線程代碼中使用線程安全集合總是更安全
-
10.2.1. ConcurrentHashMap的迭代器是弱一致性
-
10.2.2. 如果你需要一個在被多個線程更新時始終反映當前hashmap狀態的迭代器,就要付出性能代價,ConcurrentHashMap不是正確的選擇