1. 程式人生 > >不知道如何實現服務的動態發現?快來看看 Dubbo 是如何做到的

不知道如何實現服務的動態發現?快來看看 Dubbo 是如何做到的

上篇文章如果有人問你 Dubbo 中註冊中心工作原理,就把這篇文章給他大致瞭解了註冊中心作用以及 Dubbo Registry 模組原始碼,這篇文章將深入 Dubbo ZooKeeper 模組,去了解如何實現服務動態的發現。

ps: 以下將 ZooKeeper 縮寫為 zk。

一、dubbo zk 資料結構

在 ZooKeeper 基本概念分享一文講道,ZK 內部是一種樹形層次結構,節點存在多種型別。而 Dubbo 只會建立持久節點和臨時節點。

若服務提供者服務介面為 com.service.FooService,將會在 ZK 中建立建立如下路徑 /dubbo/com.service.FooService/providers/providerURL

服務路徑分為四層,根節點預設為 dubbo,可以在 dubbo-registry 設定 group 屬性改變該值。

ps: 若無註冊中心隔離需求,不要隨便修改。

第二層節點為服務節點全名稱,如 com.service.FooService

第三層節點為服務目錄,如 providers。另外還存在其他目錄節點,分別為 consumers(消費者目錄),configurators(配置目錄),routers(路由目錄)。下面服務訂閱主要針對這一層節點。

第四個節點為具體服務節點,節點名為具體的 URL 字串,如 dubbo://2.0.1.13:12345/com.dubbo.example.DemoService?xx=xx

,該節點預設為臨時節點。
dubbo ZK 樹形內部結構示例為:

ZK 內部服務具體示例如下:

二、RegistryFactory 實現

Dubbo 可以在配置檔案中指定使用註冊中心,可以使用 dubbo.registry.protocol 指定具體註冊中心型別,也可以設定 dubbo.registry.address 指定。註冊中心相關實現將會使用 RegistryFactory 工廠類建立。

RegistryFactory 介面原始碼如下:

@SPI("dubbo")
public interface RegistryFactory {
    @Adaptive({"protocol"})
    Registry getRegistry(URL url);
}

RegistryFactory 介面方法使用 @Adaptive 註解,這裡將會使用 Dubbo SPI 機制,自動生成程式碼的一些實現邏輯。這裡將會根據 URL 中 protocol 屬性,去呼叫最終實現子類。

RegistryFactory 實現子類如圖所示:

AbstractRegistryFactory 將會實現介面的 getRegistry 方法,主要完成加鎖,並呼叫抽象模板方法 createRegistry 建立具體註冊中心實現類,並將其快取在記憶體中。

AbstractRegistryFactory#getRegistry 原始碼如下所示:

    public Registry getRegistry(URL url) {
        url = URLBuilder.from(url)
                .setPath(RegistryService.class.getName())
                .addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
                .removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY)
                .build();
        String key = url.toServiceStringWithoutResolving();
        // 加鎖,防止併發
        LOCK.lock();
        try {
        // 先從快取中取
            Registry registry = REGISTRIES.get(key);
            if (registry != null) {
                return registry;
            }
            //使用 Dubbo SPI 進位制建立
            registry = createRegistry(url);
            if (registry == null) {
                throw new IllegalStateException("Can not create registry " + url);
            }
        // 放入快取
            REGISTRIES.put(key, registry);
            return registry;
        } finally {
            // Release the lock
            LOCK.unlock();
        }
    }

註冊中心例項將會通過具體工廠類建立,這裡我們看下 ZookeeperRegistryFactory 原始碼:

public class ZookeeperRegistryFactory extends AbstractRegistryFactory {

    private ZookeeperTransporter zookeeperTransporter;

    /**
     * 通過 Dubbo SPI 進位制注入
     * @param zookeeperTransporter
     */
    public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
        this.zookeeperTransporter = zookeeperTransporter;
    }

    @Override
    public Registry createRegistry(URL url) {
        return new ZookeeperRegistry(url, zookeeperTransporter);
    }

}

ps:Dubbo SPI 機制還具有 IOC 特性,這裡的ZookeeperTransporter 注入可以參考:Dubbo 擴充套件點載入

三、zk 模組原始碼解析

講完註冊中心例項建立過程,下面深入 ZookeeperRegistry 實現原始碼。

ZookeeperRegistry 繼承 FailbackRegistry抽象類,所以其需要實現其父類抽象模板方法,下面主要了解 doRegisterdoSubscribe原始碼 。

3.1 doRegister

服務提供者需要將服務註冊到註冊中心,註冊的目的是為了讓消費者感知到服務的存在,從而發起遠端呼叫,另一方面也讓服務治理中心感知新的服務提供者上線。zk 模組服務註冊程式碼比較簡單,直接使用 zk 客戶端在註冊中心建立節點。

ZookeeperRegistry#doRegister 實現原始碼如下:

    public void doRegister(URL url) {
        try {
            zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
        } catch (Throwable e) {
            throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

zkClient.create 方法需要傳入兩個引數。

void create(String path, boolean ephemeral);

第一個引數為節點路徑,將會通過 toUrlPath 將 URL 例項轉化成 ZK 中路徑格式,轉化結果如下:

## 轉化前 URL 如下:

dubbo://10.20.82.31:12345/com.dubbo.example.DemoService

## 呼叫  `toUrlPath`  轉換之後
/dubbo/com.dubbo.example.DemoService/providers/dubbo%3A%2F%2F10.20.82.31%3A12345%2Fcom.dubbo.example.DemoService

第二個引數主要決定 ZK 節點型別主要取自 URL 例項物件中 dynamic 引數值,若不存在,預設為 true,也就是預設將會建立臨時節點。

zkClient.create 方法裡將會遞迴呼叫,首先父節點是否存在,不存在就會建立,直到最後一個節點跳出遞迴方法。

    public void create(String path, boolean ephemeral) {
    // 建立永久節點之前需要判斷是否已存在
        if (!ephemeral) {
            if (checkExists(path)) {
                return;
            }
        }
    // 判斷是否存在父節點
        int i = path.lastIndexOf('/');
        if (i > 0) {
       // 遞迴建立父節點
            create(path.substring(0, i), false);
        }
        if (ephemeral) {
        // 建立臨時節點
            createEphemeral(path);
        } else {
       // 建立永久節點
            createPersistent(path);
        }
    }

最後 createEphemeralcreatePersistent 實際建立節點操作將會交給 ZK 客戶端類,這裡實現比較簡單,可以自行參考原始碼。

ps: dubbo 在 2.6.1 起將 zk 客戶端預設使用 Curator,之前版本使用 zkclient。dubbo 2.7.1 開始去除 zkclient 實現,也就是說只能使用 Curator 。

3.2 為何 dubbo 服務提供者節點使用 zk 臨時節點

zk 臨時節點將會在 zk 客戶端斷開後,自動刪除。dubbo 服務提供者正常下線,其會主動刪除 zk 服務節點。

如果服務異常宕機,zk 服務節點就不能正常刪除,這就導致失效的服務一直存在 ZK 上,消費者還會呼叫該失效節點,導致消費者報錯。通過 zk 臨時節點特性,讓 zk 服務端主動刪除失效節點,從而下線失效服務。

四、doSubscribe: 服務動態發現的原理

4.1 訂閱基本原理

服務訂閱通常有 pull 和 push 兩種方式。pull 模式需要客戶端定時向註冊中心拉取配置,而 push 模式採用註冊中心主動推送資料給客戶端。

dubbo zk 註冊中心採用是事件通知與客戶端拉取方式。服務第一次訂閱的時候將會拉取對應目錄下全量資料,然後在訂閱的節點註冊一個 watcher。一旦目錄節點下發生任何資料變化,zk 將會通過 watcher 通知客戶端。客戶端接到通知,將會重新拉取該目錄下全量資料,並重新註冊 watcher。利用這個模式,dubbo 服務就可以就做到服務的動態發現。

4.2 原始碼解析

講完訂閱的基本原理,接著深入原始碼。

doSubscribe 方法需要傳入兩個引數,一個為 URL 例項,另一個為 NotifyListener,變更事件的監聽器。 方法內部會根據 URL 介面型別分成兩部分邏輯,全量訂閱服務與部分類別訂閱服務。

doSubscribe 方法整體原始碼邏輯:

    public void doSubscribe(final URL url, final NotifyListener listener) {
        if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
        // 全量訂閱邏輯
        } else {
        // 部分類別訂閱邏輯
        }
    }

服務治理中心(dubbo-admin),需要訂閱 service 全量介面,用以感知每個服務的狀態,所以訂閱之前將會把 service 設定成 *,處理所有service。

服務消費者或服務提供者將會走部分類別訂閱服務,下面我們以消費者視角,深入後續原始碼。

文章剛開頭講道了 zk 目錄節點存在四種類型,這裡將會根據 根據 URL 中 category值,決定訂閱節點路徑。

服務提供者 URL 中 category值預設為 configurators,而消費者 URL 中category值預設為 providers,configurators,routers。如果 category類別值為 *,將會訂閱四種類別路徑,否則將會只訂閱 providers型別的路徑。

toCategoriesPath 原始碼如下:

    private String[] toCategoriesPath(URL url) {
        String[] categories;
    // 如果類別為 *,訂閱四種類型的全量資料
        if (Constants.ANY_VALUE.equals(url.getParameter(Constants.CATEGORY_KEY))) {
            categories = new String[]{Constants.PROVIDERS_CATEGORY, Constants.CONSUMERS_CATEGORY,
                    Constants.ROUTERS_CATEGORY, Constants.CONFIGURATORS_CATEGORY};
        } else {
            categories = url.getParameter(Constants.CATEGORY_KEY, new String[]{Constants.DEFAULT_CATEGORY});
        }
    // 返回路徑陣列
        String[] paths = new String[categories.length];
        for (int i = 0; i < categories.length; i++) {
            paths[i] = toServicePath(url) + Constants.PATH_SEPARATOR + categories[i];
        }
        return paths;
    }

接著迴圈路徑陣列,迴圈內將會快取節點監聽器,用以提高效能。

    // 迴圈路徑陣列
    for (String path : toCategoriesPath(url)) {
        ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
        // listeners  快取為空,建立快取
        if (listeners == null) {
            zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
            listeners = zkListeners.get(url);
        }
        ChildListener zkListener = listeners.get(listener);
        // zkListener  快取為空則建立快取
        if (zkListener == null) {
            listeners.putIfAbsent(listener, (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds)));
            zkListener = listeners.get(listener);
        }
        // 建立訂閱節點
        zkClient.create(path, false);
        // 使用 ZK 客戶端訂閱節點
        List<String> children = zkClient.addChildListener(path, zkListener);
        if (children != null) {
            // 儲存全量需要通知的 URL
            urls.addAll(toUrlsWithEmpty(url, path, children));
        }
    }
    // 回撥 NotifyListener
    notify(url, listener, urls);

最終將會使用 CuratorClient.getChildren().usingWatcher(listener).forPath(path) 在 ZK 節點註冊 watcher,並獲取目錄節點下所有子節點資料。

這裡 watcher 使用 Curator 介面 CuratorWatcher,一旦 ZK 節點發生會變化,將會回撥 CuratorWatcher#process 方法。

CuratorWatcher#process 方法原始碼如下:

        public void process(WatchedEvent event) throws Exception {
            if (childListener != null) {
                String path = event.getPath() == null ? "" : event.getPath();
                childListener.childChanged(path,
                       // 重新設定 watcher,並獲取節點下所有子節點
                        StringUtils.isNotEmpty(path)
                                ? client.getChildren().usingWatcher(this).forPath(path)
                                : Collections.<String>emptyList());
            }
        }

消費者訂閱時序圖如下:

4.3 listener 關係圖

訂閱方法中我們碰到了多個 listener類,剛開始理解時候可能有點亂。可以參考下面關係圖理清楚這其中的關係。

listener 關係圖如下:

回撥關係如圖所示:

4.4 ZK 模組訂閱存在問題

ZK 第一次訂閱將會獲得目錄節點下所有子節點,後續任意子節點變更,將會通過 watcher 進位制回撥通知。回撥通知將會再次全量拉取節點目錄下所有子節點。這樣全量拉取將會有個侷限,當服務節點較多時將會對網路造成很大的壓力。

Dubbo 2.7 之後版本引入元資料中心解決該問題,詳情可參考,阿里技術專家詳解 Dubbo 實踐,演進及未來規劃。

引用文中一種解決方案如下圖:

總結

本文主要介紹了 dubbo zk 的資料結構,其次深入研究 ZookeeperRegistry 相關實現原始碼。通過了解服務註冊以及訂閱原理,瞭解 Dubbo 服務動態發現實現方式。

相關推薦

知道如何實現服務動態發現看看 Dubbo 是如何做到的

上篇文章如果有人問你 Dubbo 中註冊中心工作原理,就把這篇文章給他大致瞭解了註冊中心作用以及 Dubbo Registry 模組原始碼,這篇文章將深入 Dubbo ZooKeeper 模組,去了解如何實現服務動態的發現。 ps: 以下將 ZooKeeper 縮寫為 zk。 一、dubbo zk 資

知道怎麼提高程式碼質量?看看這幾種設計模式吧!

## 提高程式碼質量的目的 程式猿的本職工作就是寫程式碼,寫出高質量的程式碼應該是我們的追求和對自己的要求,因為: > 1. 高質量的程式碼往往意味著更少的BUG,更好的模組化,是我們擴充套件性,複用性的基礎 > 2. 高質量的程式碼也意味著更好的書寫,更好的命名,有利於我們的維護 ## 什

知道的JS(3)聊聊this

ops func arr args 調用函數 程序 mozilla 字符 理解 為什麽要使用this?什麽是this? 來看一段代碼 function identify() { return this.name.toUpperCase(); } function

裁員、年終獎減半:在公司打工,你永遠知道意外和明天哪個先

今年對網際網路公司來說,是非常難熬的一年,所謂“皮之不存毛將焉附”,如果哪裡都是寒冬,離開了這個平臺,對很多人來說,又該去哪裡呢?或者說,還有哪家公司能有今天滴滴這般的名氣、規模、保障及福利呢?很多危機發生的時候,總是讓人猝不及防。     2008年的那場金融

Java語法基礎梳理,會的小白看看

Java 概述 1991 年Sun公司的James Gosling(詹姆斯·高斯林)等人開始開發名稱為 Oak 的語言,希望用於控制嵌入在有線電視交換盒、PDA等的微處理器;1994年將Oak語言更名為Java; 基本概念 JDK Java Development Kit,jav

在程式設計師面前千萬要說這9句話,看看撒~

最近,有幾位程式設計師朋友,向我吐槽,經常有人和他說一些讓他惱怒的話,他聽完都想打人啦。 我聽完之後也是哭笑不得,將這些程式設計師朋友的話給整理出來了,大家以後在程式設計師面前說話可得小心點。。 01“我先下班了啦~你加油喔。” 作為同事,你真的不怕你明天的電腦打不開嗎? 02

java重啟服務動態載入properties檔案

動態載入properties檔案內容,不需要重啟服務! 1 、Maven 工程,在resource下新建一個properties檔案 target/classes/config.properties user=dufy phoneNo=123456

講一些你所知道的Java動態代理

同名 運行 pack 編譯 pri class final art mat 簡介 Proxy 是設計模式中的一種。當需要在已存在的 class 上添加或修改功能時,可以通過創建 proxy object 來實現 通常 proxy object 和被代理對象擁有相同的方法,並

Zookeeper實現服務註冊/發現

what that? Zookeeper在分散式開發中使用頻繁,但許多框架都對其進行了封裝,初學者可能無法較好的理解其工作原理,該文章演示了使用Zookeeper實現服務註冊,服務發現的簡單demo,希望能達到拋磚引玉的效果; why need RegisterCenter? 之所以需要訪問註冊和服務

單例模式是一件小事,回來看看

use 需要 簡單的 ini blog system faq 依然 集中   上次寫了一篇《單例模式那件小事,看了你不會後悔》的文章,總結了常用的單例模式的實現。本文是上文的延續,單例模式絕不是一件小事,想弄清楚,真不是那麽簡單的。上文提到了常用的三種單例模式的實現方法:餓

看看Google出品的Protocol Buffer,別僅僅會用Json和XML了

println 輸出流 基本數據類型 下一個 ebr sid 官網 序列 reg 前言 習慣用 Json、XML 數據存儲格式的你們,相信大多都沒聽過Protocol Buffer Protocol Buffer 事實上 是 Google出品的一種輕

扼殺孩子樂觀性格的10大語錄 看看你說過嗎?

AI 努力 玩具 爸爸 是我 原本 能夠 失望 感恩 陽光般溫暖燦爛的笑容、平和開朗的脾氣、遇事從容不緊張、自信快樂的寶貝,相信每個媽媽都希望擁有吧。但有時我們幾句無心的話,卻會扼殺孩子的樂觀性格,這些話你有沒有說過呢?  當孩子被贊賞時說 |沒有沒有,我們並沒有這麽優

MySQL的事務,容易懂,看看吧!

事務 1.儲存引擎 Mysql核心儲存引擎。 Mysql5.5 預設採用innoDB。(my.ini)   2.什麼是事務 事務用於保證資料的一致性,由一組DML操作組成,該組SQL語句要麼同時成功,要麼同時失敗。例如轉賬。 3.事務

看看與OpenStack掛鉤的高性能邊緣雲軟件堆棧StarlingX

ron toc user 結合 技術發展 xtra check tle around

看看與OpenStack掛鉤的高效能邊緣雲軟體堆疊StarlingX

StarlingX既是一個開發專案又是一個整合專案。它將新服務與更多開源專案結合到一個總體邊緣雲軟體堆疊中。 基於由英特爾和Wind River提供並由OpenStack Foundation託管的程式碼。它將自己的元件與率先的開源專案(包含OpenStack、Ceph和O

自媒體既然知道些文章,那麼我們可以這樣去

對於剛接觸自媒體的人來說,要馬上寫出好文章是很困難的,還有就是要大量的寫也是很困難。那麼我們該怎麼去做讓自己快速的熟悉起來呢,我們可以在接觸之前對這方面有一定的瞭解,看自己是否是願意去投入那麼多的精力去學習,不然你在這方面要堅持下去會很困難,畢竟寫文章是很枯燥的。如果有興趣但是不知道從何下手

小程式又放大招,小程式支援直播,看看你能夠開發自己的直播小程式嗎?

小程式又放大招,小程式支援直播,快來看看你能夠開發自己的直播小程式嗎? https://blog.csdn.net/towtotow/article/details/78923839 https://blog.csdn.net/u012536034/article/details/80524748 &n

PDF轉CAD,看看

從事CAD相關工作的小夥伴們應該都知道,在日常工作中,我們經常會遇到關於CAD格式轉換的問題。其中PDF轉CAD就是其中非常常見的。那麼PDF轉CAD,該如何轉換才更加簡潔呢?小編今天就給大傢俱體演示一下。具體演示步驟如下: 步驟一:我們在瀏覽器裡搜尋迅捷CAD

#Java基礎知識之面試題總結,看看你會了嗎!

什麼是Java程式的主類?應用程式和小程式的主類有何不同? 一個程式中可以有多個類,但只能有一個類是主類。在Java應用程式中,這個主類是指包含main()方法的類。而在Java小程式中,這個主類是一個繼承自系統類JApplet或Applet的子類。應用程式的主

看看你過去處理異常Exception的方式是否足夠優雅?

背景介紹 我們每天都需要與各種個樣的異常打交到,但是我們對異常瞭解嗎?對其處理方式正確嗎?瞭解的話就算了,不瞭解的可以看看下面的內容。 開啟Exception Exception的分類 先來看看下面這張圖: 從圖中可以看出: Error(錯誤)和Exc