今天咱們來討論分佈式通訊技術。java
咱們以前在講分佈式資源調度的時候,把分佈式系統中的各個節點與操做系統的進程作了類比。咱們知道,操做系統的進程之間因爲須要數據的交換,是須要進程通訊機制的。那麼同理,分佈式系統之間一樣須要通訊。在業務層面,每一個分佈式系統通常都承載着一個微服務,因此,微服務之間也必定是須要通訊的。好比,咱們各條業務線均須要查詢用戶中心微服務的數據等等。咱們經常使用的通訊方式有三種:RPC、發佈-訂閱、消息隊列。算法
在傳統的B/S模式中,服務端會對外暴露接口,而後客戶端經過調用這個接口來完成兩者之間的通訊。那麼在分佈式系統中,咱們一樣也能夠採用這種模式。可是,B/S 架構是基於 HTTP 協議實現的,每次調用接口時,都須要先進行 HTTP 請求。這樣既繁瑣又浪費時間,不適用於有低時延要求的大規模分佈式系統,因此遠程調用的實現大多采用更底層的網絡通訊協議。咱們先用一張圖俯瞰一下RPC的架構:
在這裏,訂單系統進程並不須要知道底層是如何傳輸的,在用戶眼裏,遠程過程調用和調用一次本地服務沒什麼不一樣。這,就是 RPC 的核心。即圖中的第3步和第8步,對咱們調用方是透明的。與咱們常用的接口調用不一樣,圖中的網絡通訊基本是基於TCP協議本身封裝的一些協議。這樣作能夠約定通訊雙方的數據格式,從而讓客戶端封包和服務端解包更加快速,更加適用於分佈式系統。這裏的通訊協議封裝可參考Redis的RESP協議與FastCGI協議。spring
假設咱們要本身去實現一個RPC通訊框架,咱們應該如何實現呢?假如咱們用4個調用方與4個服務提供方,咱們該如何管理他們呢?
首先,咱們最容易想到的,就是服務提供方爲服務調用方,提供相關的SDK,服務調用方直接引入SDK便可發起RPC調用請求,而SDK內部具體是利用什麼協議,調用方並不關心。這是一種方案。可是,隨着服務提供方和服務調用方愈來愈多,服務調用關係會越發複雜。假設服務提供方有 n個, 服務調用方有 m 個,則調用關係可達 n*m,這會致使系統的通訊量很大,SDK就顯得力不從心了。此時,你可能會想到,在計算機領域,全部的問題均可以經過增長一箇中間層來解決。那麼,咱們爲何不使用一個服務註冊中心來進行統一管理呢,這樣調用方只須要到服務註冊中心去查找相應的地址便可,並不關心有多少個服務提供方,從而實現了服務調用方與服務提供方的解耦:
Dubbo 在引入服務註冊中心的基礎上,又加入了監控中心組件(用來監控服務的調用狀況,以方便進行服務治理),實現了一個 RPC 框架。以下圖所示,Dubbo 的架構主要包括 4 部分:數據庫
下面是Dubbo官網給出的一個調用方的Demo。首先是對須要調用的服務在服務註冊中心的地址進行配置:apache
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> <!-- consumer's application name, used for tracing dependency relationship (not a matching criterion), don't set it same as provider --> <dubbo:application name="demo-consumer"/> <!-- use multicast registry center to discover service --> <dubbo:registry address="multicast://224.5.6.7:1234"/> <!-- generate proxy for the remote service, then demoService can be used in the same way as the local regular interface --> <dubbo:reference id="demoService" check="false" interface="org.apache.dubbo.demo.DemoService"/> </beans>
而後,在業務代碼中調用剛剛配置好的服務提供方地址便可。咱們再也不須要SDK了:segmentfault
import org.springframework.context.support.ClassPathXmlApplicationContext; import org.apache.dubbo.demo.DemoService; public class Consumer { public static void main(String[] args) throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"META-INF/spring/dubbo-demo-consumer.xml"}); context.start(); // Obtaining a remote service proxy DemoService demoService = (DemoService)context.getBean("demoService"); // Executing remote methods String hello = demoService.sayHello("world"); // Display the call result System.out.println(hello); } }
發佈-訂閱的思想在生活中隨處可見。好比咱們吃雞的時候,通常是4我的組隊開黑,咱們在分佈式系統中能夠比做4個節點。舉一個至關經典的場景,好比我跳了機場,資源不少,有5.56的子彈、7.62的子彈等。因而我就和隊友說,我多5.56和7.62子彈,誰須要的話和我說一下。可是有些隊友去打野了,就會比較窮,他們就會和我說,我要5.56子彈或者我要7.62子彈。而後,我就會找到這個窮隊友,而後把相應的5.56和7.62子彈分給他們,這樣就完成了一次發佈-訂閱的流程。其中,」我就和隊友說,我多5.56和7.62子彈,誰須要的話和我說一下「,這個就是將」我多子彈「這個消息事件發佈出去,而後很窮的隊友說」我須要xxx子彈「,就至關於訂閱我發佈的這個消息事件,而後我就會把子彈給到他們,這個子彈就至關於咱們的消息,這樣就完成了一次發佈-訂閱模型的通訊:
其中,生產者能夠發送消息到中心,而消息中心一般以主題(Topic)進行劃分,每條消息都會有相應的主題,它表明該條消息的類型。訂閱該主題的全部消費者都可得到該消息進行消費。這裏咱們的5.56子彈與7.62子彈,就至關於兩個topic,咱們能夠訂閱其中一個topic,來得到咱們須要的子彈類型。網絡
Kafka是一個典型的發佈訂閱消息系統,其系統架構也是包括生產者、消費者和消息中心三部分:
在Kafka中,爲了解決消息存儲的負載均衡和系統可靠性問題,因此引入了主題(topic)和分區(partition)的概念。topic的概念咱們剛纔講過了,它是一個邏輯概念,指的是消息類型或數據類型。那麼分區是基於topic而言的。一個topic的內容能夠被劃分紅多個分區,而這些分區又分佈在不一樣的集羣節點上,每一個分區的數據內容依賴數據同步機制,來確保每一個分區內部存儲數據的一致性:
每一個broker就表明了一個集羣中的物理節點。經過分區機制,咱們避免了「將數據都放在一個籃子裏」,將數據分散在不一樣的broker機器上,提升了系統的數據可靠性,且實現了負載均衡。
在圖中,還有一點不同的地方就是,有兩個消費者組成了一個消費組。那麼爲何要引入消費組呢?咱們知道,在消息過多的狀況下,單個消費者消費能力有限時,會致使消費效率太低,從而致使 Broker 存儲溢出,從而不得不丟棄一部分消息。Kafka爲了解決這個問題,因此引入了消費組,提升了消費的速度。
在Kafka中,除了基本的三要素以外,還使用了Zookeeper。ZooKeeper是一個提供了分佈式服務協同能力的第三方組件,用來協調和管理整個集羣中的Broker和Consumer,實現了Broker 和Consumer的解耦,併爲系統提供可靠性保證。Consumer 和 Broker 啓動時均會向 ZooKeeper 進行註冊,由 ZooKeeper 進行統一管理和協調。
ZooKeeper 中會存儲一些元數據信息,好比對於 Broker,會存儲主題對應哪些分區(Partition),每一個分區的存儲位置等;對於 Consumer,會存儲消費組(Consumer Group)中包含哪些 Consumer,每一個 Consumer 會負責消費哪些分區等。架構
消息隊列與發佈-訂閱模型比較類似,可是也有一些不一樣之處。接着咱們以前吃雞的例子來講,消息隊列並不關心誰須要什麼子彈,只把本身多的資源放到某個位置,讓隊友來拿就行了。若是隊友有須要,自取便可。消息隊列並不直接把資源分配到某個具體消費者,只負責發佈到消息隊列中,而後消費者各取所需。最典型的一個場景就是異步通訊。
舉個例子,用戶註冊須要寫數據庫、發送郵件,按照最簡單的同步通訊方式,那麼從用戶提交註冊到收到響應,須要等系統完成這兩個步驟,纔會給用戶返回註冊成功。若是發送郵件耗時很是之長,那麼用戶就得一直等下去:
以下圖所示,若是引入消息隊列,做爲註冊消息寫入數據庫和發送郵件、短信這三個組件間的中間通訊者,那麼這三個組件就能夠實現異步通訊、異步執行:
即用戶只須要在寫入數據庫以後,寫入發送郵件的消息隊列便可返回註冊成功,而並不須要等待真正的去發送郵件以後纔會返回。因此,咱們解除了註冊與發送郵件兩種操做之間的耦合,大大提升了註冊的響應速度。那你可能會問,若是發送郵件失敗了怎麼辦?咱們通常會在業務層寫一些重試邏輯,確保郵件發送成功以後,纔算成功消費。而隊列通常也會有持久化機制,確保消息不會丟失。
除了將同步轉化爲異步,消息隊列在高併發系統中也承擔着流量削峯的做用。對於流量控制,還有漏桶和令牌桶算法,感興趣的讀者能夠進一步去了解。併發
【分佈式系統遨遊】分佈式計算app
歡迎對本系列文章感興趣的讀者訂閱咱們的公衆號,關注博主下次不迷路~