nacos服務註冊剖析

最近正在負責將公司內部的服務註冊中心做轉移工作,正準備切入到nacos註冊中心,於是花了些時間去整理學習關於使用nacos的一些筆記,並進行一番文檔的輸出。

使用版本 nacos-1.1.4版本
nacos註冊中心的簡單介紹
nacos其實是一款阿里巴巴開源的註冊中心+配置中心,除此之外nacos還有很多強大的功能。

nacos的文件目錄
在nacos安裝包底下,我們可以大概看到nacos包含了以下幾類文件夾,不同的文件底下存儲了不同的數據信息。
在這裏插入圖片描述
最後在target文件夾底下發現了nacos的jar包,因此我好奇地將其打開來一探究竟。
發現內部的jar將nacos的管理臺源代碼給打包了一份。

通過閱讀源碼發現在工程的內部有個lib目錄文件夾,這裏面似乎有自己希望找到的內容
在這裏插入圖片描述
但是目前還是沒法看到內部的源碼,在工程的外部主要就是結合一些源碼的api做了一套可視化界面的控制檯。
這裏面有份jar命名爲:nacos-server.jar。
在startup.sh腳本里面可以看到,nacos有着對應的腳本細節:
在這裏插入圖片描述
這裏對應了java -jar的命令關鍵行進行控制檯的啓動:
在這裏插入圖片描述

配置中心源碼分析

如何進行本地源碼的debug

選擇Nacos工程,然後設置對應的啓動參數:

-Dnacos.standalone=true -Dnacos.home=F:\nacos-local-config


從github下載一份nacos的源碼之後可以看到內部的基礎結構爲:
在這裏插入圖片描述
整體項目裏面對應的工程有好幾個,這裏我們選擇了Config工程這個模塊進行分析,因爲這裏麪包含了nacos控制檯中的拉去服務詳情,查看配置列表等常用接口,有助於我們對工作中常用功能的深入理解。

參照控制檯的接口路徑很快能定位到controller內容
下邊這段接口是對應了查看配置屬性的內容:

http://127.0.0.1:8848/nacos/v1/cs/configs?search=accurate&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTU5MDE1MzAwM30.
TFlSFPTzKd0-2zmmsFjnghV74JfL_tWwo6BOFYAfdjk&dataId&group&pageNo=1&pageSize=10

配置信息查詢接口對應源代碼:

com.alibaba.nacos.config.server.controller.ConfigController#searchConfig --->
com.alibaba.nacos.config.server.service.repository.PersistService#findConfigInfo4Page

在com.alibaba.nacos.config.server.service.repository.EmbeddedStoragePersistServiceImpl#findConfigInfo4Page
裏面看到有關於sql的查詢,深入內部去查看:
單機版本的nacos使用的是 derby 數據庫進行配置存儲的持久化

在這裏插入圖片描述

具體的初始化步驟位於:
com.alibaba.nacos.config.server.service.datasource.LocalDataSourceServiceImpl#init

初始化的時候會鏈接本地數據庫:
jdbc:derby:F:\nacos-local-config\data\derby-data;create=true

在這裏插入圖片描述
單機版本的nacos重啓之後數據並不會丟失,依舊是從本地的存儲文件中讀取數據信息。
derby數據庫的介紹
官網地址:http://db.apache.org/derby/
一款java語言編寫的內嵌於jvm的數據庫,可以支持sql查詢,以及jdbc協議,關於其持久化,大概推斷是存儲到了指定的目錄文件下邊:
在這裏插入圖片描述
服務列表源碼分析

服務註冊原理跟蹤
根據debug會發現,在com.alibaba.nacos.naming.core.ServiceManager 類裏面包含了相關的服務列表存儲信息:
在這裏插入圖片描述
在源碼裏面會發現存儲這些服務列表的本質就是一個ConcurrentHashMap數據結構:
(採用了ConcurrentHashMap來解決併發衝突問題,1.8之前是採用了分段鎖,但是這種方式的鎖粒度過大,所以後邊改爲了採用cas+synchronized的方式來進行加鎖,通過使用無所插入頭結點,如果插入失敗,說明同一時刻有其他線程進行頭插入,再次循壞插入)

private Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();

有點奇怪,這個map的數據是存儲在內存裏面的,那麼服務在重啓的時候應該是有進行初始化操作。並且當我們將provider的服務下架之後nacos依舊會有服務信息,在服務關閉之後的三十秒後nacos就查詢不出任何信息了。
藉此推測會有一個調度去專門維護這些數據信息。(猜測是心跳機制)

註冊服務信息到nacos的接口:

/nacos/v1/ns/instance
com.alibaba.nacos.naming.controllers.InstanceController#register
->
com.alibaba.nacos.naming.core.ServiceManager#registerInstance

那麼,假設我們通過啓動dubbo工程,註冊dubbo服務到nacos服務中心之後會看到哪些情況呢?

發現循環調用某些接口
【DistroFilter request url】/nacos/v1/ns/instance/beat
【DistroFilter request url】/nacos/v1/ns/instance/list
通過日誌過濾發現會循環調用這兩個接口,後來查詢文檔估計是某些調度在維護兩端的數據。

客戶端會重複發送心跳包到nacos這邊,這份心跳包包含的數據還挺多的。關於心跳模塊涉及到的類爲:

com.alibaba.nacos.client.naming.beat.BeatReactor

發送的心跳數據基本格式通過BeatInfo格式進行數據傳輸。
關於循環發送心跳數據包的核心是藉助了jdk內部的

ScheduledExecutorService

這個api來實現的,相關模板代碼:
在這裏插入圖片描述
這樣就能實現每個三秒發送一次心跳的功能。
同理,在nacos的服務端和客戶端之間也存在心跳協調的代碼:

 class BeatTask implements Runnable {

        BeatInfo beatInfo;

        public BeatTask(BeatInfo beatInfo) {
            this.beatInfo = beatInfo;
        }

        @Override
        public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            long nextTime = beatInfo.getPeriod();
            try {
            //發送心跳包
                JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
                long interval = result.getIntValue("clientBeatInterval");
                boolean lightBeatEnabled = false;
                if (result.containsKey(CommonParams.LIGHT_BEAT_ENABLED)) {
                    lightBeatEnabled = result.getBooleanValue(CommonParams.LIGHT_BEAT_ENABLED);
                }
                BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
                if (interval > 0) {
                    nextTime = interval;
                }
                int code = NamingResponseCode.OK;
                if (result.containsKey(CommonParams.CODE)) {
                    code = result.getIntValue(CommonParams.CODE);
                }
                if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
                //如果服務實例消失或者不存在,則註冊一個服務實例
                    Instance instance = new Instance();
                    instance.setPort(beatInfo.getPort());
                    instance.setIp(beatInfo.getIp());
                    instance.setWeight(beatInfo.getWeight());
                    instance.setMetadata(beatInfo.getMetadata());
                    instance.setClusterName(beatInfo.getCluster());
                    instance.setServiceName(beatInfo.getServiceName());
                    instance.setInstanceId(instance.getInstanceId());
                    instance.setEphemeral(true);
                    try {
                        serverProxy.registerService(beatInfo.getServiceName(),
                            NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
                    } catch (Exception ignore) {
                    }
                }
            } catch (NacosException ne) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                    JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());

            }
            //每隔5秒重新發送一次心跳包
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }
    }

其實我們深入sendbeat函數可以看到最底層就是請求nacos服務端的心跳接口

   public JSONObject sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {

        if (NAMING_LOGGER.isDebugEnabled()) {
            NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
        }
        Map<String, String> params = new HashMap<String, String>(8);
        String body = StringUtils.EMPTY;
        if (!lightBeatEnabled) {
            try {
                body = "beat=" + URLEncoder.encode(JSON.toJSONString(beatInfo), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new NacosException(NacosException.SERVER_ERROR, "encode beatInfo error", e);
            }
        }
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
        params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
        params.put("ip", beatInfo.getIp());
        params.put("port", String.valueOf(beatInfo.getPort()));
        String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, body, HttpMethod.PUT);
        return JSON.parseObject(result);
    }

結合springboot的starter如何做服務發現

首先你可能會有思路推斷,加入了一個starter就能生效,估計是有什麼springboot的自動化配置在生效吧。
springboot也有自己的一套spi機制,將spirng.factories配置文件下的類進行實例化操作。
在這裏插入圖片描述
然後根據這些配置的類進行初始化操作。

這裏面有個 NacosServiceRegistryAutoConfiguration 類
參考源代碼:

com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration
com.alibaba.cloud.nacos.registry.NacosAutoServiceRegistration
org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration

這個類裏面繼承類spring的事件,ApplicationListener,當spring容器啓動的時候會去觸發onApplicationEvent函數的。

bind(event)-->start --> register--> com.alibaba.nacos.api.naming.NamingService#registerInstance(java.lang.String, java.lang.String, com.alibaba.nacos.api.naming.pojo.Instance)

其實本質就是在這裏調用類nacos的一個遠程方法,關於nacos的遠程方法看看源碼包就瞭解了,這個不難。
註冊的參數

private Instance getNacosInstanceFromRegistration(Registration registration) {
   Instance instance = new Instance();
   instance.setIp(registration.getHost());
   instance.setPort(registration.getPort());
   instance.setWeight(nacosDiscoveryProperties.getWeight());
   instance.setClusterName(nacosDiscoveryProperties.getClusterName());
   instance.setMetadata(registration.getMetadata());

   return instance;
}

整體的註冊源碼其實可以濃縮爲下邊這張圖

在這裏插入圖片描述

nacos的集羣化
基本配置條件:
一般集羣需要至少3個節點。我們先準備3臺機器,我這裏選擇了三臺機器作爲集羣搭建基礎:

192.168.11.200:8748
192.168.11.196:8748
192.168.11.126:8748

首先需要有三臺基本的服務器用於運行多個nacos服務端程序。
然後修改conf配置文件:

[root@localhost conf]# ls
application.properties  application.properties.example  cluster.conf  cluster.conf.example.bak  nacos-logback.xml  nacos-mysql.sql  schema.sql
[root@localhost conf]# cat cluster.conf
#it is ip
#example
192.168.164.131:8848
192.168.164.132:8848
192.168.164.133:8848

最後再配置一下數據庫連接部分:

### Count of DB:
 db.num=1
### Connect URL of DB:
 db.url.0=jdbc:mysql://10.11.9.243:3306/linhao_test?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
 db.user=crm
 db.password=USszJ497whda

啓動之後日誌會有明顯說明提示nacos的集羣已經部署成功。
如果需要方便操作可以藉助使用nginx來做頁面的轉發。

upstream nacos_server {
  server 192.168.11.200:8748;
  server 192.168.11.196:8748;
  server 192.168.11.126:8748;
}

server {
  listen 80;
  server_name localhost;
  #charset koi8-r;
  #access_log logs/host.access.log main;
  location / {
    proxy_pass http://nacos_server;
    index index.html index.htm;
  }
}

初始化登錄賬號

登錄賬號可以從源碼裏面翻查,然後根據這裏的加密方式在數據庫裏面設置賬號信息:

package com.alibaba.nacos.console.utils;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * Password encoder tool
 *
 * @author nacos
 */
public class PasswordEncoderUtil {

    public static void main(String[] args) {
        System.out.println(new BCryptPasswordEncoder().encode("nacos"));
    }

    public static Boolean matches(String raw, String encoded) {
        return new BCryptPasswordEncoder().matches(raw, encoded);
    }

    public static String encode(String raw) {
        return new BCryptPasswordEncoder().encode(raw);
    }
}

下邊這段是nacos初始化時候給定的賬號密碼:

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');

經過檢測,不同賬號登錄nacos看到的基礎配置信息大多都是相似的。
在這裏插入圖片描述
nacos裏面的日誌輸出在nacos-logback.xml 配置了日誌輸出位置和等級,如果需要跟蹤或者調整可以進去進行修改。

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