讀構建可擴充套件分散式系統:方法與實踐15可擴充套件系統的基本要素

躺柒發表於2024-09-26

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不是正確的選擇

相關文章