SpringCloud原始碼學習筆記3——Nacos服務註冊原始碼分析

2023-04-08 21:00:20

系列文章目錄和關於我

一丶基本概念&Nacos架構

1.為什麼需要註冊中心

  • 實現服務治理、服務動態擴容,以及呼叫時能有負載均衡的效果。

    如果我們將服務提供方的ip地址設定在服務消費方的組態檔中,當服務提供方範例上線下線,消費方都需要重啟服務,導致二者耦合度過高。註冊中心就是在二者之間加一層,實現解耦合。

  • 健康檢查和服務摘除:主動的檢查服務健康情況,對於宕機的服務將其摘除服務列表

2.Nacos 的架構

  • Naming Service :註冊中心,提供服務註冊,登出,管理
  • Config Service:設定中心,Nacos 設定中心為服務設定提供了編輯、儲存、分發、變更管理、歷史版本管理等功能,並且支援在範例執行中,更改設定。
  • OpenAPI:nacos對外暴露的介面,Provider App(服務提供者)就是呼叫這裡的介面,實現將自己註冊到nacos,Consumer App(服務消費者)也是使用這裡的介面拉去設定中心中的服務提供者的資訊。

3.nacos資料模型

二丶nacos註冊中心簡單使用

我們使用nacos作為註冊中心,只需要下載nacos提供的jar包並執行啟動nacos服務,然後在服務提供者,消費者中引入spring-cloud-starter-alibaba-nacos-discovery,並設定spring.cloud.nacos.discovery.server-addr=nacos服務啟動的地址,即可在nacos視覺化介面看到:

那麼服務是如何註冊到nacos的暱?

三丶服務註冊原始碼分析

當我們服務引入spring-cloud-starter-alibaba-nacos-discovery,便可以實現自動進行註冊,這是因為在spring.facotries中自動裝配了NacosServiceRegistryAutoConfiguration

SpringBoot原始碼學習1——SpringBoot自動裝配原始碼解析+Spring如何處理設定類的

1.NacosServiceRegistryAutoConfiguration 引入了哪些類

點進NacosServiceRegistryAutoConfiguration 原始碼中,發現它注入了一下三個類

1.1.NacosServiceRegistry

  • ServiceInstance 表示的是服務發現中的一個範例

    這個介面定義了類似於getHost,getIp獲取註冊範例host,ip等方法,是springcloud定義的規範介面

  • Registration一個標記介面,ServiceRegistry<R>這裡面的R泛型就是Registration

    是springcloud定義的規範介面

  • ServiceRegistry 服務註冊,定義如何向註冊中心進行註冊,和取消註冊

    這個介面定義了register服務註冊,deregister服務取消註冊等方法,入參是Registration。它是springcloud定義的規範介面。

spring cloud 定義了諸多規範介面,無論是服務註冊,還是負載均衡,讓其他中介軟體實現
  • NacosServiceRegistry nacos服務註冊介面,實現了ServiceRegistry,定義瞭如何註冊,如何取消註冊,維護服務狀態等。

1.2.NacosRegistration

NacosRegistrationRegistration的實現類,象徵著一個Nacos註冊中心的服務,也就是我們自己寫的springboot服務

1.3.NacosAutoServiceRegistration

  • AutoServiceRegistration一個標記介面,表示當前類是一個自動服務註冊類
  • AbstractAutoServiceRegistration 實現了ApplicationListener,監聽WebServerInitializedEvent web服務初始化結束事件,在ApplicationListener#onApplicationEvent中進行服務註冊
  • NacosAutoServiceRegistration使用NacosServiceRegistryNacosRegistration的註冊到nacos註冊中心

一通分析之後,可以看到NacosAutoServiceRegistration 是最核心的類,它負責監聽事件,呼叫NacosServiceRegistry,將服務註冊到註冊中心。

2.AbstractAutoServiceRegistration 監聽事件進行註冊

此類是SpringCloud提供的模板類,讓市面上眾多註冊中心中介軟體實現它,快速接入SpringCloud生態。

2.1 WebServerInitializedEvent 從何而來

AbstractAutoServiceRegistration想響應WebServerInitializedEvent ,那麼WebServerInitializedEvent 是哪兒發出的暱?

WebServerStartStopLifecycle#start方法

WebServerStartStopLifecycle實現了Lifecycle,在spring容器重新整理結束的時候,會使用LifecycleProcessor呼叫所以Lifecycle#start,從而傳送ServletWebServerInitializedEvent(WebServerInitializedEvent子類)推播事件

Reactive的springboot上下文則是由WebServerStartStopLifecycle推播ReactiveWebServerInitializedEvent事件,原理一樣,如下圖

2.2 NacosAutoServiceRegistration如何進行服務註冊

AbstractAutoServiceRegistration在響應事件後,會呼叫bind方法,進而呼叫register進行服務註冊,這裡就會呼叫到NacosAutoServiceRegistration#register

那麼到底如何進行服務註冊?

可以看到直接呼叫NacosServiceRegistry#register(NacosRegistration)進行服務註冊

3.NacosServiceRegistry 服務註冊

可以看到這裡使用NamingServiceInstance進行註冊

  • NamingService,nacos框架中的類,負責服務註冊和取消註冊
  • Instance,nacos框架中的類,定義一個服務,記錄ip,埠等資訊
可以看到nacos有自己一套東西,脫離springcloud,也可以使用,這就是鬆耦合

下面我們看下NamingService是如何進行服務註冊的

  • 如果是臨時範例,會使用ScheduledThreadPoolExecutor,每5秒傳送一次心跳,傳送心跳即請求nacos註冊中心/instance/beat介面

  • 然後呼叫NamingProxy 進行服務註冊

    最終底層通過Http請求的方式,請求nacos服務的/nacos/v1/ns/instance

4.nacos註冊中心如何處理服務註冊的請求

上面一通分析,我們直到了springboot服務是如何啟動的時候,自動進行服務註冊的,如何進行服務註冊的,但是nacos伺服器端是如何響應註冊請求的的暱

  • 從請求中拿範例資訊

    主要包含上述這些欄位。

  • ServiceManager#registerInstance

    服務註冊的邏輯主要在addInstance方法中

    首先根據待註冊服務的namespaceId命名中間idserviceName服務名稱ephemeral是否臨時服務構建出一個key,由於我們是一個臨時範例,key最終為com.alibaba.nacos.naming.iplist.ephemeral + namespaceId ## + serviceName

    然後呼叫ConsistencyService一致性協定服務#put進行註冊,這裡和Nacos支援AP,CP架構有關,後續我們分析到一致性協定再補充。

    這裡會呼叫到DelegateConsistencyServiceImpl(一致性協定門面)他會根據key中的是臨時範例,還是非臨時範例,選擇協定,最終選擇到DistroConsistencyServiceImpl,繼續呼叫put方法

    可以看到DistroConsistencyServiceImpl(Distro一致性協定服務)會同步到nacos叢集中的其他範例,這部分我們後續分析,我們重點看下onPut,看看nacos服務到底如何註冊。

    至此服務註冊請求結束了,只是將註冊請求資訊包裝成了任務加入到Notifier的任務佇列中。

5.nacos 服務登入檔結構

在看怎麼處理阻塞佇列中的任務前,我們看下nacos的登入檔結構

對應ServiceManager中的serviceMap屬性

/**
 * key 是名稱空間
 * value 是 分組名稱和Service服務的map
 *
 */
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();

//Service結構如下
//叢集和叢集物件組成map
private Map<String, Cluster> clusterMap = new HashMap<>();

//Cluster 中的屬性記錄所有範例Instance的集合

6.nacos服務註冊非同步任務佇列處理註冊任務

上面分析到最終服務註冊請求被包裝放到Notifier的任務佇列中。我們看下任務佇列的任務在哪裡被拿出來消費。

Notifier實現了Runnable,在DistroConsistencyServiceImpl中使用@PostConstruct將它提交到了排程執行緒池中。

也就是說會有一個單執行緒呼叫Notifier#run

後續會呼叫到Service#onChange,其updateIPs方法會更新範例的ip地址

// 這裡 instances 裡面就包含了新範例物件
// ephemeral 為 ture,臨時範例
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {

    // clusterMap 對應叢集的Map
    Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
    // 把叢集名字都放入到ipMap裡面,value是一個空的ArrayList
    for (String clusterName : clusterMap.keySet()) {
        ipMap.put(clusterName, new ArrayList<>());
    }

    // 遍歷全部的Instance,這個List<Instance> 包含了之前已經註冊過的範例,和新註冊的範例物件
    // 這裡的主要作用就是把相同叢集下的 instance 進行分類
    for (Instance instance : instances) {
        try {
          
            // 判斷使用者端傳過來的是 Instance 中,是否有設定 ClusterName
            if (StringUtils.isEmpty(instance.getClusterName())) {
                // 如果沒有,就給ClusterName賦值為 DEFAULT
                instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
            }

            // 判斷之前是否存在對應的 ClusterName,如果沒有則需要建立新的 Cluster 物件
            if (!clusterMap.containsKey(instance.getClusterName())) {
                // 建立新的叢集物件
                Cluster cluster = new Cluster(instance.getClusterName(), this);
                cluster.init();
                // 放入到叢集 clusterMap 當中
                getClusterMap().put(instance.getClusterName(), cluster);
            }

            // 通過叢集名字,從 ipMap 裡面取
            List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
            // 只有是新建立叢集名字,這裡才會為空,之前老的叢集名字,在方法一開始裡面都 value 賦值了 new ArrayList物件
            if (clusterIPs == null) {
                clusterIPs = new LinkedList<>();
                ipMap.put(instance.getClusterName(), clusterIPs);
            }

            // 把對應叢集下的instance,新增進去
            clusterIPs.add(instance);
        } catch (Exception e) {
        }
    }

    // 分好類之後,針對每一個 ClusterName ,寫入到登入檔中
    for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
        // entryIPs 已經是根據ClusterName分好組的範例列表
        List<Instance> entryIPs = entry.getValue();
        
        // 對每一個 Cluster 物件修改登入檔  ->updateIps
        clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
    }

}

針對每一個叢集分別進行Cluster#updateIps

public void updateIps(List<Instance> ips, boolean ephemeral) {

    // 先判斷是否是臨時範例
    // ephemeralInstances 臨時範例
    // persistentInstances 持久化範例
    // 把對應資料先拿出來,放入到 新建立的 toUpdateInstances 集合中
    Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;

    // 先把老的範例列表複製一份 , 先複製一份新的
    //寫時複製,先複製一份
    HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
    for (Instance ip : toUpdateInstances) {
        oldIpMap.put(ip.getDatumKey(), ip);
    }

    //省略了同步到其他nacos服務的程式碼。。。
    
    // 最後把傳入進來的範例列表,重新初始化一個 HaseSet,賦值給toUpdateInstances
    toUpdateInstances = new HashSet<>(ips);
    
    // 判斷是否是臨時範例
    if (ephemeral) {
        // 直接把之前的範例列表替換成新的
        ephemeralInstances = toUpdateInstances;
    } else {
        persistentInstances = toUpdateInstances;
    }
}