dubbo 系統整理

1.1 註冊中心概述

Dubbo通過註冊中心實現了分佈式 環境中各服務之間的註冊與發現,是各個分佈式節點之間的紐帶。

其主要作用如下:

• 動態加入。一個服務提供者通過註冊中心可以動態地把自己暴露給其他消費者,無須 消費者逐個去更新配置文件。

• 動態發現。一個消費者可以動態地感知新的配置、路由規則和新的服務提供者,無須 重啓服務使之生效。

• 動態調整。註冊中心支持參數的動態調整,新參數自動更新到所有相關服務節點。

• 統一配置。避免了本地配置導致每個服務的配置不一致問題。

1.2 1工作流程 註冊中心的總體流程比較簡單,Dubbo官方也有比較詳細的說明。

• 服務提供者啓動時,會向註冊中心寫入自己的元數據信息,同時會訂閱配置元數據信息。

• 消費者啓動時,也會向註冊中心寫入自己的元數據信息,並訂閱服務提供者、路由和 配置元數據信息。

• 服務治理中心(dubbo-admin)啓動時,會同時訂閱所有消費者、服務提供者、路由和 配置元數據信息。

• 當有服務提供者離開或有新的服務提供者加入時,註冊中心服務提供者目錄會發生變 化,變化信息會動態通知給消費者、服務治理中心。

• 當消費方發起服務調用時,會異步將調用、統計信息等上報給監控中心。

1.3緩存機制

緩存的存在就是用空間換取時間,如果每次遠程調用都要先從註冊中心獲取一次可調用的 服務列表,則會讓註冊中心承受巨大的流量壓力。另外,每次額外的網絡請求也會讓整個系統 的性能下降。

消費者或服務治理中心獲取註冊信息後會做本地緩存。內存中會有一份,保存在Properties 對象裏,磁盤上也會持久化一份文件,通過file對象引用。 內存中的緩存notified是ConcurrentHashMap裏面又嵌套了一個Map,外層Map的key 是消費者的 URL,內層 Map 的 key 是分類,包含 providers.,consumers, routes, configurators 四種。value則是對應的服務列表,對於沒有服務提供者提供服務的URL,它會以特殊的empty:// 前綴開頭。

public AbstractRegistry(URL url) {
    this.setUrl(url);
    this.syncSaveFile = url.getParameter("save.file", false);
    //文件名字
    String filename = url.getParameter("file", System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter("application") + "-" + url.getAddress() + ".cache");
    File file = null;
    if(ConfigUtils.isNotEmpty(filename)) {
        file = new File(filename);
        if(!file.exists() && file.getParentFile() != null && !file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
            throw new IllegalArgumentException("Invalid registry store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
        }
    }

    this.file = file;
    this.loadProperties();
    this.notify(url.getBackupUrls());
}

1.3.1 緩存的加載

在服務初始化的時候,AbstractRegistry構造函數裏會從本地磁盤文件中把持久化的註冊 數據讀到Properties對象裏,並加載到內存緩存中 , Properties保存了所有服務提供者的URL,使用URL#serviceKey()作爲key,提供者列表、 路由規則列表、配置規則列表等作爲value。由於value是列表,當存在多個的時候使用空格隔 開。還有一個特殊的key.registies,保存所有的註冊中心的地址。如果應用在啓動過程中, 註冊中心無法連接或宕機,則Dubbo框架會自動通過本地緩存加載Invokers。

private void loadProperties() {
    if(this.file != null && this.file.exists()) {
        FileInputStream in = null;

        try {
            in = new FileInputStream(this.file);
            this.properties.load(in);//把文件轉成properties對象
            if(this.logger.isInfoEnabled()) {
                this.logger.info("Load registry store file " + this.file + ", data: " + this.properties);
            }
        } catch (Throwable var11) {
            this.logger.warn("Failed to load registry store file " + this.file, var11);
        } finally {
            if(in != null) {
                try {
                    in.close();
                } catch (IOException var10) {
                    this.logger.warn(var10.getMessage(), var10);
                }
            }

        }
    }

}

1.3.2緩存的保存與更新

緩存的保存有同步和異步兩種方式。異步會使用線程池異步保存,如果線程在執行過程中 出現異常,則會再次調用線程池不斷重試。 AbstractRegistry#notify方法中封裝了更新內存緩存和更新文件緩存的邏輯。當客戶端第一 次訂閱獲取全量數據,或者後續由於訂閱得到新數據時,都會調用該方法進行保存。

private void saveProperties(URL url) {
    if(this.file != null) {
        try {
            StringBuilder t = new StringBuilder();
        Map categoryNotified = (Map)this.notified.get(url);
            ...
            this.properties.setProperty(url.getServiceKey(), t.toString());
            long version1 = this.lastCacheChanged.incrementAndGet();
            //這個syncSaveFile是save.file 參數,在初始化時賦值
            if(this.syncSaveFile) {
                //同步進行緩存
                this.doSaveProperties(version1);
            } else {
                //異步進行緩存,他是用線程池newFixThreadPool,限定一個線程跑進行緩存的任務
                this.registryCacheExecutor.execute(new AbstractRegistry.SaveProperties(version1, null));
            }
        } catch (Throwable var8) {
            this.logger.warn(var8.getMessage(), var8);
        }

    }
}

1.4 重試機制

com.alibaba.dubbo.registry.support.FailbackRegistry 繼承了 AbstractRegistry,並在此基礎上增加了失敗重試機制作爲抽象能力。ZookeeperRegistry和 RedisRegistry繼承該抽象方法後,直接使用即可。 FailbackRegistry抽象類中定義了一個ScheduledExecutorService,每經過固定間隔(默認 爲5秒)調用FailbackRegistry#retry()方法 該抽象類中還有五個比較重要的集合,

在定時器中調用retry方法的時候,會把這五個集合分別遍歷和重試,重試成功則從集合中 移除。FailbackRegistry實現了 subscribe> unsubscribe等通用方法,裏面調用了未實現的模板 方法,會由子類實現。通用方法會調用這些模板方法,如果捕獲到異常,則會把URL添加到對 應的重試集合中,以供定時器去重試。

代碼分析:

FailbackRegistry 構造方法如下,

public FailbackRegistry(URL url) {
    super(url);
    int retryPeriod = url.getParameter("retry.period", 5000);
    this.retryFuture = this.retryExecutor.scheduleWithFixedDelay(new Runnable() {
        public void run() {
            try {
                FailbackRegistry.this.retry();
            } catch (Throwable var2) {
                FailbackRegistry.this.logger.error("Unexpected error occur at failed retry, cause: " + var2.getMessage(), var2);
            }

        }
    }, (long)retryPeriod, (long)retryPeriod, TimeUnit.MILLISECONDS);
}
protected void retry() {
    HashSet failed;
    Iterator t;
    URL values;
    if(!this.failedRegistered.isEmpty()) {
        ...
    }

    if(!this.failedSubscribed.isEmpty()) {
        ...
    }

    if(!this.failedUnsubscribed.isEmpty()) {
        ...
    }

    if(!this.failedNotified.isEmpty()) {
        ...
    }
    if(!recoverSubscribed1.isEmpty()) {
        ...
    }
}

ps:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,//執行任務
                                                 long initialDelay,//第一次執行延遲加載的時間
                                                 long delay,//兩次執行之間間隔時間
                                                 TimeUnit unit);//時間單位

什麼時候添加到列表中,以註冊爲例子:

public void register(URL url) {
    super.register(url);//這一步只是先把URL添加到列表中
    this.failedRegistered.remove(url);
    this.failedUnregistered.remove(url);

    try {
        this.doRegister(url);
    } catch (Exception var6) {
        Object t = var6;
        boolean check = this.getUrl().getParameter("check", true) && url.getParameter("check", true) && !"consumer".equals(url.getProtocol());
        boolean skipFailback = var6 instanceof SkipFailbackWrapperException;
        if(check || skipFailback) {
            if(skipFailback) {
                t = var6.getCause();
            }

            throw new IllegalStateException("Failed to register " + url + " to registry " + this.getUrl().getAddress() + ", cause: " + ((Throwable)t).getMessage(), (Throwable)t);
        }

        this.logger.error("Failed to register " + url + ", waiting for retry, cause: " + var6.getMessage(), var6);
        this.failedRegistered.add(url);//註冊異常之後添加的。
    }

}

1.5 設計模式

模板模式:

1.5.1以註冊爲粒子:

public interface RegistryService {
    void register(URL var1);

    void unregister(URL var1);

    void subscribe(URL var1, NotifyListener var2);

    void unsubscribe(URL var1, NotifyListener var2);

    List<URL> lookup(URL var1);
}
public interface Node {
    URL getUrl();

    boolean isAvailable();

    void destroy();
}
public interface Registry extends Node, RegistryService {
}

AbstractRegistry實現了 Registry接口中的註冊、訂閱、查詢、通知等方法,還實現了磁 盤文件持久化註冊信息這一通用方法。但是註冊、訂閱、查詢、通知等方法只是簡單地把URL 加入對應的集合,沒有具體的註冊或訂閱邏輯。

AbstractRegistry: 只是簡單地把URL 加入對應的集合

public void subscribe(URL url, NotifyListener listener) {
    if(url == null) {
        throw new IllegalArgumentException("subscribe url == null");
    } else if(listener == null) {
        throw new IllegalArgumentException("subscribe listener == null");
    } else {
        if(this.logger.isInfoEnabled()) {
            this.logger.info("Subscribe: " + url);
        }

        Set listeners = (Set)this.subscribed.get(url);
        if(listeners == null) {
            this.subscribed.putIfAbsent(url, new ConcurrentHashSet());
            listeners = (Set)this.subscribed.get(url);
        }

        listeners.add(listener);
    }
}

FailbackRegistry又繼承了 AbstractRegistry,重寫了父類的註冊、訂閱、查詢和通知等 方法,並且添加了重試機制。此外,還添加了四個未實現的抽象模板方法。

FailbackRegistry:

這個方法就是模板的訂閱方法了,

public void subscribe(URL url, NotifyListener listener) {
    //簡單地把URL 加入對應的集合,形式是final ConcurrentMap<URL, Set<NotifyListener>>
    super.subscribe(url, listener);
    //將註冊失敗的監聽器從上述的ConcurrentMap<URL, Set<NotifyListener>> 中去除
    this.removeFailedSubscribed(url, listener);

    try {
        //具體訂閱的邏輯,這個是抽象方法,由子類去具體實現
        this.doSubscribe(url, listener);
    } catch (Exception var8) {
        Object t = var8;
        //從存儲在磁盤中的緩存獲取URL
        List urls = this.getCacheUrls(url);
        if(urls != null && !urls.isEmpty()) {
            //更新內存中的緩存
            this.notify(url, listener, urls);
            this.logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + this.getUrl().getParameter("file", System.getProperty("user.home") + "/dubbo-registry-" + url.getHost() + ".cache") + ", cause: " + var8.getMessage(), var8);
        } else {
            boolean check = this.getUrl().getParameter("check", true) && url.getParameter("check", true);
            boolean skipFailback = var8 instanceof SkipFailbackWrapperException;
            if(check || skipFailback) {
                if(skipFailback) {
                    t = var8.getCause();
                }

                throw new IllegalStateException("Failed to subscribe " + url + ", cause: " + ((Throwable)t).getMessage(), (Throwable)t);
            }

            this.logger.error("Failed to subscribe " + url + ", waiting for retry, cause: " + var8.getMessage(), var8);
        }
        //添加URL到註冊失敗的集合中,重試機制會進行重新註冊
        this.addFailedSubscribed(url, listener);
    }

}

由上面我們看出,模板方法做了頭尾的操作,將最主要的註冊的方法交給子類去實現,值得借鑑。

工廠模式:

下面是模板模式,真正創建註冊類Registry 是createRegistry(url),這是AbstractRegistryFactory 中的抽象方法。

private static final Map<String, Registry> REGISTRIES = new ConcurrentHashMap();
public Registry getRegistry(URL url) {
    url = url.setPath(RegistryService.class.getName()).addParameter("interface", RegistryService.class.getName()).removeParameters(new String[]{"export", "refer"});
    String key = url.toServiceString();
    LOCK.lock();

    Registry var4;
    try {
        Registry registry = (Registry)REGISTRIES.get(key);
        if(registry != null) {
            var4 = registry;
            return var4;
        }

        registry = this.createRegistry(url);
        if(registry == null) {
            throw new IllegalStateException("Can not create registry " + url);
        }

        REGISTRIES.put(key, registry);
        var4 = registry;
    } finally {
        LOCK.unlock();
    }

    return var4;
}
protected abstract Registry createRegistry(URL var1);

dubbo是通過SPI來實現生成不同的工廠類:

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

其實很好理解,創建自定義註解, 這個註解會自動生成代碼實現一些邏輯,它的value參 數會從URL中獲取protocol鍵的值,並根據獲取的值來調用不同的工廠類。上面的接口類就由dubbo生成了DubboRegistryFactory,MulticastRegistryFactory,RedisRegistryFactory,ZookeeperRegistryFactory

以DubboRegistryFactory爲例子,下面是真正創建對象的代碼:

public Registry createRegistry(URL url) {
    url = getRegistryURL(url);
    ArrayList urls = new ArrayList();
    urls.add(url.removeParameter("backup"));
    String backup = url.getParameter("backup");
    if(backup != null && backup.length() > 0) {
        String[] directory = Constants.COMMA_SPLIT_PATTERN.split(backup);
        String[] registryInvoker = directory;
        int registryService = directory.length;

        for(int registry = 0; registry < registryService; ++registry) {
            String address = registryInvoker[registry];
            urls.add(url.setAddress(address));
        }
    }

    RegistryDirectory var9 = new RegistryDirectory(RegistryService.class, url.addParameter("interface", RegistryService.class.getName()).addParameterAndEncoded("refer", url.toParameterString()));
    Invoker var10 = this.cluster.join(var9);
    RegistryService var11 = (RegistryService)this.proxyFactory.getProxy(var10);
    DubboRegistry var12 = new DubboRegistry(var10, var11);
    var9.setRegistry(var12);
    var9.setProtocol(this.protocol);
    var9.notify(urls);
    var9.subscribe(new URL("consumer", NetUtils.getLocalHost(), 0, RegistryService.class.getName(), url.getParameters()));
    return var12;
}

 

2. Dubbo擴展點加載 機制

2.1java spi

Java SPI使用了策略模式,一個接口多種實現。我們只聲明接口,具體的實現並不在程序 中直接確定,而是由程序之外的配置掌控,用於具體實現的裝配。具體步驟如下: 

(1) 定義一個接口及對應的方法。

(2) 編寫該接口的一個實現類。

(3) 在META-INF/services/目錄下,創建一個以接口全路徑命名的文件,如com.test.spi. PrintService0 

(4) 文件內容爲具體實現類的全路徑名,如果有多個,則用分行符分隔。

(5) 在代碼中通過java.util.ServiceLoader來加載所有具體的實現類。 

代碼示例:
接口類:
public interface People {
    public String speak();
}
實現類:
public class Chinese implements People{
    public String speak() {
        return "chinese speak zhongwen";
    }
}
public class English implements People{
    public String speak() {
        return "English speak English";
    }
}
配置文件:
路徑:resources/META-INF/services/com.atguigu.gmall.SPI.People
文件內容(有多個按照換行符分割):
com.atguigu.gmall.SPI.Chinese
com.atguigu.gmall.SPI.English
測試代碼:
@Test
public void testSpi(){
    ServiceLoader<People> peoples=ServiceLoader.load(People.class);
    for (People people:peoples){
        System.out.println(people.speak());
    }
}
結果:
chinese speak zhongwen
English speak English
2.2dubbo spi

2.2.1dubbo spi 相對於java spi、

JDK標準的SPI會一次性實例化擴展點所有實現,如果有擴展實現則初始化很耗時,如果沒 用上也加載,則浪費資源。 (dubbo spi 延遲加載)

Dubbo SPI只是加載配置文件中的類, 並分成不同的種類緩存在內存中,而不會立即全部初始化,在性能上有更好的表現。 
代碼示例:
接口類:
@SPI
public interface People {
    public String speak();
}
實現類:
public class Chinese implements People{
    public String speak() {
        return "chinese speak zhongwen";
    }
}
public class English implements People{
    public String speak() {
        return "English speak English";
    }
}
配置文件:
路徑:resources/META-INF/services/com.atguigu.gmall.SPI.People
文件內容(有多個按照換行符分割):
Chinese=com.atguigu.gmall.SPI.Chinese
English=com.atguigu.gmall.SPI.English
測試代碼:
 @Test
    public void testDubbo(){
        ExtensionLoader<People> extensionLoader=
                ExtensionLoader.getExtensionLoader(People.class);
        People chinese = extensionLoader.getExtension("Chinese");
        System.out.println(chinese.speak());
    }
結果:
chinese speak zhongwen
2.3擴展點的分類與緩存
Dubbo SPI可以分爲Class緩存、實例緩存。這兩種緩存又能根據擴展類的種類分爲普通擴展類、包裝擴展類(Wrapper類)、自適應擴展類(Adaptive類)等。
  Class緩存:Dubbo SPI獲取擴展類時,會先從緩存中讀取。如果緩存中不存在,則加載配置文件,根據配置把Class緩存到內存中,並不會直接全部初始化。
  實例緩存:基於性能考慮,Dubbo框架中不僅緩存Class,也會緩存Class實例化後的對象。每次獲取的時候,會先從緩存中讀取,如果緩存中讀不到,則重新加載並緩存起來。這也是爲什麼Dubbo SPI相對Java SPI性能上有優勢的原因,因爲Dubbo SPI緩存的Class並不會全部實例化,而是按需實例化並緩存,因此性能更好。 
被緩存的Class和對象實例可以根據不同的特性分爲不同的類別: 
(1) 普通擴展類。最基礎的,配置在SPI配置文件中的擴展類實現。
(2) 包裝擴展類。這種Wrapper類沒有具體的實現,只是做了通用邏輯的抽象,並且需要在構造方法中傳入一個具體的擴展接口的實現。
(3)自適應擴展類。一個擴展接口會有多種實現類,具體使用哪個實現類可以不寫死在配
置或代碼中,在運行時,通過傳入URL中的某些參數動態來確定。這屬於擴展點的自適應特性, 使用的@Adaptive註解也會在4.1.5節中詳細介紹。
(4) 其他緩存,如擴展類加載器緩存、擴展名緩存等。

https://www.jianshu.com/p/bc523348f519
2.4 ExtensionLoader 的工作原理

ExtensionLoader是整個擴展機制的主要邏輯類,在這個類裏面賣現了配置的加載、擴展類 緩存、自適應對象生成等所有工作。

2.4.1 工作流程

ExtensionLoader 的邏輯入口可以分爲 getExtension、getAdaptiveExtension、 getActivateExtension三個,分別是獲取普通擴展類、獲取自適應擴展類、獲取自動激活的擴 展類

getActivateExtension方法只是根據不同的條件同時激活多個普通 第4章Dubbo擴展點加載機制 67 擴展類。因此,該方法中只會做一些通用的判斷邏輯,如接口是否包含©Activate註解、匹配條 件是否符合等。最終還是通過調用getExtension方法獲得具體擴展點實現類。

getExtension (String name)是整個擴展加載器中最核心的方法,實現了一個完整的普通擴 展類加載過程。加載過程中的每一步,都會先檢查緩存中是否己經存在所需的數據,如果存在 則直接從緩存中讀取,沒有則重新加載。這個方法每次只會根據名稱返回一個擴展點實現類。 初始化的過程可以分爲4步:

(1)框架讀取SPI對應路徑下的配置文件,並根據配置加載所有擴展類並緩存(不初始化)。

(2) 根據傳入的名稱初始化對應的擴展類。

(3) 嘗試查找符合條件的包裝類:包含擴展點的setter方法,例如setProtocol(Protocol protocol)方法會自動注入protocol擴展點實現;包含與擴展點類型相同的構造函數,爲其注入擴 展類實例,例如本次初始化了一個Class A,初始化完成後,會尋找構造參數中需要Class A的 包裝類(Wrapper),然後注入Class A實例,並初始化這個包裝類

(4) 返回對應的擴展類實例。 getAdaptiveExtension也相對獨立,只有加載配置信息部分與getExtension共用了同一個 方法。和獲取普通擴展類一樣,框架會先檢查緩存中是否有已經初始化化好的Adaptive實例, 沒有則調用createAdaptiveExtension重新初始化。初始化過程分爲4步:

    (1) 和getExtension 一樣先加載配置文件。

    (2) 生成自適應類的代碼字符串。

    (3) 獲取類加載器和編譯器,並用編譯器編譯剛纔生成的代碼字符串

    (4) 返回對應的自適應類實例。

2.4.2 getExtension 的實現原理

當調用getExtension(String name)方法時,會先檢查緩存中是否有現成的數據,沒有則 調用createExtension開始創建。這裏有個特殊點,如果getExtension傳入的name是true, 則加載並返回默認擴展類。

在調用createExtension開始創建的過程中,也會先檢查緩存中是否有配置信息,如果不 存在擴展類,則會從 META-INF/services/> META-INF/dubbo/、META-INF/dubbo/internal/這幾 個路徑中讀取所有的配置文件,通過I/O讀取字符流,然後通過解析字符串,得到配置文件中 對應的擴展點實現類的全稱(如 com.alibaba.dubbo.common.extensionloader.activate.impl. GroupActivateExtImpl)

3 Dubbo啓停原理解析

3.1 2遠程服務的暴露機制

1.生成代理對象

JavassistProxyFactory 利用字節碼技術生成代理對象,比jdkProxyFactory的優勢是被代理類不用實現接口。

動態根據SPI生成代理類對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章