<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
對於Nacos
大家應該都不太陌生,出身阿里名聲在外,能做動態服務發現、設定管理,非常好用的一個工具。然而這樣的技術用的人越多面試被問的概率也就越大,如果只停留在使用層面,那面試可能要吃大虧。
比如我們今天要討論的話題,Nacos
在做設定中心的時候,設定資料的互動模式是伺服器端推過來還是使用者端主動拉的?
這裡我先丟擲答案:使用者端主動拉的!
接下來咱們扒一扒Nacos
的原始碼,來看看它具體是如何實現的?
設定中心
聊Nacos
之前簡單回顧下設定中心的由來。
簡單理解設定中心的作用就是對設定統一管理,修改設定後應用可以動態感知,而無需重啟。
因為在傳統專案中,大多都採用靜態設定的方式,也就是把設定資訊都寫在應用內的yml
或properties
這類檔案中,如果要想修改某個設定,通常要重啟應用才可以生效。
但有些場景下,比如我們想要在應用執行時,通過修改某個設定項,實時的控制某一個功能的開閉,頻繁的重啟應用肯定是不能接受的。
尤其是在微服務架構下,我們的應用服務拆分的粒度很細,少則幾十多則上百個服務,每個服務都會有一些自己特有或通用的設定。假如此時要改變通用設定,難道要我挨個改幾百個服務設定?很顯然這不可能。所以為了解決此類問題設定中心應運而生。
推與拉模型
使用者端與設定中心的資料互動方式其實無非就兩種,要麼推push
,要麼拉pull
。
推模型
使用者端與伺服器端建立TCP
長連線,當伺服器端設定資料有變動,立刻通過建立的長連線將資料推播給使用者端。
優勢:長連結的優點是實時性,一旦資料變動,立即推播變更資料給使用者端,而且對於使用者端而言,這種方式更為簡單,只建立連線接收資料,並不需要關心是否有資料變更這類邏輯的處理。
弊端:長連線可能會因為網路問題,導致不可用,也就是俗稱的假死
。連線狀態正常,但實際上已無法通訊,所以要有的心跳機制KeepAlive
來保證連線的可用性,才可以保證設定資料的成功推播。
拉模型
使用者端主動的向伺服器端發請求拉設定資料,常見的方式就是輪詢,比如每3s向伺服器端請求一次設定資料。
輪詢的優點是實現比較簡單。但弊端也顯而易見,輪詢無法保證資料的實時性,什麼時候請求?間隔多長時間請求一次?都是不得不考慮的問題,而且輪詢方式對伺服器端還會產生不小的壓力。
開篇我們就給出了答案,nacos
採用的是使用者端主動拉pull
模型,應用長輪詢(Long Polling
)的方式來獲取設定資料。
額?以前只聽過輪詢,長輪詢又是什麼鬼?它和傳統意義上的輪詢(暫且叫短輪詢吧,方便比較)有什麼不同呢?
短輪詢
不管伺服器端設定資料是否有變化,不停的發起請求獲取設定,比如支付場景中前段JS輪詢訂單支付狀態。
這樣的壞處顯而易見,由於設定資料並不會頻繁變更,若是一直髮請求,勢必會對伺服器端造成很大壓力。還會造成推播資料的延遲,比如:每10s請求一次設定,如果在第11s時設定更新了,那麼推播將會延遲9s,等待下一次請求。
為了解決短輪詢的問題,有了長輪詢方案。
長輪詢
長輪詢可不是什麼新技術,它不過是由伺服器端控制響應使用者端請求的返回時間,來減少使用者端無效請求的一種優化手段,其實對於使用者端來說與短輪詢的使用並沒有本質上的區別。
使用者端發起請求後,伺服器端不會立即返回請求結果,而是將請求掛起等待一段時間,如果此段時間內伺服器端資料變更,立即響應使用者端請求,若是一直無變化則等到指定的超時時間後響應請求,使用者端重新發起長連結。
為了後續演示操作方便我在本地搭了個Nacos
。注意: 執行時遇到個小坑,由於Nacos
預設是以cluster
叢集的方式啟動,而本地搭建通常是單機模式standalone
,這裡需手動改一下啟動指令碼startup.X
中的啟動模式。
直接執行/bin/startup.X
就可以了,預設使用者密碼均是nacos
。
Nacos
設定中心的幾個核心概念:dataId
、group
、namespace
,它們的層級關係如下圖:
dataId
:是設定中心裡最基礎的單元,它是一種key-value
結構,key
通常是我們的組態檔名稱,比如:application.yml
、mybatis.xml
,而value
是整個檔案下的內容。
目前支援JSON
、XML
、YAML
等多種設定格式。
group
:dataId設定的分組管理,比如同在dev環境下開發,但同環境不同分支需要不同的設定資料,這時就可以用分組隔離,預設分組DEFAULT_GROUP
。
namespace
:專案開發過程中肯定會有dev
、test
、pro
等多個不同環境,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(); // 1、獲取dataId對應的CacheData,如沒有則向伺服器端發起長輪詢請求獲取設定 CacheData cache = addCacheDataIfAbsent(dataId, group, tenant); synchronized (cache) { // 2、註冊對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這些相關的基礎屬性,還有幾個比較重要的屬性如:listeners
、md5
(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
佇列,上邊我們知道,這個佇列中維護的是所有使用者端的長輪詢請求任務,從這些任務中找到包含當前發生變更的groupkey
的ClientLongPolling
任務,以此實現資料更變推播給使用者端,並從allSubs
佇列中剔除此長輪詢任務。
DataChangeTask
而我們在看給使用者端響應response
時,呼叫asyncContext.complete()
結束了非同步請求。
上邊只揭開了nacos
設定中心的冰山一角,實際上還有非常多重要的技術細節都沒提及到,建議大家沒事看看原始碼,原始碼不需要通篇的看,只要抓住核心部分就夠了。就比如今天這個題目以前我真沒太在意,突然被問一下子吃不準了,果斷看下原始碼,而且這樣記憶比較深刻(別人嚼碎了餵你的知識總是比自己咀嚼的差那麼點意思)。
nacos
的原始碼我個人覺得還是比較樸素的,程式碼並沒有過多炫技,看起來相對輕鬆。大家不要對看原始碼有什麼抵觸,它也不過是別人寫的業務程式碼而已,just so so!
以上就是阿里面試Nacos設定中心互動模型是push還是pull原理解析的詳細內容,更多關於Nacos設定中心互動模型的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45