深入淘寶Diamond之客戶端架構解析

說明:本文不介紹如何使用Diamond,只介紹Diamond的實現原理

一、什么是Diamond

diamond是淘寶內部使用的一個管理持久配置的系統,它的特點是簡單、可靠、易用,目前淘寶內部絕大多數系統的配置,由diamond來進行統一管理。
diamond為應用系統提供了獲取配置的服務,應用不僅可以在啟動時從diamond獲取相關的配置,而且可以在運行中對配置數據的變化進行感知并獲取變化后的配置數據。
持久配置是指配置數據會持久化到磁盤和數據庫中。

二、Diamond的特點

  • 簡單:整體結構非常簡單,從而減少了出錯的可能性。
  • 可靠:應用方在任何情況下都可以啟動,在承載淘寶核心系統并正常運行一年多以來,沒有出現過任何重大故障。
  • 易用:客戶端使用只需要兩行代碼,暴露的接口都非常簡單,易于理解。

三、Diamond的持久機制

Paste_Image.png

訂閱方獲取配置數據時,直接讀取服務端本地磁盤文件,盡量減少對數據庫壓力。 這種架構用短暫的延時換取最大的性能和一致性,一些配置不能接受延時的情況下,通過API可以獲取數據庫中的最新配置

四、Diamond的容災機制

Diamond作為一個分布式環境下的持久配置系統,有一套完備的容災機制,數據被存儲在:數據庫,服務端磁盤,客戶端緩存目錄,以及可以手工干預的容災目錄。 客戶端通過API獲取配置數據按照固定的順序去不同的數據源獲取數據:容災目錄,服務端磁盤,客戶端緩存。

? 數據庫主庫不可用,可以切換到備庫,Diamond繼續提供服務
? 數據庫主備庫全部不可用,Diamond通過本地緩存可以繼續提供讀服務
? 數據庫主備庫全部不可用,Diamond服務端全部不可用,Diamond客戶端使用緩存目錄繼續運行,支持離線啟動
? 數據庫主備庫全部不可用,Diamond服務端全部不可用,Diamond客戶端緩存數據被刪,可以通過拷貝備份的緩存目錄到容災目錄下繼續使用

五、Diamond的架構圖

圖片 1.png

六、Diamond訂閱端(客戶端)分析

先看一個簡單的客戶端訂閱代碼實現:

public class DiamondTestClient {
    public static DiamondManager manager;
    public static void main(String[] str) {
        initDiamondManager();
    }
    private static void initDiamondManager() {
        manager = new DefaultDiamondManager("group_test", "dataId_test", new ManagerListener() {
            public void receiveConfigInfo(String configInfo) {
                System.out.println("configInfo="+ configInfo);
            }   
        });   
    }  
}

參數的說明:
DefaultDiamondManager有三個參數分別是:groupId,dataId和listener。
group和dataId為String類型,二者結合為diamond-server端保存數據的惟一key
ManagerListener 是客戶端注冊的數據監聽器, 它的作用是在運行中接受變化的配置數據,然后回調receiveConfigInfo()方法,執行客戶端處理數據的邏輯。如果要在運行中對變化的配置數據進行處理,就一定要注冊ManagerListener
我們來看一下DefaultDiamondManager的類圖

Paste_Image.png

DefaultDiamondManager的構造方法代碼如下:

public DefaultDiamondManager(String group, String dataId, ManagerListener managerListener) {
        this.dataId = dataId;
        this.group = group;

        diamondSubscriber = DiamondClientFactory.getSingletonDiamondSubscriber();

        this.managerListeners.add(managerListener);
        ((DefaultSubscriberListener) diamondSubscriber.getSubscriberListener()).addManagerListeners(this.dataId,
            this.group, this.managerListeners);
        diamondSubscriber.addDataId(this.dataId, this.group);
        diamondSubscriber.start();

    }

說明
1、利用工廠類DiamondClientFactory創建單例訂閱者類。
2、將客戶端創建的偵聽器類添加到偵聽器管理list中并注入到新創建的訂閱者類中。
3、為訂閱者設置dataId和groupId。
4、啟動訂閱者線程,開始輪詢消息。

DiamondSubscriber的類圖如下:

Paste_Image.png

執行diamondSubScriber.start()方法直接進入DefaultDiamondSubscriber子類中,先看如下代碼:

/**
     * 啟動DiamondSubscriber:<br>
     * 1.阻塞主動獲取所有的DataId配置信息<br>
     * 2.啟動定時線程定時獲取所有的DataId配置信息<br>
     */
    public synchronized void start() {
        if (isRun) {
            return;
        }

        if (null == scheduledExecutor || scheduledExecutor.isTerminated()) {
            scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
        }

        localConfigInfoProcessor.start(this.diamondConfigure.getFilePath() + "/" + DATA_DIR);
        serverAddressProcessor = new ServerAddressProcessor(this.diamondConfigure, this.scheduledExecutor);
        serverAddressProcessor.start();

        this.snapshotConfigInfoProcessor =
                new SnapshotConfigInfoProcessor(this.diamondConfigure.getFilePath() + "/" + SNAPSHOT_DIR);
        // 設置domainNamePos值
        randomDomainNamePos();
        initHttpClient();

        // 初始化完畢
        isRun = true;

        if (log.isInfoEnabled()) {
            log.info("當前使用的域名有:" + this.diamondConfigure.getDomainNameList());
        }

        if (MockServer.isTestMode()) {
            bFirstCheck = false;
        }
        else {
            // 設置輪詢間隔時間
            this.diamondConfigure.setPollingIntervalTime(Constants.POLLING_INTERVAL_TIME);
        }
        // 輪詢
        rotateCheckConfigInfo();

        addShutdownHook();
    }

說明:
1、ServerAddressProcessor類從服務端獲取提供服務的地址列表(可能會多個)。
2、randomDomainNamePos這個方法是隨機從服務地址列表中選取一個地址。
3、初始化httpClient客戶端,使用initHttpClient方法。
4、設置讀取配置文件的輪詢時間默認為15秒。
5、rotateCheckConfigInfo這個方法是真正與服務端交互的輪詢方法。

rotateCheckConfigInfo方法的代碼如下:

/**
     * 循環探測配置信息是否變化,如果變化,則再次向DiamondServer請求獲取對應的配置信息
     */
    private void rotateCheckConfigInfo() {
        scheduledExecutor.schedule(new Runnable() {
            public void run() {
                if (!isRun) {
                    log.warn("DiamondSubscriber不在運行狀態中,退出查詢循環");
                    return;
                }
                try {
                    checkLocalConfigInfo();
                    checkDiamondServerConfigInfo();
                    checkSnapshot();
                }
                catch (Exception e) {
                    e.printStackTrace();
                    log.error("循環探測發生異常", e);
                }
                finally {
                    rotateCheckConfigInfo();
                }
            }

        }, bFirstCheck ? 60 : diamondConfigure.getPollingIntervalTime(), TimeUnit.SECONDS);
        bFirstCheck = false;
    }

說明
1、方法內部啟動一個定時線程,默認每隔60秒執行一次。
2、方法內部實際上三個主方法分別是:

  • checkLocalConfigInfo:主要是檢查本地數據是否有更新,如果沒有則返回,有則返回最新數據,并通知客戶端配置的listener。
  • checkDiamondServerConfigInfo:遠程調用服務端,獲取最新修改的配置數據并通知客戶端listener。
  • checkSnapshot:主要是持久化數據信息用的方法。

6.1 checkLocalConfigInfo代碼分析

private void checkLocalConfigInfo() {
        for (Entry<String/* dataId */, ConcurrentHashMap<String/* group */, CacheData>> cacheDatasEntry : cache
            .entrySet()) {
            ConcurrentHashMap<String, CacheData> cacheDatas = cacheDatasEntry.getValue();
            if (null == cacheDatas) {
                continue;
            }
            for (Entry<String, CacheData> cacheDataEntry : cacheDatas.entrySet()) {
                final CacheData cacheData = cacheDataEntry.getValue();
                try {
                    String configInfo = getLocalConfigureInfomation(cacheData);
                    if (null != configInfo) {
                        if (log.isInfoEnabled()) {
                            log.info("本地配置信息被讀取, dataId:" + cacheData.getDataId() + ", group:" + cacheData.getGroup());
                        }
                        popConfigInfo(cacheData, configInfo);
                        continue;
                    }
                    if (cacheData.isUseLocalConfigInfo()) {
                        continue;
                    }
                }
                catch (Exception e) {
                    log.error("向本地索要配置信息的過程拋異常", e);
                }
            }
        }

說明:
1、循環本地緩存數據,比較數據是否更新變化,重點看getLocalConfigureInfomation方法。
2、如果有更新數據則調用popConfigInfo方法通知客戶端listener。

再深入看getLocalConfigureInfomation方法,代碼如下:

// 判斷是否變更,沒有變更,返回null
        if (!filePath.equals(cacheData.getLocalConfigInfoFile())
                || existFiles.get(filePath) != cacheData.getLocalConfigInfoVersion()) {
            String content = FileUtils.getFileContent(filePath);
            cacheData.setLocalConfigInfoFile(filePath);
            cacheData.setLocalConfigInfoVersion(existFiles.get(filePath));
            cacheData.setUseLocalConfigInfo(true);

            if (log.isInfoEnabled()) {
                log.info("本地配置數據發生變化, dataId:" + cacheData.getDataId() + ", group:" + cacheData.getGroup());
            }

            return content;
        }
        else {
            cacheData.setUseLocalConfigInfo(true);

            if (log.isInfoEnabled()) {
                log.debug("本地配置數據沒有發生變化, dataId:" + cacheData.getDataId() + ", group:" + cacheData.getGroup());
            }

            return null;
        }

說明:
這段代碼很關鍵,判斷當前緩存的數據是否持久化的文件數據是否一致,包括版本號,文件路徑等信息,如果服務器端有配置數據更新,客戶端則拿到最新的數據后更新本地文件內容。

popConfigInfo方法的代碼如下:

void popConfigInfo(final CacheData cacheData, final String configInfo) {
        final ConfigureInfomation configureInfomation = new ConfigureInfomation();
        configureInfomation.setConfigureInfomation(configInfo);
        final String dataId = cacheData.getDataId();
        final String group = cacheData.getGroup();
        configureInfomation.setDataId(dataId);
        configureInfomation.setGroup(group);
        cacheData.incrementFetchCountAndGet();
        if (null != this.subscriberListener.getExecutor()) {
            this.subscriberListener.getExecutor().execute(new Runnable() {
                public void run() {
                    try {
                        subscriberListener.receiveConfigInfo(configureInfomation);
                        saveSnapshot(dataId, group, configInfo);
                    }
                    catch (Throwable t) {
                        log.error("配置信息監聽器中有異常,group為:" + group + ", dataId為:" + dataId, t);
                    }
                }
            });
        }
        else {
            try {
                subscriberListener.receiveConfigInfo(configureInfomation);
                saveSnapshot(dataId, group, configInfo);
            }
            catch (Throwable t) {
                log.error("配置信息監聽器中有異常,group為:" + group + ", dataId為:" + dataId, t);
            }
        }
    }

說明:
這段代碼主要是將已經更新的數據通知給客戶端織入的listener程序,使能夠達到最新數據通知給客戶端。

6.2 checkDiamondServerConfigInfo代碼分析

private void checkDiamondServerConfigInfo() {
        Set<String> updateDataIdGroupPairs = checkUpdateDataIds(diamondConfigure.getReceiveWaitTime());
        if (null == updateDataIdGroupPairs || updateDataIdGroupPairs.size() == 0) {
            log.debug("沒有被修改的DataID");
            return;
        }
        // 對于每個發生變化的DataID,都請求一次對應的配置信息
        for (String freshDataIdGroupPair : updateDataIdGroupPairs) {
            int middleIndex = freshDataIdGroupPair.indexOf(WORD_SEPARATOR);
            if (middleIndex == -1)
                continue;
            String freshDataId = freshDataIdGroupPair.substring(0, middleIndex);
            String freshGroup = freshDataIdGroupPair.substring(middleIndex + 1);

            ConcurrentHashMap<String, CacheData> cacheDatas = cache.get(freshDataId);
            if (null == cacheDatas) {
                continue;
            }
            CacheData cacheData = cacheDatas.get(freshGroup);
            if (null == cacheData) {
                continue;
            }
            receiveConfigInfo(cacheData);
        }
    }

說明:
1、通過HttpClient方式從服務端獲取更新過的dataId和groupId集合。
2、根據dataId和groupId再從服務端將相應變化的數據獲取下來。
3、通知客戶端注冊的listener程序。

上面二種方式通知客戶端的listener程序,都是通過allListeners這個屬性獲取的

private final ConcurrentMap<String/* dataId + group */, CopyOnWriteArrayList<ManagerListener>/* listeners */> allListeners =
            new ConcurrentHashMap<String, CopyOnWriteArrayList<ManagerListener>>();

這行代碼就是在最開始的那個客戶端使用的例子中注冊在allListeners中的。

七、Diamond客戶端與服務端交互時序圖

圖片 1.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,151評論 4 61
  • 最近一波牛市,很多忠誠的云幣網的用戶,某種程度上踏空了,特別對于所有的買賣和資金都在云幣的人,還有可能在...
    盧海濱閱讀 190評論 0 0
  • 畢業兩年了,每月還是月光族。自從在長投這段時間,感覺學習到了很多東西,比如: 1.、先儲蓄,再消費。 2、資產...
    紅葵米閱讀 1,281評論 0 1
  • 昨天遇到一個問題,跟大家分享一下某個APP,簡稱A應用,開機以u0 用戶進行自啟動。Setting應用,開機分別以...
    四大爺閱讀 1,642評論 1 1