Nacos配置中心交互模型是 push 仍是 pull ?你應該這麼回答

本文案例收錄在 https://github.com/chengxy-nds/Springboot-Notebookjava

你們好,我是小富~git

對於Nacos你們應該都不太陌生,出身阿里名聲在外,能作動態服務發現、配置管理,很是好用的一個工具。然而這樣的技術用的人越多面試被問的機率也就越大,若是隻停留在使用層面,那面試可能要吃大虧。程序員

好比咱們今天要討論的話題,Nacos在作配置中心的時候,配置數據的交互模式是服務端推過來仍是客戶端主動拉的?github

這裏我先拋出答案:客戶端主動拉的!面試

接下來我們扒一扒Nacos的源碼,來看看它具體是如何實現的?sql

配置中心

Nacos以前簡單回顧下配置中心的由來。網絡

簡單理解配置中心的做用就是對配置統一管理,修改配置後應用能夠動態感知,而無需重啓。數據結構

由於在傳統項目中,大多都採用靜態配置的方式,也就是把配置信息都寫在應用內的ymlproperties這類文件中,若是要想修改某個配置,一般要重啓應用才能夠生效。mybatis

但有些場景下,好比咱們想要在應用運行時,經過修改某個配置項,實時的控制某一個功能的開閉,頻繁的重啓應用確定是不能接受的。多線程

尤爲是在微服務架構下,咱們的應用服務拆分的粒度很細,少則幾十多則上百個服務,每一個服務都會有一些本身特有或通用的配置。假如此時要改變通用配置,難道要我挨個改幾百個服務配置?很顯然這不可能。因此爲了解決此類問題配置中心應運而生。

配置中心

推與拉模型

客戶端與配置中心的數據交互方式其實無非就兩種,要麼推push,要麼拉pull

推模型

客戶端與服務端創建TCP長鏈接,當服務端配置數據有變更,馬上經過創建的長鏈接將數據推送給客戶端。

優點:長連接的優勢是實時性,一旦數據變更,當即推送變動數據給客戶端,並且對於客戶端而言,這種方式更爲簡單,只創建鏈接接收數據,並不須要關心是否有數據變動這類邏輯的處理。

弊端:長鏈接可能會由於網絡問題,致使不可用,也就是俗稱的假死。鏈接狀態正常,但實際上已沒法通訊,因此要有的心跳機制KeepAlive來保證鏈接的可用性,才能夠保證配置數據的成功推送。

拉模型

客戶端主動的向服務端發請求拉配置數據,常見的方式就是輪詢,好比每3s向服務端請求一次配置數據。

輪詢的優勢是實現比較簡單。但弊端也顯而易見,輪詢沒法保證數據的實時性,何時請求?間隔多長時間請求一次?都是不得不考慮的問題,並且輪詢方式對服務端還會產生不小的壓力。

長輪詢

開篇咱們就給出了答案,nacos採用的是客戶端主動拉pull模型,應用長輪詢(Long Polling)的方式來獲取配置數據。

額?之前只聽過輪詢,長輪詢又是什麼鬼?它和傳統意義上的輪詢(暫且叫短輪詢吧,方便比較)有什麼不一樣呢?

短輪詢

無論服務端配置數據是否有變化,不停的發起請求獲取配置,好比支付場景中前段JS輪詢訂單支付狀態。

這樣的壞處顯而易見,因爲配置數據並不會頻繁變動,如果一直髮請求,勢必會對服務端形成很大壓力。還會形成推送數據的延遲,好比:每10s請求一次配置,若是在第11s時配置更新了,那麼推送將會延遲9s,等待下一次請求。

爲了解決短輪詢的問題,有了長輪詢方案。

長輪詢

長輪詢可不是什麼新技術,它不過是由服務端控制響應客戶端請求的返回時間,來減小客戶端無效請求的一種優化手段,其實對於客戶端來講與短輪詢的使用並無本質上的區別。

客戶端發起請求後,服務端不會當即返回請求結果,而是將請求掛起等待一段時間,若是此段時間內服務端數據變動,當即響應客戶端請求,如果一直無變化則等到指定的超時時間後響應請求,客戶端從新發起長連接。

Nacos初識

爲了後續演示操做方便我在本地搭了個Nacos注意: 運行時遇到個小坑,因爲Nacos默認是以cluster集羣的方式啓動,而本地搭建一般是單機模式standalone,這裏需手動改一下啓動腳本startup.X中的啓動模式。

直接執行/bin/startup.X就能夠了,默認用戶密碼均是nacos

幾個概念

Nacos配置中心的幾個核心概念:dataIdgroupnamespace,它們的層級關係以下圖:

dataId:是配置中內心最基礎的單元,它是一種key-value結構,key一般是咱們的配置文件名稱,好比:application.ymlmybatis.xml,而value是整個文件下的內容。

目前支持JSONXMLYAML等多種配置格式。

group:dataId配置的分組管理,好比同在dev環境下開發,但同環境不一樣分支須要不一樣的配置數據,這時就能夠用分組隔離,默認分組DEFAULT_GROUP

namespace:項目開發過程當中確定會有devtestpro等多個不一樣環境,namespace則是對不一樣環境進行隔離,默認全部配置都在public裏。

架構設計

下圖簡要描述了nacos配置中心的架構流程。

客戶端、控制檯經過發送Http請求將配置數據註冊到服務端,服務端持久化數據到Mysql。

客戶端拉取配置數據,並批量設置對dataId的監聽發起長輪詢請求,如服務端配置項變動當即響應請求,如無數據變動則將請求掛起一段時間,直到達到超時時間。爲減小對服務端壓力以及保證配置中心可用性,拉取到配置數據客戶端會保存一份快照在本地文件中,優先讀取。

這裏我省略了比較多的細節,如鑑權、負載均衡、高可用方面的設計(其實這部分纔是真正值得學的,後邊另出文講吧),主要弄清客戶端與服務端的數據交互模式。

下邊咱們以Nacos 2.0.1版本源碼分析,2.0之後的版本改動較多,和網上的不少資料略有些不一樣 地址:https://github.com/alibaba/nacos/releases/tag/2.0.1

客戶端源碼分析

Nacos配置中心的客戶端源碼在nacos-client項目,其中NacosConfigService實現類是全部操做的核心入口。

說以前先了解個客戶端數據結構cacheMap,這裏你們重點記住它,由於它幾乎貫穿了Nacos客戶端的全部操做,因爲存在多線程場景爲保證數據一致性,cacheMap採用了AtomicReference原子變量實現。

/**
 * groupKey -> cacheData.
 */
private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(new HashMap<>());

cacheMap是個Map結構,key爲groupKey,是由dataId, group, tenant(租戶)拼接的字符串;value爲CacheData對象,每一個dataId都會持有一個CacheData對象。

獲取配置

Nacos獲取配置數據的邏輯比較簡單,先取本地快照文件中的配置,若是本地文件不存在或者內容爲空,則再經過HTTP請求從遠端拉取對應dataId配置數據,並保存到本地快照中,請求默認重試3次,超時時間3s。

獲取配置有getConfig()getConfigAndSignListener()這兩個接口,但getConfig()只是發送普通的HTTP請求,而getConfigAndSignListener()則多了發起長輪詢和對dataId數據變動註冊監聽的操做addTenantListenersWithContent()

@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
    return getConfigInner(namespace, dataId, group, timeoutMs);
}

@Override
public String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener)
        throws NacosException {
    String content = getConfig(dataId, group, timeoutMs);
    worker.addTenantListenersWithContent(dataId, group, content, Arrays.asList(listener));
    return content;
}

註冊監聽

客戶端註冊監聽,先從cacheMap中拿到dataId對應的CacheData對象。

public void addTenantListenersWithContent(String dataId, String group, String content,
                                          List<? extends Listener> listeners) throws NacosException {
    group = blank2defaultGroup(group);
    String tenant = agent.getTenant();
    // 一、獲取dataId對應的CacheData,如沒有則向服務端發起長輪詢請求獲取配置
    CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
    synchronized (cache) {
        // 二、註冊對dataId的數據變動監聽
        cache.setContent(content);
        for (Listener listener : listeners) {
            cache.addListener(listener);
        }
        cache.setSyncWithServer(false);
        agent.notifyListenConfig();
    }
}

如沒有則向服務端發起長輪詢請求獲取配置,默認的Timeout時間爲30s,並把返回的配置數據回填至CacheData對象的content字段,同時用content生成MD5值;再經過addListener()註冊監聽器。

CacheData也是個出場頻率很是高的一個類,咱們看到除了dataId、group、tenant、content這些相關的基礎屬性,還有幾個比較重要的屬性如:listenersmd5(content真實配置數據計算出來的md5值),以及註冊監聽、數據比對、服務端數據變動通知操做都在這裏。

其中listeners是對dataId所註冊的全部監聽器集合,其中的ManagerListenerWrap對象除了持有Listener監聽類,還有一個lastCallMd5字段,這個屬性很關鍵,它是判斷服務端數據是否更變的重要條件。

在添加監聽的同時會將CacheData對象當前最新的md5值賦值給ManagerListenerWrap對象的lastCallMd5屬性。

public void addListener(Listener listener) {
    ManagerListenerWrap wrap =
        (listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)
            : new ManagerListenerWrap(listener, md5);
}

看到這對dataId監聽設置就完事了?咱們發現全部操做都圍着cacheMap結構中的CacheData對象,那麼大膽猜想下必定會有專門的任務來處理這個數據結構。

變動通知

客戶端又是如何感知服務端數據已變動呢?

咱們仍是從頭看,NacosConfigService類的構造器中初始化了一個ClientWorker,而在ClientWorker類的構造器中又啓動了一個線程池來輪詢cacheMap

而在executeConfigListen()方法中有這麼一段邏輯,檢查cacheMap中dataId的CacheData對象內,MD5字段與註冊的監聽listener內的lastCallMd5值,不相同表示配置數據變動則觸發safeNotifyListener方法,發送數據變動通知。

void checkListenerMd5() {
    for (ManagerListenerWrap wrap : listeners) {
        if (!md5.equals(wrap.lastCallMd5)) {
            safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
        }
    }
}

safeNotifyListener()方法單獨起線程,向全部對dataId註冊過監聽的客戶端推送變動後的數據內容。

客戶端接收通知,直接實現receiveConfigInfo()方法接收回調數據,處理自身業務就能夠了。

configService.addListener(dataId, group, new Listener() {
    @Override
    public void receiveConfigInfo(String configInfo) {
        System.out.println("receive:" + configInfo);
    }

    @Override
    public Executor getExecutor() {
        return null;
    }
});

爲了理解更直觀我用測試demo演示下,獲取服務端配置並設置監聽,每當服務端配置數據變化,客戶端監聽都會收到通知,一塊兒看下效果。

public static void main(String[] args) throws NacosException, InterruptedException {
    String serverAddr = "localhost";
    String dataId = "test";
    String group = "DEFAULT_GROUP";
    Properties properties = new Properties();
    properties.put("serverAddr", serverAddr);
    ConfigService configService = NacosFactory.createConfigService(properties);
    String content = configService.getConfig(dataId, group, 5000);
    System.out.println(content);
    configService.addListener(dataId, group, new Listener() {
        @Override
        public void receiveConfigInfo(String configInfo) {
            System.out.println("數據變動 receive:" + configInfo);
        }
        @Override
        public Executor getExecutor() {
            return null;
        }
    });

    boolean isPublishOk = configService.publishConfig(dataId, group, "我是新配置內容~");
    System.out.println(isPublishOk);

    Thread.sleep(3000);
    content = configService.getConfig(dataId, group, 5000);
    System.out.println(content);
}

結果和預想的同樣,當向服務端publishConfig數據變化後,客戶端能夠當即感知,愣是用主動拉pull模式作出了服務端實時推送的效果。

數據變動 receive:我是新配置內容~
true
我是新配置內容~

服務端源碼分析

Nacos配置中心的服務端源碼主要在nacos-config項目的ConfigController類,服務端的邏輯要比客戶端稍複雜一些,這裏咱們重點看下。

處理長輪詢

服務端對外提供的監聽接口地址/v1/cs/configs/listener,這個方法內容很少,順着doPollingConfig往下看。

服務端根據請求header中的Long-Pulling-Timeout屬性來區分請求是長輪詢仍是短輪詢,這裏我們只關注長輪詢部分,接着看LongPollingService(記住這個service很關鍵)類中的addLongPollingClient()方法是如何處理客戶端的長輪詢請求的。

正常客戶端默認設置的請求超時時間是30s,但這裏咱們發現服務端「偷偷」的給減掉了500ms,如今超時時間只剩下了29.5s,那爲何要這樣作呢?

用官方的解釋之因此要提早500ms響應請求,爲了最大程度上保證客戶端不會由於網絡延時形成超時,考慮到請求可能在負載均衡時會耗費一些時間,畢竟Nacos最初就是按照阿里自身業務體量設計的嘛!

此時對客戶端提交上來的groupkey的MD5與服務端當前的MD5比對,如md5值不一樣,則說明服務端的配置項發生過變動,直接將該groupkey放入changedGroupKeys集合並返回給客戶端。

MD5Util.compareMd5(req, rsp, clientMd5Map)

如未發生變動,則將客戶端請求掛起,這個過程先建立一個名爲ClientLongPolling的調度任務Runnable,並提交給scheduler定時線程池延後29.5s執行。

ConfigExecutor.executeLongPolling(
                new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));

這裏每一個長輪詢任務攜帶了一個asyncContext對象,使得每一個請求能夠延遲響應,等延時到達或者配置有變動以後,調用asyncContext.complete()響應完成。

asyncContext 爲 Servlet 3.0新增的特性,異步處理,使Servlet線程再也不須要一直阻塞,等待業務處理完畢才輸出響應;能夠先釋放容器分配給請求的線程與相關資源,減輕系統負擔,其響應將被延後,在處理完業務或者運算後再對客戶端進行響應。

ClientLongPolling任務被提交進入延遲線程池執行的同時,服務端會經過一個allSubs隊列保存全部正在被掛起的客戶端長輪詢請求任務,這個是客戶端註冊監聽的過程。

如延時期間客戶端據數一直未變化,延時時間到達後將本次長輪詢任務從allSubs隊列剔除,並響應請求response,這是取消監聽。收到響應後客戶端再次發起長輪詢,循環往復。

處理長輪詢

到這咱們知道服務端是如何掛起客戶端長輪詢請求的,一旦請求在掛起期間,用戶經過管理平臺操做了配置項,或者服務端收到了來自其餘客戶端節點修改配置的請求。

怎麼能讓對應已掛起的任務當即取消,而且及時通知客戶端數據發生了變動呢?

數據變動

管理平臺或者客戶端更改配置項接位置ConfigController中的publishConfig方法。

值得注意得是,在publishConfig接口中有這麼一段邏輯,某個dataId配置數據被修改時會觸發一個數據變動事件Event

ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));

仔細看LongPollingService會發如今它的構造方法中,正好訂閱了數據變動事件,並在事件觸發時執行一個數據變動調度任務DataChangeTask

訂閱數據變動事件

DataChangeTask內的主要邏輯就是遍歷allSubs隊列,上邊咱們知道,這個隊列中維護的是全部客戶端的長輪詢請求任務,從這些任務中找到包含當前發生變動的groupkeyClientLongPolling任務,以此實現數據更變推送給客戶端,並從allSubs隊列中剔除此長輪詢任務。

DataChangeTask

而咱們在看給客戶端響應response時,調用asyncContext.complete()結束了異步請求。

結束語

上邊只揭開了nacos配置中心的冰山一角,實際上還有很是多重要的技術細節都沒說起到,建議你們沒事看看源碼,源碼不須要通篇的看,只要抓住核心部分就夠了。就好比今天這個題目之前我真沒太在乎,忽然被問一會兒吃不許了,果斷看下源碼,並且這樣記憶比較深入(別人嚼碎了餵你的知識老是比本身咀嚼的差那麼點意思)。

nacos的源碼我我的以爲仍是比較樸素的,代碼並無過多炫技,看起來相對輕鬆。你們不要對看源碼有什麼抵觸,它也不過是別人寫的業務代碼而已,just so so!


我是小富~,若是對你有用在看關注支持下,我們下期見~

整理了幾百本各種技術電子書,有須要的同窗自取。技術羣快滿了,想進的同窗能夠加我好友,和大佬們一塊兒吹吹技術。

電子書地址

我的公衆號: 程序員內點事,歡迎交流

相關文章
相關標籤/搜索