【分佈式系統遨遊】分佈式通信

為什麼需要分佈式通信

我們之前在講分佈式資源調度的時候,把分佈式系統中的各個節點與操作系統的進程做了類比。我們知道,操作系統的進程之間由於需要數據的交換,是需要進程通信機制的。那麼同理,分佈式系統之間同樣需要通信。在業務層面,每個分佈式系統一般都承載著一個微服務,所以,微服務之間也一定是需要通信的。比如,我們各條業務線均需要查詢用戶中心微服務的數據等等。我們常用的通信方式有三種:RPC、發佈-訂閱、消息隊列。

RPC

在傳統的B/S模式中,服務端會對外暴露接口,然後客戶端通過調用這個接口來完成二者之間的通信。那麼在分佈式系統中,我們同樣也可以採用這種模式。但是,B/S 架構是基於 HTTP 協議實現的,每次調用接口時,都需要先進行 HTTP 請求。這樣既繁瑣又浪費時間,不適用於有低時延要求的大規模分佈式系統,所以遠程調用的實現大多采用更底層的網絡通信協議。我們先用一張圖俯瞰一下RPC的架構:

【分佈式系統遨遊】分佈式通信

在這裡,訂單系統進程並不需要知道底層是如何傳輸的,在用戶眼裡,遠程過程調用和調用一次本地服務沒什麼不同。這,就是 RPC 的核心。即圖中的第3步和第8步,對我們調用方是透明的。與我們經常使用的接口調用不同,圖中的網絡通信基本是基於TCP協議自己封裝的一些協議。這樣做可以約定通信雙方的數據格式,從而讓客戶端封包和服務端解包更加快速,更加適用於分佈式系統。這裡的通信協議封裝可參考Redis的RESP協議與FastCGI協議。

RPC的典型實現 - Dubbo

假設我們要自己去實現一個RPC通信框架,我們應該如何實現呢?假如我們用4個調用方與4個服務提供方,我們該如何管理他們呢?

【分佈式系統遨遊】分佈式通信

首先,我們最容易想到的,就是服務提供方為服務調用方,提供相關的SDK,服務調用方直接引入SDK即可發起RPC調用請求,而SDK內部具體是利用什麼協議,調用方並不關心。這是一種方案。但是,隨著服務提供方和服務調用方越來越多,服務調用關係會愈加複雜。假設服務提供方有 n個, 服務調用方有 m 個,則調用關係可達 n*m,這會導致系統的通信量很大,SDK就顯得力不從心了。此時,你可能會想到,在計算機領域,所有的問題都可以通過增加一箇中間層來解決。那麼,我們為什麼不使用一個服務註冊中心來進行統一管理呢,這樣調用方只需要到服務註冊中心去查找相應的地址即可,並不關心有多少個服務提供方,從而實現了服務調用方與服務提供方的解耦:

【分佈式系統遨遊】分佈式通信

Dubbo 在引入服務註冊中心的基礎上,又加入了監控中心組件(用來監控服務的調用情況,以方便進行服務治理),實現了一個 RPC 框架。如下圖所示,Dubbo 的架構主要包括 4 部分:

  • 服務提供方。服務提供方會向服務註冊中心註冊自己提供的服務。
  • 服務註冊中心。服務註冊與發現中心,負責存儲和管理服務提供方註冊的服務信息和服務調用方訂閱的服務類型等。
  • 服務調用方。根據服務註冊中心返回的服務所在的地址列表,通過遠程調用訪問遠程服務。
  • 監控中心。統計服務的調用次數和調用時間等信息的監控中心,以方便進行服務管理或服務失敗分析等。

下面是Dubbo官網給出的一個調用方的Demo。首先是對需要調用的服務在服務註冊中心的地址進行配置:

<code>


     
    
     
    
     
    
/<code>

然後,在業務代碼中調用剛剛配置好的服務提供方地址即可。我們不再需要SDK了:

<code>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);
    }
}/<code>

發佈-訂閱

發佈-訂閱的思想在生活中隨處可見。比如我們吃雞的時候,一般是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是一個典型的發佈訂閱消息系統,其系統架構也是包括生產者、消費者和消息中心三部分:

【分佈式系統遨遊】分佈式通信

在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 會負責消費哪些分區等。

消息隊列

消息隊列與發佈-訂閱模型比較相似,但是也有一些不同之處。接著我們之前吃雞的例子來說,消息隊列並不關心誰需要什麼子彈,只把自己多的資源放到某個位置,讓隊友來拿就好了。如果隊友有需要,自取即可。消息隊列並不直接把資源分配到某個具體消費者,只負責發佈到消息隊列中,然後消費者各取所需。最典型的一個場景就是異步通信。比如用戶註冊需要寫數據庫、發送郵件,按照最簡單的同步通信方式,那麼從用戶提交註冊到收到響應,需要等系統完成這兩個步驟,才會給用戶返回註冊成功。如果發送郵件耗時非常之長,那麼用戶就得一直等下去:

【分佈式系統遨遊】分佈式通信

如下圖所示,如果引入消息隊列,作為註冊消息寫入數據庫和發送郵件、短信這三個組件間的中間通信者,那麼這三個組件就可以實現異步通信、異步執行:

【分佈式系統遨遊】分佈式通信

即用戶只需要在寫入數據庫之後,寫入發送郵件的消息隊列即可返回註冊成功,而並不需要等待真正的去發送郵件之後才會返回。所以,我們解除了註冊與發送郵件兩種操作之間的耦合,大大提高了註冊的響應速度。那你可能會問,如果發送郵件失敗了怎麼辦?我們一般會在業務層寫一些重試邏輯,確保郵件發送成功之後,才算成功消費。

除了將同步轉化為異步,消息隊列在高併發系統中也承擔著流量削峰的作用。對於流量控制,還有漏桶和令牌桶算法,感興趣的讀者可以進一步去了解。


分享到:


相關文章: