1. 程式人生 > 實用技巧 >Java架構之微服務下微服務架構廣告設計系統實戰分散式微服務架構體系詳解

Java架構之微服務下微服務架構廣告設計系統實戰分散式微服務架構體系詳解

課程介紹

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

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

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

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

專家推薦

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

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

作者介紹

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

課程內容

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

微服務架構的演變

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

單體架構

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

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)部分失敗

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

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

所以在開發需要進行網路通訊的介面時(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% 的誤差。如果需要同步所有機器的時間,還需要對所有機器時間值進行監控,如果一個機器的時間和其他的有很大差異,需要移除不一致的節點。因為能改變機器時間的因素比較多,比如無法判斷是否有人登上某臺機器改變了其本地時間。

雖然全域性時鐘很難實現,並且有一定的限制,但基於全域性時鐘的假設還是有一些實踐上的應用。比如 FacebookCassandra使用 NTP 同步時間來實現 LWW(Last Write Win)。Cassandra 假設有一個全域性時鐘,並基於這個時鐘的值,用最新的寫入覆蓋舊值。當然時鐘上的最新不代表順序的最新,LWW 區分不了實際順序;另外還有如 GoogleSpanner使用 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。

簡單畫了個示例如下圖:

初始的 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 的時間順序記錄:

三個 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 演算法處理事件併發。這兩個儲存系統都有很多優秀的分散式系統設計和思想,在後續文章中會更詳細的介紹資料複製、一致性、共識演算法等。

參考資料

點選瞭解更多《案例上手分散式微服務架構》

第02課:資料儲存

前言

微服務架構下,很適合用 DDD(Domain-Drive Design)思維來設計各個微服務,使用領域驅動設計的理念,工程師們的關注點需要從 CRUD 思維中跳出來,更多關注通用語言的設計、實體以及值物件的設計。至於資料倉庫,會有更多樣化的選擇。分散式系統中資料儲存服務是基礎,微服務的領域拆分、領域建模可以讓資料儲存方案的選擇更具靈活性。

不一定所有的微服務都需要有一個底層的關係型資料庫作為實體物件例項的儲存。以一個簡單的電商系統為例:“使用者微服務”和“商品微服務”都分別需要關係型資料庫儲存結構化的關聯資料。但比如有一個“關聯推薦微服務“需要進行使用者購買、加購物車、訂單、瀏覽等多維度的資料整合,這個服務不能將其他所有訂單,使用者、商品等服務的資料冗餘進來,這種場景可以考慮使用圖形資料庫。又比如有一個“驗證碼微服務”,儲存手機驗證碼、或者一些類似各種促銷活動發的活動碼、口令等,這種簡單的資料結構,而且讀多寫少,不需長期持久化的場景,可以只使用一個 K-V(鍵值對)資料庫服務。

本文先簡單介紹下適合微服務架構體系的一些分散式資料儲存方案,然後深入介紹下這些儲存服務的資料結構實現,知其然知其所以然。後續文章會繼續介紹分散式資料儲存的複製、分割槽。

資料儲存型別介紹

不同的資料儲存引擎有著不同的特徵,也適合不同的微服務。在做最初的選型時,需要先根據對整體業務範圍的判斷,選擇儘量普適於大多數微服務的儲存。例如,初創型企業,需要綜合考慮成本節約以及團隊的知識掌握度等問題,MySQL 是比較常見的選擇,電商型別的微服務應用更適合 InnoDB 引擎(事務、外來鍵的支援、行鎖的效能),雖然 InnoDB 的讀效能會比 MyISAM 差,但是讀場景有很多可以優化的方案,如搜尋引擎、分散式快取、本地快取等。

下面會以不同場景為例,整理一部分常用的資料儲存引擎,實際的企業應用中會針對不同場景、服務特徵綜合使用多種儲存引擎。

關係型資料庫

儲存結構化資料,以及需要更多維度關聯,需要給使用者提供豐富的實時查詢場景時,應該使用關係型資料庫。從開源以及可部署高可用性叢集的方面來看,MySQLPostgreSQL都是不錯的選擇。PostgreSQL 的歷史更為悠久,兩者都有很多大網際網路公司如 Twitter、Facebook、Yahoo 等部署著大規模分散式儲存叢集,叢集的複製、分割槽方案會在後續文章詳細介紹。

NoSQL

NoSQL 即 Not Only SQL,其概念比關係型資料庫更新,NoSQL 為資料的查詢提供了更靈活、豐富的場景。下面簡單列舉了一些 NoSQL 資料庫及其應用場景。工程師不一定需要掌握所有的 NoSQL 資料庫的細節,對於不同的領域模型的設計,能有更多的靈感會更好。

KeyValue 儲存

KeyValue 可以說是 NoSQL 中比較簡單的一族,大多數操作只有 get()、put(),基礎的資料格式也都是簡單的 Key-Value。

目前比較流行的鍵值儲存服務有RedisMemcached以及上篇文中提到的Dynamo。其中 Redis 有 Redis Cluster 提供了支援 Master 選舉的高可用性叢集。Dynamo 也有分散式高可用叢集,基於 Gossip 協議的節點間故障檢測,以及支援節點暫時、永久失效的故障恢復,這兩者為了保證高可用以及效能,犧牲了強一致性的保證,但是都支援最終一致性。Memcached 提供了高效能的純基於記憶體的 KV 儲存,並且提供 CAS 操作來支援分散式一致性,但 Memcached 沒有官方提供的內建叢集方案,需要使用一些代理中介軟體,如Magento來部署叢集。

在實際選擇時,如果需要快取記憶體的效能並且可以接受快取不被命中的情況,以及可以接受 Memcached 服務例項重啟後資料全部丟失,可以選擇 Memcached。用 Memcached 做二級快取來抗住一些高 QPS 的請求是很適合的,比如對於一些 Hot 商品的資訊,可以放到 Memcached 中,緩解 DB 壓力。

如果既需要有資料持久化的需求,也希望有好的快取效能,並且會有一些全域性排序、資料集合並等需求,可以考慮使用 Redis。Redis 除了支援 K-V 結構的資料,還支援 list、set、hash、zset 等資料結構,可以使用 Redis 的SET key value操作實現一些類似手機驗證碼的儲存,對於需要按照 key 值排序的 kv 資料可以用ZADD key score member。利用 Redis 的單執行緒以及持久化特性,還可以實現簡單的分散式鎖,具體可以參考筆者之前寫的這篇《基於 Redis 實現分散式鎖實現》文章。

文件型資料庫

面向文件的資料庫可以理解成 Value 是一個文件型別資料的 KV 儲存,如果領域模型是個檔案型別的資料、並且結構簡單,可以使用文件型資料庫,比較有代表性的有MongoDBCouchDB。MongoDB 相比可用性,更關注一致性,Value 儲存格式是內建的 BSON 結構,CouchDB 支援內建 JSON 儲存,通過 MVCC 實現最終一致性,但保證高可用性。

如果你需要的是一個高可用的多資料中心,或者需要 Master-Master,並且需要能承受資料節點下線的情況,可以考慮用 CouchDB。如果你需要一個高效能的,類似儲存文件型別資料的 Cache 層,尤其寫入更新比較多的場景,那就用 MongoDB 吧。另外,2018 年夏天可以期待下,MongoDB 官方宣佈即將釋出的 4.0 版本,支援跨副本集(Replica set)的 ACID 事務,4.2 版本將支援跨叢集的事務,詳情可以關注 MongoDB 的Beta 計劃

圖形資料庫

在現實世界中,一個圖形的構成主要有“點”和“邊”,在圖形資料庫中也是一樣,只不過點和邊有了抽象的概念,“點”代表著一個實體、節點,“邊”代表著關係。開源的Neo4j是可以支援大規模分散式叢集的圖形資料庫。一般被廣泛用於道路交通應用、SNS 應用等,Neo4j 提供了獨特的查詢語言CypherQueryLanguage。

為了直觀瞭解 Neo4j 的資料結構,可以看下這個示例(在執行 Neo4j 後,官方的內建資料示例),圖中綠色節點代表“Person”實體,中間的有向的剪頭連線就是代表節點之間的關係“Knows”。

通過以下 CQL 語句就可以查詢所有 Knows、Mike 的節點以及關係:

MATCH p=()-[r:KNOWS]->(g) where g.name ='Mike' RETURN p LIMIT 25 

以上只是單個點和單維度關係的例子,在實際中 Person 實體間可能還存在 Follow、Like 等關係,如果想找到 Knows 並且 Like Mike,同時又被 Jim Follow 的Person。在沒有圖形資料庫的情況下,用關係型資料庫雖然也可以查詢各種關聯資料,但這需要各種表 join、union,效能差而且需要寫很多 SQL 程式碼,用 CQL 只要一行即可。

在 SpringBoot 工程中,使用 Springboot-data 專案,可以很簡單地和 Neo4j 進行整合,官方示例可以直接 checkout 檢視java-spring-data-neo4j

文件資料庫一般都是很少有資料間的關聯的,圖形資料庫就是為了讓你快速查詢一切你想要的關聯。如果想更進一步瞭解 Neo4j,可以直接下載Neo4j 桌面客戶端,一鍵啟動、然後在瀏覽器輸入http://localhost:7474/browser/就可以用起來了。

列族資料庫

列族資料庫一般都擁有大規模的分散式叢集,可以用來做靈活的資料分析、處理資料報表,尤其適合寫多讀少的場景。列族和關係型資料庫的差別,從應用角度來看,主要是列族沒有 Schema 的概念,不像關係型資料庫,需要建表的時候定義好每個列的欄位名、欄位型別、欄位大小等。

列族資料庫中目前比較廣泛應用的有 Hbase,Hbase 是基於 GoogleBigTable設計思想的開源版。BigTable 雖然沒開源,但是其論文Bigtable: A Distributed Storage System for Structured Data提供了很多設布式列族 DB 的實現邏輯。另外 FacebookCassandra也是一個寫效能很好的列族資料庫,其參考了 Dynamo 的分散式設計以及 BigTable 的資料儲存結構,支援最終一致性,適合跨地域的多資料中心的分散式儲存。不過 Cassandra 中文社群相對薄弱,國內還是 Hbase 的叢集更為廣泛被部署。

儲存服務的資料結構

在瞭解了一些分散式資料儲存的產品之後,為了能更深地理解,下面會對分散式儲存引擎的一些常用資料結構做進一步介紹。一臺計算機,可以作為資料儲存的地方就是記憶體、磁碟。分散式的資料儲存就是將各個計算機(Node)的記憶體和磁碟結合起來,不同型別的儲存服務使用的核心資料結構也會不同。

雜湊表

雜湊表是一種比較簡單 K-V 儲存結構,通過雜湊函式將 Key 雜湊開,Key 雜湊值相同的 Value 一般會以單鏈表結構儲存。雜湊表查詢效率很高,常用於記憶體型儲存服務如 Memcached、Redis。Redis 除了雜湊表,因為其支援的操作的資料型別很多,所以還有像 Skiplist、SDS、連結串列等儲存結構,並且 Redis 的雜湊表結構可以通過自動再雜湊進行擴容。

雜湊表一般儲存在記憶體中,隨著雜湊表資料增多,會影響查詢效率,並且記憶體結構也沒法像磁碟那樣可以持久化以及進行資料恢復。Redis 預設提供了RDB持久化方案,定時持久化資料到 RDB。用 RDB 來做資料恢復、備份是很合適的方案,但是因為其定期執行,所以無法保證恢復資料的一致性、完整性。Redis 還支援另一種持久化方案——基於AOF(Append Only File)方式,對每一次寫操作進行持久化,AOF 預設不啟用,可以通過修改redis.conf啟用,AOF 增加了 IO 負荷,比較影響寫效能,適合需要保證一致性的場景。

SSTable

在我們平常在 Linux 上分析日誌檔案的時候,比如用 grep、cat、tail 等命令,其實可以想象成在 Query 一個持久化在磁碟的 log 檔案。我們可以用命令輕鬆查詢以及分析磁碟檔案,查詢一個記錄的時間複雜度是 O(n) 的話(因為要遍歷檔案),查詢兩個記錄就是 2*O(n),並且如果檔案很大,我們沒法把檔案 load 到記憶體進行解析,也沒法進行範圍查詢。

SSTable(Sorted String Table) 就解決了排序和範圍查詢的問題,SSTable 將檔案分成一個一個 Segment(段),不同的 Segment File 可能有相同的值,但每個 Segement File 內部是按照順序儲存的。不過雖然只是將檔案分段,並且按照內容順序(Sorted String)儲存可以解決排序,但是查詢磁碟檔案的效率是很低的。

為了能快速查詢檔案資料,可以在記憶體中附加一個 KV 結構的索引:(key-offset)。key 值是索引的值並且也是有序的,Offset 指向 Segment File 的實際儲存位置(地址偏移)。

如下圖簡單畫了一個有記憶體 KV 儲存的 SSTable 資料結構:

這個 k-v 結構的儲存結構又叫 Memtable,因為 Memtable 的 key 也是有序的,所以為了實現記憶體快速檢索,Memtable 本身可以使用紅黑樹、平衡二叉樹、skip list 等資料結構來實現。Ps:B-Tree、B+Tree 的結構適合做大於記憶體的資料的索引儲存(如 MySQL 使用 B+ 樹實現索引檔案的儲存),所以其更適合磁碟檔案系統,一般不會用來實現 Memtable。

SSTable 也是有些侷限性的,記憶體的空間是有限的,隨著檔案數越來越多,查詢效率會逐漸降低。為了優化查詢,可以將 Segment File 進行合併,減少磁碟 IO,並且一定程度持久化 Memtable(提高記憶體查詢效率)——這就是LSM-tree(Log-structured Merge-Tree)。LSM-tree 最初由 Google 釋出的Bigtable 的設計論文提出,目前已經被廣泛用於列族資料庫如 HBase、Cassandra,並且 Google 的LevelDB也是用 LMS-tree 實現,LevelDB 的 Memtable 使用的是 skip list 資料結構。

這種提供 SSTable 合併、壓縮以及定期 flush Memtable 到磁碟的優化,使 LMS-tree 的寫入吞吐量高,適合高寫場景。下面以 Cassandra 為例介紹下 LMS-tree 的典型資料流。

(1)Cassandra LMS-tree 寫

資料先寫到 Commit Log 檔案中(Commit Log 用WAL實現)WAL 保證了故障時,可以恢復記憶體中 Memtable 的資料。

資料順序寫入 Memtable 中。

隨著 Memtable Size 達到一定閥值或者時間達到閥值時,會 flush 到 SSTable 中進行持久化,並且在 Memtable 資料持久化到 SSTable 之後,SSTables 都是不可再改變的。

後臺程序會進行 SSTable 之間的壓縮、合併,Cassendra 支援兩種合併策略:對於多寫的資料可以使用 SizeTiered 合併策略(小的、新的 SSTable 合併到大的、舊的 SSTable 中),對於多讀的資料可以使用 Leveled 合併策略(因為分層壓縮的 IO 比較多,寫多的話會消耗 IO),詳情可以參考when-to-use-leveled-compaction

(2)Cassandra LMS-tree 讀

先從 Memtable 中查詢資料。

Bloom Filter中讀取 SStable 中資料標記,Bloom Filter 可以簡單理解為一個記憶體的 set 結構,儲存著被“刪除”的資料,因為剛才介紹到 SSTable 不能改變,所以一些刪除之後的資料放到這個 set 中,讀請求需要從這個標記著拋棄物件們的集合中讀取“不存在”的物件,並在結果中過濾。對於 SSTables 中一些過期的,會在合併時被清除掉。

從多個 SSTables 中讀取資料。

合併結果集、返回。另外,對於“更新”操作,是直接更新在 Memtable 中的,所以結果集會優先返回 Memtable 中的資料。

BTree、B + Tree

BTree 和 B + Tree 比較適合磁碟檔案的檢索,一般用於關係型資料庫的索引資料的儲存,如 Mysql-InnoDB、PostgreSQL。為了提高可用性,一般 DB 中都會有一個 append-only 的 WAL(一般也叫 redo-log)用來恢復資料,比如 MySQL InnoDB 中用 binlog 記錄所有寫操作,binlog 還可以用於資料同步、複製。

使用 Btree、B+Tree 的索引需要每個資料都寫兩次,一次寫入 redo-log、一次將資料寫入 Tree 中對應的資料頁(Page)裡。LMS-tree 結構其實需要寫入很多次,因為會有多次的資料合併(後臺程序),因為都是順序寫入,所以寫入的吞吐量更好,並且磁碟空間利用率更高。而 B 樹會有一些空的 Page 沒有資料寫入、空間利用率較低。讀取的效率來說,Btree 更高,同樣的 key 在 Btree 中儲存在一個固定的 Page 中,但是 LSM-tree 可能會有多個 Segment File 中儲存著同個 Key 對應的值。

小結

本篇介紹了很多分散式儲存服務,在實際的開發中,需要結合領域服務的特點選擇。有的微服務可能只需要一個Neo4j,有的微服務只需要 Redis。微服務的架構應該可以讓領域服務的儲存更加靈活和豐富,在選擇時可以更加契合領域模型以及服務邊界。

文章後半部分介紹了部分儲存服務的資料結構。瞭解了實現的資料結構可以讓我們更深刻理解儲存引擎本身。從最簡單的 append-only 的檔案儲存,再到雜湊表、SSTable、BTree,基本涵蓋了目前流行的儲存服務的主流資料結構。如果想深入理解 LSM-tree,可以讀一下 BigTable 的那篇經典論文。

除了資料庫服務,像 Lucene 提供了全文索引的搜尋引擎服務,