1. 程式人生 > >分散式微服務架構體系詳解

分散式微服務架構體系詳解

課程介紹

微服務架構的技術體系、社群目前已經越來越成熟。在最初系統架構的搭建,或者當現有架構已到達瓶頸需要進行架構演進時,很多架構師、運維工程師會考慮是否需要搭建微服務架構體系。雖然很多文章都說微服務架構是複雜的、會帶來很多分散式的問題,但只要我們瞭解這些問題,並找到解法,就會有種撥開雲霧的感覺。

微服務架構也不是完美的,世上沒有完美的架構,微服務架構也是隨著業務、團隊成長而不斷演進的。最開始可能就幾個、十幾個微服務,每個服務是分庫的,通過 API Gateway 並行進行服務資料合併、轉發。隨著業務擴大、不斷地加入搜尋引擎、快取技術、分散式訊息佇列、資料儲存層的資料複製、分割槽、分表等。

本課程會一一解開微服務架構下分散式場景的問題,以及通過對於一些分散式技術的原理、模型和演算法的介紹,來幫助想要實施微服務架構的工程師們知其然並知其所以然。並且,本課程通過對分散式問題的體系化梳理,結合一些方案的對比選型,可以讓工程師們一覽微服務的知識圖譜。

注:為了方便初學者理解微服務實踐,以及掌握怎樣在微服務中使用 DDD(Domain-Driven Design)思想,在本課程第 05 課中講解了 Demo 示例,該示例是基於 SpringBoot、SpringCloud-Eureka 技術寫的,Microservice 程式碼詳見這裡Gateway 程式碼詳見這裡

專家推薦

近年來隨著網際網路的快速發展,尤其是移動網際網路以及雲端計算的迅猛發展,對於軟體交付與迭代速度和效率的要求在不斷提高。微服務架構憑藉其簡單清晰、靈活可擴充套件、獨立部署等優勢,越來越成為了分散式架構中的主流。相關的書籍和課程也層出不窮,但更多還是集中在基本理論介紹和一個簡單的示例上。本系列課程內容融合了作者多年的實踐經驗,將微服務架構下的一些經典的分散式問題和場景逐一展開,結合最新的技術潮流,理論結合實際,深入剖析講解,並且給出了很多對於具體實踐選型非常有益的建議。可以說,該課程內容融合了作者從阿里巴巴到創業公司這一路走來所積累的精華,是微服務及分散式領域難得的佳作。

——阿里巴巴技術專家,榮博

作者介紹

李靜瑤,2011 年畢業於中南大學(校優秀畢業生、優秀學生幹部),畢業後入職阿里巴巴集團,在職期間主要負責淘寶網營銷產品線的研發工作,曾擔任試用中心產品線 PM。2015 年開始,在終點網路科技公司擔任後端架構師,以及參與 Android、iOS 等客戶端研發。現就職於赤金資訊科技有限公司,擔任 CTO 職位。從零搭建基於 Docker 容器技術的微服務分散式企業叢集,深度的 DDD 思想踐行者。CSDN 部落格專家。個人部落格

課程內容

導讀:微服務架構下的分散式概覽

微服務架構的演變

微服務是一種服務間鬆耦合的、每個服務之間高度自治並且使用輕量級協議進行通訊的可持續整合部署的分散式架構體系。這一句包含了微服務的特點,微服務架構和其他架構有什麼區別?以下對比一些常見的架構。

單體架構

單體架構是最簡單的軟體架構,常用於傳統的應用軟體開發以及傳統 Web 應用。傳統 Web 應用,一般是將所有功能模組都打包(jar、war)在一個 Web 容器(JBoss、Tomcate)中部署、執行。隨著業務複雜度增加、技術團隊規模擴大,在一個單體應用中維護程式碼,會降低開發效率,即使是處理一個小需求,也需要將所有機器上的應用全部部署一遍,增加了運維的複雜度。

SOA 架構

當某一天使用單體架構發現很難推進需求的開發、以及日積月累的技術債時,很多企業會開始做單體服務的拆分,拆分的方式一般有水平拆分和垂直拆分。垂直拆分是把一個應用拆成鬆耦合的多個獨立的應用,讓應用可以獨立部署,有獨立的團隊進行維護;水平拆分是把一些通用的,會被很多上層服務呼叫的模組獨立拆分出去,形成一個共享的基礎服務,這樣拆分可以對一些效能瓶頸的應用進行單獨的優化和運維管理,也在一定程度上防止了垂直拆分的重複造輪子。

SOA 也叫面向服務的架構,從單體服務到 SOA 的演進,需要結合水平拆分及垂直拆分。SOA 強呼叫統一的協議進行服務間的通訊,服務間執行在彼此獨立的硬體平臺但是需通過統一的協議介面相互協作,也即將應用系統服務化。舉個易懂的例子,單體服務如果相當於一個快餐店,所有的服務員職責都是一樣的,又要負責收銀結算,又要負責做漢堡,又要負責端盤子,又要負責打掃,服務員之間不需要有交流,使用者來了後,服務員從前到後負責到底。SOA 相當於讓服務員有職責分工,收銀員負責收銀,廚師負責做漢堡,保潔阿姨負責打掃等,所有服務員需要用同一種語言交流,方便工作協調。

微服務和 SOA

微服務也是一種服務化,不過其和 SOA 架構的服務化概念也是有區別的,可以從以下幾個關鍵字來理解:

  • 鬆耦合:每個微服務內部都可以使用 DDD(領域驅動設計)的思想進行設計領域模型,服務間儘量減少同步的呼叫,多使用訊息的方式讓服務間的領域事件來進行解耦。
  • 輕量級協議:Dubbo 是 SOA 的開源的標準實現之一,類似的還有像 gRPC、Thrift 等。微服務更傾向於使用 Restful 風格的 API,輕量級的協議可以很好得支援跨語言開發的服務,可能有的微服務用 Java 語言實現,有的用 Go 語言,有的用 C++,但所有的語言都可以支援 Http 協議通訊,所有的開發人員都能理解 Restful 風格 API 的含義。
  • 高度自治和持續整合:從底層的角度來說,SOA 更加傾向於基於虛擬機器或者伺服器的部署,每個應用都部署在不同的機器上,一般持續整合工具更多是由運維團隊寫一些 Shell 指令碼以及提供基於共同協議(比如 Dubbo 管理頁面)的開發部署頁面。微服務可以很好得和容器技術結合,容器技術比微服務出現得晚,但是容器技術的出現讓微服務的實施更加簡便,目前 Docker 已經成為很多微服務實踐的基礎容器。因為容器的特色,所以一臺機器上可以部署幾十個、幾百個不同的微服務。如果某個微服務流量壓力比其他微服務大,可以在不增加機器的情況下,在一臺機器上多分配一些該微服務的容器例項。同時,因為 Docker 的容器編排社群日漸成熟,類似 Mesos、Kubernetes 及 Docker 官方提供的 Swarm 都可以作為持續整合部署的技術選擇。

其實從架構的演進的角度來看,整體的演進都是朝著越來越輕量級、越來越靈活的應用方向發展,甚至到近兩年日漸成熟起來的 Serverless(無服務)架構。從單體服務到分層的服務,再到面向服務、再到微服務甚至無服務,對於架構的挑戰是越來越大。

微服務架構和分散式

微服務架構屬於分散式系統嗎?答案是肯定的。微服務和 SOA 都是典型的分散式架構,只不過微服務的部署粒度更細,服務擴充套件更靈活。

理解微服務中的分散式

怎樣理解微服務中的分散式?舉一個招聘時一個同學來面試的例子。A 同學說,目前所在公司在做從單應用到微服務架構遷移,已經差不多完成了。提到微服務感覺就有話題聊了,於是便問“是否可以簡單描述下服務拆分後的部署結構、底層儲存的拆分、遷移方案?”。於是 A 同學說,只是做了程式碼工程結構的拆分,還是原來的部署方式,資料庫還是那個庫,所有的微服務都用一個庫,分散式事務處理方式是“避免”,儘量都同步呼叫……於是我就跟這位同學友好地微笑說再見了。

微服務的分散式不僅僅是容器應用層面的分散式,其為了高度自治,底層的儲存體系也應該互相獨立,並且也不是所有的微服務都需要持久化的儲存服務。一個“手機驗證碼”微服務可能底層儲存只用一個 Redis;一個“營銷活動搭建頁面”微服務可能底層儲存只需要一個 MongoDB。

微服務中的分散式場景除了服務本身需要有服務發現、負載均衡,微服務依賴的底層儲存也會有分散式的場景:為了高可用性和效能需要處理資料庫的複製、分割槽,並且在儲存的分庫情況下,微服務需要能保證分散式事務的一致性。

課程背景

微服務架構的技術體系、社群目前已經越來越成熟,所以在初期選擇使用或者企業技術體系轉型微服務的時候,需要了解微服務架構中的分散式的問題:

  • 在所有服務都是更小單元的部署結構時,一個請求需要調動更多的服務資源,怎樣獲得更好的效能?
  • 當業務規模增大,需要有地理分佈不同的微服務叢集時,其底層的資料儲存叢集是多資料中心還是單資料叢集?
  • 資料儲存如何進行資料複製?
  • 業務資料達到大資料量時怎樣進行資料的分割槽?
  • 分散式事務怎樣保證一致性?
  • 不同程度的一致性有什麼差別?
  • 基於容器技術的服務發現怎麼處理?
  • 應該用哪些 RPC 技術,用哪些分散式訊息佇列來完成服務通訊和解耦?
  • 那麼多的分散式技術框架、演算法、服務應該選哪個才適合企業的業務場景?

本課程從微服務不得不面對和解決的分散式問題出發,包含分散式技術的一系列理論以及架構模型、演算法的介紹,同時結合技術選型和實踐應用,提供一系列解決方案的梳理。相信閱讀完整個課程,你會對微服務的分散式問題有個系統地理解。本課程會對微服務的分散式場景問題一一擊破,為你提供解決思路。

課程內容

本課程示例程式碼地址如下:

分散式系統的問題

  • 引出分散式系統的可能問題:節點故障、網路延遲,結合錯誤檢測的可行方案進行介紹;
  • 分散式中的時間和順序的問題,以及標量時鐘和向量時鐘的實現。

分散式資料儲存

  • 分散式資料儲存的技術選型、關係型資料庫以及一些流行的 NoSQL 技術介紹(MongoDB、Redis、Neo4j 及 Cassandra 等);
  • 分散式儲存技術使用的資料結構,瞭解底層資料儲存原理(HashTable、SSTable、LSMTree、BTree 等);
  • 各個儲存方案使用的場景以及對比。

資料複製

  • 對於大規模儲存叢集,需要進行資料庫的複製、排除單點故障;
  • 資料複製的模型和實現以及幾種複製日誌的實現方式;
  • 主備同步、主主複製、多資料中心的資料複製方案;
  • 資料複製中的讀寫一致性問題以及寫衝突問題的解決;
  • 介紹以 MySQL 為例延伸叢集資料複製方案。

資料分割槽

  • 當單個領域模型維度的資料已經到一定規模時,需要進行資料分割槽,減輕單庫壓力。資料分割槽和分表又有哪些不同?資料分割槽可以如何實現?
  • 以 MySQL 的分割槽策略為例介紹不同分割槽策略的實現。
  • 資料分割槽後,請求的路由有哪些解決方案?會展開介紹不同的方案又有什麼差別。

服務發現和服務通訊

  • 基於容器技術的微服務體系,怎樣選擇服務發現、負載均衡的技術實現?不同的服務發現的技術有什麼區別,如何選型?
  • 為了達到鬆耦合的目的以及基於 DDD 思想,微服務之間減少服務呼叫可以通過哪些技術實現?API Gateway 可以用哪些技術框架實現?遠端呼叫可以有哪些技術框架?怎樣提高同步通訊的效能?
  • 分散式的訊息佇列都有哪些開源、商業實現?應該怎樣選擇適合的訊息佇列?
  • 使用 DDD 思想應該如何應對服務通訊,如何在實踐中應用 DDD?

分散式儲存叢集的事務

  • 理解分散式中的事務以及本地事務的基礎概念;
  • 分散式儲存的隔離級別以及各個 DB 支援的隔離方案的實現原理;
  • 以 MySQL InnoDB 中的 MVCC 為例看併發控制的在 MySQL 的實現,學習儲存系統對於分散式事務的實現思想。

分散式一致性

  • 瞭解分散式系統的一致性有哪些問題以及一致性的幾種實現程度的模型:線性一致性(強一致性)、順序一致性及因果一致性、最終一致性;
  • 分散式一致性相關的理論 CAP(CA、CP、AP 的相關演算法)的介紹以及適合用於哪些實踐;
  • 介紹 FLP 不可能結果,以及 BASE 理論。

分散式事務實踐

  • 瞭解微服務中分散式事務的問題;
  • 介紹強一致性的實踐:二階段、三階段。2PC、3PC 的優缺點和限制,XA 協議的介紹和實踐方案,以及最終一致性實踐:TCC 模型和實踐方案;
  • 分散式鎖的實現模型和實踐方案;
  • 基於微服務下的分散式事務實踐案例分析。

共識問題

  • 瞭解為什麼分散式場景下有共識問題;
  • 介紹共識演算法和全域性訊息廣播的實現,公式演算法的基礎:leader 選舉和 quorum 演算法,以及一些已實現的演算法的介紹和對比:VSR、Raft、Paxos、ZAB;
  • 共識演算法在微服務體系的應用場景介紹:服務發現、一致性 kv 儲存(Etcd、Zk)以及技術的選型如何權衡一致性的追求和效能。

架構設計

  • 瞭解了很多分散式的問題和解決方案之後,迴歸微服務架構模型、技術選型、再回顧下微服務的弊端和特點;
  • 微服務體系的架構要素分析:安全、伸縮性、效能、可用性、擴充套件性;
  • 結合團隊、業務場景的 DDD 實踐和總結。

課程寄語

  • 如果你是一位開發工程師,相信閱讀完本系列課程,將會了解很多分散式系統的理論知識,同時也會理解一些分散式儲存、中介軟體技術的原理,對工作中的分散式架構會有體系化的清晰認知。
  • 如果你是一位架構師,本系列課程提供了對於分散式系統問題的全面梳理,以及一些技術背後的理論,結合實踐和目前業界先進的方案,對於技術選型和架構搭建提供了參考。
第01課:分散式系統的問題

前言

無論是 SOA 或者微服務架構,都是必須要面對和解決一些分散式場景下的問題。如果只是單服務、做個簡單的主備,那麼程式設計則會成為一件簡單幸福的事,只要沒有 bug,一切都會按照你的預期進行。然而在分散式系統中,如果想當然的去按照單服務思想程式設計和架構,那可能會收穫很多意想不到的“驚喜”:網路延遲導致的重複提交、資料不一致、部分節點掛掉但是任務處理了一半等。在分散式系統環境下程式設計和在單機器系統上寫軟體最大的差別就是,分散式環境下會有很多很“詭異”的方式出錯,所以我們需要理解哪些是不能依靠的,以及如何處理分散式系統的各種問題。

理想和現實

微服務架構跟 SOA 一樣,也是服務化的思想。在微服務中,我們傾向於使用 RESTful 風格的介面進行通訊,使用 Docker 來管理服務例項。我們的理想是希望分散式系統能像在單個機器中執行一樣,就像客戶端應用,再壞的情況,使用者只要一鍵重啟就可以重新恢復,然而現實是我們必須面對分散式環境下的由網路延遲、節點崩潰等導致的各種突發情況。

在決定使用分散式系統,或者微服務架構的時候,往往是因為我們希望獲得更好的伸縮性、更好的效能、高可用性(容錯)。雖然分散式系統環境更加複雜,但只要能瞭解分散式系統的問題以及找到適合自己應用場景的方案,便能更接近理想的開發環境,同時也能獲得伸縮性、效能、可用性。

分散式系統的可能問題

分散式系統從結構上來看,是由多臺機器節點,以及保證節點間通訊的網路組成,所以需要關注節點、網路的特徵。

(1)部分失敗

在分散式環境下,有可能是節點掛了,或者是網路斷了,如下圖:

part failure

如果系統中的某個節點掛掉了,但是其他節點可以正常提供服務,這種部分失敗,不像單臺機器或者本地服務那樣好處理。單機的多執行緒物件可以通過機器的資源進行協調和同步,以及決定如何進行錯誤恢復。但在分散式環境下,沒有一個可以來進行協調同步、資源分配以及進行故障恢復的節點,部分失敗一般是無法預測的,有時甚至無法知道請求任務是否有被成功處理。

所以在開發需要進行網路通訊的介面時(RPC 或者非同步訊息),需要考慮到部分失敗,讓整個系統接受部分失敗並做好容錯機制,比如在網路傳輸失敗時要能在服務層處理好,並且給使用者一個好的反饋。

(2)網路延遲

網路是機器間通訊的唯一路徑,但這條唯一路徑並不是可靠的,而且分散式系統中一定會存在網路延遲,網路延遲會影響系統對於“超時時間”、“心跳機制”的判斷。如果是使用非同步的系統模型,還會有各種環節可能出錯:可能請求沒有成功發出去、可能遠端節點收到請求但是處理請求的程序突然掛掉、可能請求已經處理了但是在 Response,可能在網路上傳輸失敗(如資料包丟失)或者延遲,而且網路延遲一般無法辨別。

即使是 TCP 能夠建立可靠的連線,不丟失資料並且按照順序傳輸,但是也處理不了網路延遲。對於網路延遲敏感的應用,使用 UDP 會更好,UDP 不保證可靠傳輸,也不會丟失重發,但是可以避免一些網路延遲,適合處理音訊和視訊的應用。

(3)沒有共享記憶體、鎖、時鐘

分散式系統的節點間沒有共享的記憶體,不應理所當然認為本地物件和遠端物件是同一個物件。分散式系統也不像單機器的情況,可以共享同一個 CPU 的訊號量以及併發操作的控制;也沒有共享的物理時鐘,無法保證所有機器的時間是絕對一致的。時間的順序是很重要的,誇張一點說,假如對於一個人來說,從一個時鐘來看是 7 點起床、8 點出門,但可能因為不同時鐘的時間不一致,從不同節點上來看可能是 7 點出門、8 點起床。

在分散式環境下開發,需要我們能夠有意識地進行問題識別,以上只是舉例了一部分場景和問題,不同的介面實現,會在分散式環境下有不同的效能、擴充套件性、可靠性的表現。下面會繼續上述的問題進行探討,如何實現一個更可靠的系統。

概念和處理模型

對於上述分散式系統中的一些問題,可以針對不同的特徵做一些容錯和處理,下面主要看一下錯誤檢測以及時間和順序的處理模型。在實際處理中,一般是綜合多個方案以及應用的特點。

錯誤檢測

對於部分失敗,需要一分為二的看待。

節點的部分失敗,可以通過增加錯誤檢測的機制,自動檢測問題節點。在實際的應用中,比如有通過 Load Balancer,自動排除問題節點,只將請求傳送給有效的節點。對於需要有 Leader 選舉的服務叢集來說,可以引入實現 Leader 選舉的演算法,如果 Leader 節點掛掉了,其餘節點能選舉出新的 Leader。實現選舉演算法也屬於共識問題,在後續文章中會再涉及到幾種演算法的實現和應用。

網路問題:由於網路的不確定性,比較難說一個節點是否真正的“在工作”(有可能是網路延遲導致的錯誤),通過新增一些反饋機制可以在一定程度確定節點是否正常執行,比如:

  • 健康檢查機制,一般是通過心跳檢測來實現的,比如使用 Docker 的話,Consul、Eureka 都有健康檢查機制,當傳送心跳請求發現容器例項已經無法迴應時,可以認為服務掛掉了,但是卻很難確認在這個 Node/Container 中有多少資料被正確的處理了。
  • 如果一個節點的某個程序掛了,但是整個節點還可以正常執行。在使用微服務體系中是比較常見的,一臺機器上部署著很多容器例項,其中個容器例項(即相當於剛才描述掛掉的程序)掛掉了,可以有一個方式去通知其他容器來快速接管,而不用等待執行超時。比如 Consul 通過 Gossip 協議進行多播,關於 Consul,可以參考這篇 Docker 容器部署 Consul 叢集 內容。在批處理系統中,HBase 也有故障轉移機制。

在實際做錯誤檢測處理時,除了需要 節點、容器 做出積極的反饋,還要有一定的重試機制。重試的實現可以基於網路傳輸協議,如使用 TCP 的 RTT;也可以在應用層實現,如 Kafka 的 at-least-once 的實現。基於 Docker 體系的應用,可以使用 SpringCloud 的 Retry,結合 Hytrix、Ribbon 等。對於需要給使用者反饋的應用,不太建議使用過多重試,根據不同的場景進行判斷,更多的時候需要應用做出積極的響應即可,比如使用者的“個人中心頁面”,當 User 微服務掛了,可以給個預設頭像、預設暱稱,然後正確展示其他資訊,而不是反覆請求 User 微服務。

時間和順序

在分散式系統中,時間可以作為所有執行操作的順序先後的判定標準,也可以作為一些演算法的邊界條件。在分散式系統中決定操作的順序是很重要的,比如對於提供分散式儲存服務的系統來說,Repeated Read 以及 Serializable 的隔離級別,需要確定事務的順序,以及一些事件的因果關係等。

物理時鐘

每個機器都有兩個不同的時鐘,一個是 time-of-day,即常用的關於當前的日期、時間的資訊,例如,此時是 2018 年 6 月 23 日 23:08:00,在 Java 中可以用 System.currentTimeMillis() 獲取;另一個是 Monotonic 時鐘,代表著單調遞增的時間,一般是測量時間間距,在 Java 中呼叫 System.nanoTime() 可以獲得 Monotonic 時間,常常用於測量一個本地請求的返回時間,比如 Apache commons 中的 StopWatch 的實現。

在分散式環境中,一般不會使用 Monotonic,測量兩臺不同的機器的 Monotonic 的時間差是無意義的。

不同機器的 time-of-day 一般也不同,就算使用 NTP 同步所有機器時間,也會存在毫秒級的差,NTP 本身也允許存在前後 0.05% 的誤差。如果需要同步所有機器的時間,還需要對所有機器時間值進行監控,如果一個機器的時間和其他的有很大差異,需要移除不一致的節點。因為能改變機器時間的因素比較多,比如無法判斷是否有人登上某臺機器改變了其本地時間。

雖然全域性時鐘很難實現,並且有一定的限制,但基於全域性時鐘的假設還是有一些實踐上的應用。比如 Facebook Cassandra 使用 NTP 同步時間來實現 LWW(Last Write Win)。Cassandra 假設有一個全域性時鐘,並基於這個時鐘的值,用最新的寫入覆蓋舊值。當然時鐘上的最新不代表順序的最新,LWW 區分不了實際順序;另外還有如 Google Spanner 使用 GPS 和原子時鐘進行時間同步,但節點之間還是會存在時間誤差。

邏輯時鐘

在分散式系統中,因為全域性時鐘很難實現,並且像 NTP 同步過程,也會受到網路傳輸時間的影響,一般不會使用剛才所述的全域性同步時間,當然也肯定不能使用各個機器的本地時間。對於需要確認操作執行順序的時候,不能簡單依賴一個基於 time-of-day 的 timestamps,所以需要一個邏輯時鐘,來標記一些事件順序、操作順序的序列號。常見的方式是給所有操作加上遞增的計數器。

這種所有操作都新增一個全域性唯一的序列號的方式,提供了一種全域性順序的保證,全域性順序也包含了因果順序一致的概念。關於分散式一致性的概念和實現會在後續文章詳細介紹,我們先將關注點回歸到時間和順序上。下面看兩種典型的邏輯時鐘實現。

(1)Lamport Timestamps

Lamport timestamps 是 Leslie Lamport 在 1978 年提出的一種邏輯時鐘的實現方法。Lamport Timestamps 的演算法實現,可以理解為基於每個節點的一對值(NodeId,Counter)的全域性順序的同步。在叢集中的每個節點(Node)都有一個唯一標識,並且每個 Node 都持有一個本地的對於所有操作順序的一個 Counter(計數器)。

Lamport 實現的核心思想就是把事件分成三類(節點內處理的事件、傳送事件、接收事件):

  • 如果一個節點處理一個事件,節點 counter +1。
  • 如果是傳送一個訊息事件,則在訊息中帶上 counter 值。
  • 如果是接收一個訊息事件,則更新 counter = max(本地 counter,接收的訊息中帶的 counter) +1。

簡單畫了個示例如下圖:

lamport timestamps example

初始的 counter 都是 0,在 Node1 接收到請求,處理事件時 counter+1(C:1表示),並且再發送訊息帶上 C:1。

在 Node1 接受 ClientA 請求時,本地的 Counter=1 > ClientA 請求的值,所以處理事件時 max(1,0)+1=2(C:2),然後再發送訊息,帶上 Counter 值,ClientA 更新請求的最大 Counter 值 =2,並在下一次對 Node2 的事件傳送時會帶上這個值。

這種序列號的全域性順序的遞增,需要每次 Client 請求持續跟蹤 Node 返回的 Counter,並且再下一次請求時帶上這個 Counter。lamport 維護了全域性順序,但是卻不能更好的處理併發。在併發的情況下,因為網路延遲,可能導致先發生的事件被認為是後發生的事件。如圖中紅色的兩個事件屬於併發事件,雖然 ClientB 的事件先發出,但是因為延遲,所以在 Node 1 中會先處理 ClientA,也即在 Lamport 的演算法中,認為 Node1(C:4) happens before Node1(C:5)。

Lamport Timestamps 還有另一種併發衝突事件:不同的 NodeId,但 Counter 值相同,這種衝突會通過 Node 的編號的比較進行併發處理。比如 Node2(C:10)、Node1(C:10) 是兩個併發事件,則認為 Node2 的時間順序值 > Node1 的序列值,也就認為 Node1(C:10) happens before Node2(C:10)。

所以可見,Lamport 時間戳是一種邏輯的時間戳,其可以表示全域性的執行順序,但是無法識別併發,以及因果順序,併發情況無法很好地處理 偏序

(2)Vector Clock

Vector Clock 又叫向量時鐘,跟 Lamport Timestamps 比較類似,也是使用 SequenceNo 實現邏輯時鐘,但是最主要的區別是向量時鐘有因果關係,可以區分兩個併發操作,是否一個操作依賴於另外一個。

Lamport Timestamps 通過不斷把本地的 counter 更新成公共的 MaxCounter 來維持事件的全域性順序。Vector Clock 則各個節點維持自己本地的一個遞增的 Counter,並且會多記錄其他節點事件的 Counter。通過維護了一組 [NodeId,Counter] 值來記錄事件的因果順序,能更好得識別併發事件,也即,Vector Clock 的 [NodeId,Counter] 不僅記錄了本地的,還記錄了其他 Node 的 Counter 資訊。

Vector Clock 的 [NodeId,Counter] 更新規則:

  • 如果一個節點處理一個事件,節點本地的邏輯時鐘的 counter +1。
  • 當節點發送一個訊息,需要包含所有本地邏輯時鐘的一組 [NodeId,Counter] 記錄值。
  • 接受一個事件訊息時, 更新本地邏輯時鐘的這組 [NodeId,Counter] 值:
    • 讓這組 [NodeId,Counter] 值中每個值都是 max(本地 counter,接收的訊息中的counter)。
    • 本地邏輯時鐘 counter+1。

如下圖簡單示意了 Vector Clock 的時間順序記錄:

Vector clock example

三個 Node,初始 counter 都是 0。NodeB 在處理 NodeC 的請求時,記錄了 NodeC 的 Counter=1,並且處理事件時,本地邏輯時鐘的 counter=0+1,所以 NodeB 處理事件時更新了本地邏輯時鐘為 [B:1,C:1]。在事件處理時,通過不斷更新本地的這組 Counter,就可以根據一組 [NodeId,Counter] 值來確定請求的因果順序了,比如對於 NodeB,第二個處理事件 [A:1,B:2,C:1] 早於第三個事件:[A:1,B:3,C:1]。

在數值衝突的時候,如圖中粉色剪頭標記的。NodeC 的 [A:2,B:2,C:3] 和 NodeB[A:3,B:4,C:1]。C:3 > C:1、B:2 < B:4,種情況認為是沒有因果關係,屬於同時發生。

Vector Clock 可以通過各個節點的時間序列值的一組值,識別兩個事件的先後順序。Vector 提供了發現數據衝突的方式,但是具體怎樣解決衝突需要由發現衝突的節點決定,比如可以將併發衝突拋給 Client 決定,或者用 Quorum-NRW 演算法進行讀取修復(Read Repair)。

Amazon Dynamo 就是通過 Vector Clock 來做併發檢測的一個很好的分散式儲存系統的例子。對於複製節點的資料衝突使用了 Quorum NRW 決議,以及讀修復(Read Repair)處理最新更新資料的丟失,詳細實現可以參考這篇論文 Dynamo: Amazon’s Highly Available Key-value Store ,Dynamo 是典型的高可用、可擴充套件的,提供弱一致性(最終一致性)保證的分散式 K-V 資料儲存服務。後續文章再介紹 Quorums 演算法時,也會再次提到。Vector Clock 在實際應用中,還會有一些問題需要處理,比如如果一個上千節點的叢集,那麼隨著時間的推移,每個 Node 將需要記錄大量 [NodeId,Counter] 資料。Dynamo 的解決方案是通過新增一個 timestamp 記錄每個 Node 更新 [NodeId,Counter] 值的時間,以及一個設定好的閥值,比如說閥值是 10,那麼本地只儲存最新的 10 個 [NodeId,Counter] 組合資料。

小結

本文引出了一些分散式系統的常見問題以及一些基礎的分散式系統模型概念,微服務的架構目前已經被更廣泛得應用,但微服務面臨的問題其實也都是經典的分散式場景的問題。本文在分散式系統的問題中,主要介紹了關於錯誤檢測以及時間和順序的處理模型。

關於時間和順序的問題處理中,沒有一個絕對最優的方案,Cassandra 使用了全域性時鐘以及 LWW 處理順序判定;Dynamo 使用了 Vector clock 發現衝突,加上 Quorum 演算法處理事件併發。這兩個儲存系統都有很多優秀的分散式系統設計和思想,在後續文章中會更詳細的介紹資料複製、一致性、共識演算法等。

參考資料