背景

前段時間有新聞報道,國外HashiCorp在官網宣佈:不允許中國境內使用、部署和安裝該企業旗下的企業版產品和軟件。

其中Consul是Java的spring cloud開發者非常熟悉的一個服務發現和配置中心的中間件,很多人擔心是否Consul會受到影響,目前來看HashiCorp只是對商業版進行了禁止使用,還沒有對開源版本進行限制,所以使用Consul的小夥伴不用擔心。但是隨着時間的發展,不同地區的對抗會不斷的升級,說不定有一天開源的版本會被也會被宣佈禁用,所以我們需要知道如何去替代Consul。

在2008年的時候,那個時候zk還沒出來,阿里巴巴當時內部需要做服務發現,於是自研了ConfigServer,過了十年,在2018年7月的時候,阿里發佈了Nacos(ConfigServer開源實現)0.1.0版本,到現在快兩年了已經到了1.3.0版本,現在已經可以支持很多功能了:

  • 服務註冊和發現:nacos和很多rpc框架已經做了集成,比如dubbo,SpringCloud等,方便我們拿來即用,同時也開放了比較簡易的api方便我們對自己的rpc進行定製。

  • 配置管理:類似apllo的一個配置管理中心,讓我們不用把配置寫在文件中了,在後臺進行統一的管理。

  • 地址服務器: 方便我們對不同環境不同隔離場景的nacos進行尋址。

  • 安全與穩定性: 性能監控,加密傳輸,權限控制管理等等。

對於nacos的來說,最大的核心功能就是服務註冊和配置管理,我的文章主要也是介紹這兩大模塊,這篇文章主要是介紹Nacos服務發現-註冊相關的一些使用,原理以及對比其他的一些優化。

基本概念

首先我們來看看再Nacos中服務發現-註冊的一些基本概念:

  • 命名空間(namespace):命名空間屬於Nacos頂層的結構,用於進行租戶級別的隔離,我們最常用的就是不同環境比如測試環境,線上環境進行隔離。

  • 服務(Service):服務的概念就和我們平常的微服務一一對應,比如訂單服務,物流服務等等。一個命名空間下可以有多個Service,不同的命名空間可以有相同的Service,比如測試環境和線上環境都可以有訂單服務。

  • 虛擬集羣:一個服務下所有的機器組成一個集羣,在Nacos中還可以對集羣根據需要進行進一步劃分成虛擬集羣。

  • 實例:粗略一點理解就是一臺機器或者一個虛擬機就是一個實例,細粒一點理解就是一個或多個服務的具有可訪問網絡地址(IP:Port)的進程。

上面是Nacos官網文檔中給出的服務領域模型圖,從圖上我們可以知道層級關係的屬於是:服務-集羣-實例,同時在服務,集羣和實例中都會保存一些數據用作其他的需求。

其實一說到服務註冊很多人首先會想到Zookeeper,其實ZK並沒有直接提供服務註冊訂閱的功能,在ZK中要實現這些功能,你必須要自己一個一個的去劃分文件目錄,非常不方便,並且ZK的Api也特別難用,對於Nacos來說服務註冊的Api使用如下:

        Properties properties = new Properties();
        properties.setProperty("serverAddr", System.getProperty("serverAddr"));
        properties.setProperty("namespace", System.getProperty("namespace"));

        NamingService naming = NamingFactory.createNamingService(properties);

        naming.registerInstance("microservice-mmp-marketing", "11.11.11.11", 8888, "TEST1");

        naming.subscribe("microservice-mmp-marketing", new EventListener() {
            @Override
            public void onEvent(Event event) {
                System.out.println(((NamingEvent)event).getServiceName());
                System.out.println(((NamingEvent)event).getInstances());
            }
        });

我們只需要創建一個NamingService,然後調用registerInstance方法和subscribe方法就可以完成我們服務的註冊和訂閱了。

如果對Nacos快速接入有興趣的同學可以去官網詳細看一下,這裏就不展開介紹了,https://nacos.io/zh-cn/docs/quick-start.html

AP or CP

CAP

說到分佈式系統就一定離不開CAP定理,CAP定理叫作布魯爾定理。對於設計分佈式系統來說(不僅僅是分佈式事務)的架構師來說,CAP就是你的入門理論。

  • C (一致性):對某個指定的客戶端來說,讀操作能返回最新的寫操作。對於數據分佈在不同節點上的數據上來說,如果在某個節點更新了數據,那麼在其他節點如果都能讀取到這個最新的數據,那麼就稱爲強一致,如果有某個節點沒有讀取到,那就是分佈式不一致。

  • A (可用性):非故障的節點在合理的時間內返回合理的響應(不是錯誤和超時的響應)。可用性的兩個關鍵一個是合理的時間,一個是合理的響應。合理的時間指的是請求不能無限被阻塞,應該在合理的時間給出返回。合理的響應指的是系統應該明確返回結果並且結果是正確的,這裏的正確指的是比如應該返回50,而不是返回40。

  • P (分區容錯性):當出現網絡分區後,系統能夠繼續工作。打個比方,這裏個集羣有多臺機器,有臺機器網絡出現了問題,但是這個集羣仍然可以正常工作。

熟悉CAP的人都知道,三者不能共有,如果感興趣可以搜索CAP的證明,在分佈式系統中,網絡無法100%可靠,分區其實是一個必然現象,如果我們選擇了CA而放棄了P,那麼當發生分區現象時,爲了保證一致性,這個時候必須拒絕請求,但是A又不允許,所以分佈式系統理論上不可能選擇CA架構,只能選擇CP或者AP架構。

對於CP來說,放棄可用性,追求一致性和分區容錯性,我們的zookeeper其實就是追求的強一致。

對於AP來說,放棄一致性(這裏說的一致性是強一致性),追求分區容錯性和可用性,這是很多分佈式系統設計時的選擇,後面的BASE也是根據AP來擴展。

順便一提,CAP理論中是忽略網絡延遲,也就是當事務提交時,從節點A複製到節點B,但是在現實中這個是明顯不可能的,所以總會有一定的時間是不一致。同時CAP中選擇兩個,比如你選擇了CP,並不是叫你放棄A。因爲P出現的概率實在是太小了,大部分的時間你仍然需要保證CA。就算分區出現了你也要爲後來的A做準備,比如通過一些日誌的手段,是其他機器回覆至可用。

註冊中心的選擇

上面說了所有的分佈式系統都會對CAP進行抉擇,同樣的註冊中心也不例外。Zookeeper是很多做服務註冊中心的首選,我目前所在的公司也在使用ZooKeeper當做註冊中心,但是隨着公司的發展,ZK越來越不穩定,並且多機房的服務發現也非常困難,在這篇文章中阿里巴巴爲什麼不用 ZooKeeper 做服務發現?,更加詳細的闡述了阿里爲什麼不用ZK當作註冊中心。這裏我簡單的闡述一下:

  • 性能不滿足,無法水平擴展:熟悉ZK的同學都知道ZK是往Leader(主節點)去寫數據的,所以很難進行水平擴展,當公司達到一定規模的時候,ZK就不適合做註冊中心,頻繁的讀寫很容易導致ZK不穩定。對於這種情況,可以分多個ZK集羣,但是就涉及到一個問題,開始的時候各個集羣可能不會打交道,但是到後期如果有什麼協同的業務,各個集羣的服務之間互相調用就成爲了一個難點。

  • ZooKeeper API難用:Zookeeper的使用真的是需要一個比較精通的專家,你需要熟悉他的很多異常,以及對這些異常到底做什麼處理。

  • 註冊中心不需要存儲一些歷史的變化:註冊中心理論上來說只需要知道此時此刻在註冊中心上,有哪些服務和實例進行了註冊,但是ZK會持續的記錄事務日誌,以便後續進行修復。

  • 同機房不可連通:如果我們有一個三機房容災5節點部署結構,如下圖所示:

    如果機房3 和 機房1,2 出現了網絡分區,也就是機房內部是好的,但是機房之間是不連通的,由於本機房的ZK出現了分區,當前ZK也會不可用的,那麼機房內部的服務就會無法使用註冊中心導致無法進行互相調用,很明顯這個是不允許的,機房內部之間的網絡是好的,就應該允許在同機房內部進行調用。

基於上面來看ZK已經不太適合作爲我們的註冊中心來用了,換句話來說註冊中心不太需要CP,我們應該更多的提供是AP。

Nacos中的協議

Distro

在Nacos的Instance(實例)中提供了一個ephemeral字段,這個字段是bool類型,這個字段和ZK中的含義差不多,都代表的是否是臨時節點,在Nacos中如果是臨時節點就會使用AP協議,如果不是臨時節點就會走CP。當然在註冊中心中所有的實例其實默認都是臨時節點。

在Nacos中爲了實現AP,自己定製了一套Distro協議。下面我們分析一下Distro到底是什麼:

純內存保存

在Distro中所有的數據都是用內存進行保存,在DistroConsistencyService中有:

可以看見Distro是用ConcurrentHashMap作爲存儲的容器,不需要使用額外的文件進行存儲。有同學會問了如果我這個機器宕機了,內存信息就全部丟失了,那我這部分數據怎麼恢復呢?

在DistroConsistencyService的load方法中,我們遍歷所有的非自己的Server,然後將數據進行同步,如果其中某一個同步成功了,那就不需要向其他的Server發出同步信息,。這樣就可以將數據全部恢復。

最終一致

雖然我們說的是AP,但是其實我們還是需要保證最終一致,防止長時間各個節點數據不一致,在DistroConsistencyService的Put方法中會對TaskDispatcher添加一個任務,如下圖:

TaskDispatcher其實叫DataSyncTaskDispatcher 更加貼切,主要用來進行數據同步:

其核心邏輯就是上面的whie循環,主要看我標紅的代碼,這裏並不是來一個更新就發送一個,如果服務的變更比較頻繁,如果來一個發送一個效率必然很低,所以在這裏採取了一個合併的策略,如果更新的數據達到一定的數量這裏默認是1000,或者說距離上一次發送已經超過了一定的時間這裏默認是2s,都會進行一個發送,這裏的發送也是生成一個SyncTask然後放到線程池中進行異步發送。

這個時候有同學會問,如果我某個機器剛剛上線,剛好沒有收到更新的這個數據這個怎麼辦呢?在Nacos中也會有一個兜底的策略TimedSync,這個任務每5s會執行一次,具體的執行任務代碼如下:

注意圈紅的部分,這裏並不是同步所有的數據,而是遍歷所有的數據,將屬於自己管理那部分數據纔會同步(什麼數據才屬於自己管理呢?下個小節會細講),然後獲取所有的非自己的Server將這些數據進行check發送。

通過這兩種方式:實時的更新和定時的更新我們就可以保證所有的Nacos節點上的數據最後都是最終一致。

水平擴展

ZK的一個缺點就是無法進行水平擴展,這個是CP的一大問題,隨着公司的發展,規模變大之後,你很難在撐起現在的業務了。在Distro中沒有Leader這個角色,每個節點都可以處理讀寫,通過這樣的方式,我們可以任意的水平擴展Nacos的節點來完成我們的需要。

在Distro中並不是每個節點都可以處理所有的讀請求,但是寫請求並不是每個節點都可以處理的,每個節點會根據key的hash值來判斷是否應該是自己處理。寫請求訪問的是域名這個是會隨機打到每個節點上的,Nacos是怎麼做到讓這些寫請求打到對應的機器上呢,這個答案就在DistroFilter中:

這個ServletFilter會對每個請求做一些過濾,如果發現這個請求不是自己的,那麼就會轉發這個請求到對應的服務器進行處理,收到結果之後再返回給用戶。

這裏Nacos其實可以做個優化,我們可以發現轉發的時候這個動作是同步的,我們這裏可以使用異步發送,並且開啓serlvet的異步,這個轉發節點就可以類似網關一樣不需要同步的等待,可以增加Nacos集羣的吞吐量

Distro的這些優勢對比其他的CP協議來說在註冊中心這方面非常大,並且整個協議的實現也比他們簡單很多,非常容易理解,如果以後大家涉及註冊中心協議的一些涉及,可以參照這種思路。

Raft

在Nacos中其實也有強一致性的協議,使用的是Raft,有兩個地方使用了Raft:

  • 註冊中心中,Nacos中有一些數據需要持久化存儲的,我們會使用Raft去進行數據的一致性同步存儲,比如Service,命名空間的一些數據,Nacos認爲實例是一個變化比較快的,臨時的數據,不需要Raft這種一致性較高的協議,但是Service和命名空間是一個變化比較少的數據,適合做持久化存儲。註冊中心的Raft實現。目前是使用的自己寫的一套Raft,閱讀了一下細節和標準的Raft還是有一些差別的,比如日誌的連續性在Nacos的Raft協議沒有進行保證。

  • Nacos在1.3.0之後爲了開始逐漸的使用標準的raft協議的實現sofa-jraft,暫時只在配置中心中進行了使用,配置中心之前只能使用mysql存儲,1.3.0之後Nacos借鑑了 Etcd 的通過Raft協議將單機KV存儲轉變爲分佈式的KV存儲的設計思想,基於SOFA-JRaft以及Apache Derby構建了一個輕量級的分佈式關係型數據庫,同時保留了使用外置數據源的能力,用戶可以根據自己的實際業務情況選擇其中一種數據存儲方案。

具體的Raft協議細節,這裏就不展開細說了,有興趣的可以看一下翻譯的論文:

https://www.infoq.cn/article/raft-paper

節點的註冊和訂閱

在註冊中心中另一個比較重要的就是我們的節點如何進行註冊和訂閱,如何進行心跳檢測防止節點宕機,訂閱的節點如何能實時收到更新。

節點的註冊

naming.registerInstance("microservice-mmp-marketing", "11.11.11.11", 8888, "TEST1");

我們只需要簡單的寫上面這行代碼就可以完成我們節點的註冊了。上面代碼對ServiceName爲 microservice-mmp-marketing ,在 TEST1 Clsuter下,註冊了一個Ip爲 11.11.11.11 ,端口爲 8888 的實例,在註冊的同時會添加一個心跳任務

如上圖紅色的部分,向線程池中添加了一個延遲心跳任務,默認是5s執行,

BeatTask

中,首先會向Nacos-Server發送心跳,如果Nacos-Server定義了心跳的間隔接下來就會根據返回的結果修改下一次執行的時間,如果我們的服務不存在,那麼說明我們的機器因爲某種原因沒有同步心跳,導致已經被摘除了,這裏需要再次註冊這個節點。

在Nacos-Server的ClientBeatCheckTask中,我們會根據Service維度,定時去掃描Service下是否有長時間沒有同步的實例,默認是15s,如下面的紅框中的代碼:

節點的訂閱

節點的訂閱在不同的註冊中心中都有不同的實現,一般的套路分爲兩種輪訓和推送。

推送是指當訂閱的節點發生更新的時候會主動向訂閱方進行推送,我們的ZK就是推送的實現方式,客戶端和服務端會建立一個TCP長連接,客戶端會註冊一個watcher,然後當有數據更新的時候,服務端會通過長連接進行推送。通過這種建立長連接的模式,會嚴重消耗服務端的資源,所以當watcher比較多,並且當更新頻繁的時候,Zookeeper的性能會非常低,甚至掛掉。

輪訓是指我們訂閱的節點主動定時獲取服務端節點的信息,然後再本地去做一個比對,如果有改變就會做一些更新。在Consul中也有一個watcher機制,但和ZK不一樣的是,他是通過Http長輪詢去實現的,Consul服務端會對請求的url中是否包含wait參數進行立即返回,還是先掛起等待指定wait時間內如果服務有變化在返回。使用輪訓的性能可能較高但是實時性就可能不是太好。

在Nacos中,結合了這兩個思想,既提供了輪訓又提供了主動推送,我們先來看看輪訓的部分,再UpdateTask類中的run方法中:

注意看上面的紅框中,我們會根據Service維度去定時輪訓,我們的ServiceInfo,然後去更新。

我們再來看下,Nacos是如何實現推送功能的,Nacos會記錄上面我們的訂閱者到我們的PushService, 下面是我們的PushService中的推送核心代碼:

  • Step 1:通過ServiceChangeEvent事件觸發我們的推送,這裏要注意的是因爲我們的節點都是通過distro進行更新,當我們distroT同步到其他機器上時,同樣也會觸發這個事件。

  • Step 2:獲取本機上維護的訂閱者,因爲訂閱者是根據是否查詢過服務節點來定義的,查詢過服務節點這個動作會被隨機的打到不同的Nacos-Server上,所以我們每個節點都會維護一部分訂閱者,並且維護的訂閱者之間還會有重複,由於後續是UDP發送,重複維護訂閱者的成本不是很高。

  • Step3:生成ackEntry,也就是我們發送的內容並且將其緩存起來,這裏緩存主要是防止重複做壓縮的過程。

  • Step4: 最後進行udp的發送

Nacos這種推送模式,對於Zookeeper那種通過tcp長連接來說會節約很多資源,就算大量的節點更新也不會讓Nacos出現太多的性能瓶頸,在Nacos中客戶端如果接受到了udp消息會返回一個ACK,如果一定時間Nacos-Server沒有收到ACK,那麼還會進行重發,當超過一定重發時間之後,就不在重發了,雖然通過udp並不能保證能真正的送到訂閱者,但是Nacos還有定時輪訓作爲兜底,不需要擔心數據不會更新的情況。

Nacos通過這兩種手段,既保證了實時性,又保證了數據更新不會漏掉。

總結

Nacos雖然是一個新開源的項目,但是其架構,源碼設計都是非常的精巧,比如在內部源碼中使用了很多EventBus這種架構,將很多流程都解耦開來,並且其Distro協議思路也是非常值得我們學習的。

在Nacos的註冊中心中還有一些其他的細節,比如根據標籤進行Selector等等。這些有興趣的可以下來再去Nacos文檔中瞭解一下。

如果大家覺得這篇文章對你有幫助,你的關注和轉發是對我最大的支持,O(∩_∩)O:

相關文章