萬字長文 | Spring Cloud Alibaba元件之Nacos實戰及Nacos客戶端服務註冊原始碼解析

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


滴滴滴,上車了!

本次旅途,你將獲取到如下知識:

  • Nacos在微服務架構中的作用
  • Nacos在Linux下的安裝與使用
  • 搭建真實專案環境,實現服務註冊與發現
  • 真實專案環境下實現Nacos的配置管理
  • Nacos叢集配置與叢集資料持久化到MySQL
  • 使用Nginx負載均衡訪問Nacos叢集
  • Nacos客戶端服務註冊原始碼分析

先贊後看,養成習慣。

舉手之勞,贊有餘香。

Nacos在微服務架構中的作用

服務發現

註冊中心是微服務架構中不可缺少的一環,用作 服務註冊發現

為何使用註冊中心?

假設有這樣一個場景,我們乘坐公車需要確定自己的座位在哪裡,才能入座,否則有可能那是別人的座位,等別人來做的時候把我攆起來那就尷尬了。正常的情況應該是有個售票員給我發個帶有座位編號的票,然後我去對號入座,這樣就可以快速找到自己的座位且沒有被攆走的風險。

這裡,售票員 其實就是 提供註冊和發現 的一箇中間聯絡員。座位的資訊已經登記到售票員那裡了,乘客來乘車,只需要找售票員就能快速找到自己的座位。

找車上座位模型

微服務架構體系中,各個微服務元件相互獨立,但最終還要組合為一個整體作為一個軟體系統服務於最終客戶,在整個大系統內部,各個服務之間需要彼此通訊,彼此呼叫方法。

微服務架構內部發起通訊呼叫方法的一方就是 服務消費者,提供遠端方法呼叫的服務稱為 服務提供者

而為了提高系統性能,一般會提供多個伺服器作為 服務提供者,此時 服務消費者 找到 服務提供者 的過程,就類似於乘客上車找座位的過程。

Nacos微服務註冊中心

因此,在微服務架構中都會引入 註冊中心 ,這樣就能使服務的消費者快速的找到它需要的服務提供者。註冊中心實現了服務提供和服務消費的快速撮合功能。

Nacos 提供了一組簡單易用的特性集,可以快速實現動態服務發現。

配置中心

除了服務發現,在服務繁多的微服務架構體系中,配置 的集中化管理也非常重要,因為服務數量有很多,每次修改一個配置有可能需要跟進多個服務對其進行同步修改,然後再重啟這些專案,那就麻煩了。配置中心 就用來完成配置的統一管理,修改一處,實時生效。

可以結合我之前的一篇介紹Apollo配置中心的文章一起食用:分散式配置中心之Apollo實戰

Nacos 不僅能做微服務的註冊中心,同時它還支援做配置中心。

Nacos安裝使用

NacosSpring Cloud Alibaba 的元件之一,支援服務的註冊發現,支援分散式系統的外部化配置和配置的自動重新整理。

現在該把 Nacos 環境支稜起來了。

本文 Nacos 安裝環境:

  • CentOS 7.6 2C 4G
  • JDK 1.8

版本選擇

下載當前官方的推薦版本:2.0.3 ,下載地址:

官方穩定版本下載地址

解壓並啟動

將下載下來的 nacos-server-2.0.3.tar.gz 上傳到伺服器中(伺服器IP地址:192.168.242.129,記住這個IP地址,後面將和MySQL和nginx所在伺服器區別),解壓:

```sh

解壓

tar -zxvf nacos-server-2.0.3.tar.gz

啟動

cd nacos/bin sh startup.sh -m standalone ```

啟動命令中 standalone 代表著單機模式執行,非叢集模式。

驗證Nacos是否啟動成功

可先在伺服器端看nacos服務是否啟動成功:

sh [root@localhost ~]# jps 9763 nacos-server.jar 13720 Jps [root@localhost ~]# ps -ef|grep nacos

Nacos啟動成功

Nacos服務的預設埠是 8848 ,瀏覽器端開啟如下網址驗證:

http://192.168.242.129:8848/nacos

TIP: IP地址換成自己實際環境的IP地址,並注意防火牆、埠開放。

Nacos登入頁

預設使用者名稱密碼:nacos/nacos

登入後可以看到有服務管理、配置管理等:

這樣,一個Nacos服務就配置好了。

專案實戰演示

本專案 SpringCloudAlibabaTest 原始碼倉庫:http://github.com/xblzer/SpringCloudAlibabaTest

因為Nacos既可以作為服務發現註冊中心,也可以是配置中心,所以我這裡也分兩部分進行操作。

demo結構用 Maven父子工程 ,Maven父工程匯入 Spring BootSpring CloudSpring Cloud Alibaba 基礎依賴,各個子工程作為module依賴父工程。

建立Maven父工程

IDE:IntelliJ IDEA

建立一個普通的Maven工程,並刪除IDE自動生成的資料夾和檔案,只保留 pom.xml 檔案:

```xml

4.0.0

<groupId>com.xblzer</groupId>
<artifactId>SpringCloudAlibabaTest</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
</properties>

<!-- 作版本仲裁 -->
<dependencyManagement>
    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.6.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- Spring Cloud -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- Spring Cloud Alibaba -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.0.1.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

```

父工程建立OK。

本文所選版本是:

  • Spring Boot:2.6.3
  • Spring Cloud:2021.0.1
  • Spring Cloud Alibaba:2021.0.1.0

Tip:本文使用的是Spring Cloud Alibaba 2021.0.1.0,該版本對應的Spring Cloud版本為2021.0.1。從 2021.0.1.0 開始,Spring Cloud Alibaba 版本將會對應 Spring Cloud 版本, 前三位為 Spring Cloud 版本,最後一位為擴充套件版本。

Spring Cloud 2021.0.1 新版本使用 Spring Cloud Loadbalancer 做負載均衡,沒有預設整合 Ribbon 了,在進行服務消費者開發的專案中需要引入 Loadbalancer 依賴,這一點需要注意一下。

Part Ⅰ:服務註冊與發現

服務提供者專案

和建立普通Spring Boot專案一樣,建立完成後,刪除無用的檔案,保留src和pom.xml。

因為是子工程,在pom中新增其父工程依賴:

xml <parent> <groupId>com.xblzer</groupId> <artifactId>SpringCloudAlibabaTest</artifactId> <version>1.0-SNAPSHOT</version> <relativePath/> </parent>

依賴中引入 spring-cloud-starter-alibaba-nacos-discovery

xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>

然後在父工程的pom中新增子模組:

xml <modules> <module>cloud-nacos-provider</module> </modules>

Nacos服務提供者配置檔案 application.yml

```yml server: port: 8080

spring: application: name: cloud-nacos-provider

cloud: nacos: discovery: server-addr: 192.168.242.129:8848

management: endpoint: web: exposure: include: "*" ```

主啟動類上加 @EnableDiscoveryClient 註解。

然後啟動 cloud-nacos-provider 專案,看Nacos後臺是否註冊上該服務了:

現在編寫一個對外提供的介面 /test-port ,訪問該介面時,返回專案的埠。寫一個 Controller 就行了:

```java package com.xblzer.cloudnacosprovider.controller;

import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;

/* * @author 行百里者 * @date 2022-06-30 18:29 / @RestController public class ProviderController {

@Value("${server.port}")
private String serverPort;

@GetMapping("/test-port")
public String getServerPort() {
    return "Nacos Provider port:" + serverPort;
}

} ```

既然是對外提供服務,一般我們會多準備幾個服務提供者的伺服器,已提高系統效率和備份,這裡再啟動一個 8081 埠的Provider服務。

啟動兩個Provider服務後,可以看到Nacos後臺服務列表註冊成功:

服務消費者專案

建立子module的過程和前面一樣,主要是配置檔案和pom有些區別。

配置檔案 application.yml

```yml server: port: 9080

spring: application: name: cloud-nacos-consumer

cloud: nacos: discovery: server-addr: 192.168.242.129:8848

消費者要訪問的服務提供者-這些服務提供者已註冊到nacos

service-url: nacos-provider-service: http://cloud-nacos-provider ```

前文提到過,既然是服務消費者,肯定需要去呼叫服務提供者提供的介面,服務提供者是多臺伺服器的,那麼我應該去呼叫哪臺服務(這裡假設不同的埠服務部署在不同的伺服器上)的介面呢?

使用 Spring Cloud Loadbalancer 就可以做負載均衡了,需要引入 Loadbalancer 依賴:

```xml

org.springframework.cloud spring-cloud-starter-loadbalancer ```

引入依賴後,我們只需要在注入 RestTemplate 的時候加上 @LoadBalanced 註解即可。

RestTemplate 是 Spring 提供的用於訪問 Rest 服務的客戶端,它提供了多種邊界訪問遠端 Http 服務的方法,能夠大大提高客戶端的編寫效率。

java @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }

編寫呼叫介面

在介面中呼叫服務提供者的介面 /test-port

```java @RestController public class ConsumerController {

@Resource
private RestTemplate restTemplate;

@Value("${service-url.nacos-provider-service}")
private String serviceUrl;

@GetMapping("/comsume")
public String consume() {
    return restTemplate.getForObject(serviceUrl + "/test-port", String.class);
}

} ```

啟動並驗證是否註冊到Nacos中:

訪問 http://localhost:9080/consume

多次呼叫該介面,返回的資訊在 8080 與 8081 之間切換,可見實現了負載均衡。

Part Ⅱ:配置中心

Nacos不僅僅可以作為註冊中心來使用,同時它支援作為配置中心,我們來看一下怎麼用。

同樣建立module,引入nacos config依賴:

```xml

com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ```

將該子模組新增到父工程:

xml <modules> <module>cloud-nacos-provider</module> <module>cloud-nacos-consumer</module> <module>cloud-nacos-config</module> </modules>

關於配置檔案,需要注意的是,spring-cloud-starter-alibaba-nacos-config 模組移除了 spring-cloud-starter-bootstrap 依賴,如果想以舊版的方式使用,需要手動加上該依賴。

舊版使用方式:

有兩個配置檔案,一個 application.yml ,一個 bootstrap.yml ,在專案初始化時,要保證先從配置中心進行配置拉取,拉取配置之後,才能保證專案的正常啟動。

bootstrap.yml檔案內容:

```yml

nacos配置

server: port: 7071

spring: application: name: nacos-config-client cloud: nacos: discovery: # Nacos服務註冊中心地址 server-addr: 192.168.242.129:8848 config: # Nacos作為配置中心地址 server-addr: 192.168.242.129:8848 # 指定yaml格式的配置 file-extension: yaml ```

這裡bootstrap.yml配置的內容起到兩個作用:

  • 讓7071這個配置服務註冊到Nacos中
  • 去Nacos中讀取指定字尾為yaml的配置檔案

現在推薦使用 spring.config.import 方式引入配置,以上述 bootstrap.yml 的配置為例,spring.config.import 引入方式如下:

配置檔案 application.yml

```yml server: port: 7071

spring: application: name: cloud-nacos-config

cloud: nacos: config: group: DEFAULT_GROUP server-addr: 192.168.242.129:8848 config: import: - nacos:test.yml ```

Tip: 配置檔案的寫法一定要注意,spring.config.import 下面的配置 nacos:test.yml 中間一定不要留空格,否則啟動不成功。

在Nacos,需要在DEFAULT_GROUP下建立一個 test.yml 檔案,這個檔名一定要和 spring.config.import 配置下的 nacos:test.yml 的yml檔名一致。

專案啟動成功,訪問 http://localhost:7071/info :

在Nacos配置管理裡面,動態修改 config.info 的值為 I am a config info, v2 再次訪問介面,將返回新值:

PS:關於舊版本Spring Boot中Nacos配置中心的配置規則

首先必須配置 spring.application.name ,是因為它是構成 Nacos 配置管理 dataId欄位的一部分。

在 Nacos Spring Cloud 中,dataId 的完整格式如下:

${prefix}-${spring.profiles.active}.${file-extension}

  • prefix 預設為 spring.application.name 的值,也可以通過配置項 spring.cloud.nacos.config.prefix來配置。
  • spring.profiles.active 即為當前環境對應的 profile。 注意:當 spring.profiles.active 為空時,對應的連線符 - 也將不存在,dataId 的拼接格式變成 ${prefix}.${file-extension}
  • file-exetension 為配置內容的資料格式,可以通過配置項 spring.cloud.nacos.config.file-extension 來配置。目前只支援 propertiesyaml 型別。

Nacos叢集

生產環境中,Nacos的配置一般是叢集模式部署,來滿足高可用。

現在我們來想一個問題,前面我們配置的test.yml在一臺機器的nacos上,也就是這個配置在這臺伺服器(192.168.242.129)的nacos內部的資料庫裡儲存著,一旦我們改成叢集部署,這些資料怎麼保證一致性呢?

看一下Nacos叢集架構圖先:

Nacos叢集架構

前面我們操作的都是Nacos單節點,Nacos預設使用嵌入式資料庫實現資料的儲存,所以,如果啟動多個預設配置下的Nacos節點,資料儲存存在一致性問題

為了解決這個問題,Nacos採用了集中儲存方式來支援叢集化部署,目前僅支援MySQL的儲存。

下面我就根據這個叢集架構來部署一套Nacos叢集。該叢集模式下,需要有nginx對Nacos做負載均衡,MySQL做儲存。

Nacos的資料持久化

Nacos預設的內部儲存資料的資料庫是內建的derby資料庫,我們搭建叢集環境的話,為了保證資料的一致性,將不再繼續使用預設的derby,通過修改配置,將資料持久化到MySQL資料庫。

Nacos預設內部儲存derby

Nacos預設Derby資料庫切換到外部MySQL資料庫方法

前面安裝的Nacos所在的伺服器IP地址為 192.168.242.129 ,MySQL所在伺服器在 192.168.242.112

第一步: 將Nacos安裝目錄 conf 下的 nacos-mysql.sql 檔案上傳到MySQL所在的伺服器 192.168.242.112 (以下簡稱112)中;

```sh

上傳sql指令碼檔案到MySQL所在的112服務

scp nacos-mysql.sql [email protected]:/usr/local/sql-scripts/ ```

使用SCP命令上傳sql指令碼到MySQL伺服器

第二步: 在MySQL伺服器上,建立 nacos 資料庫,匯入 nacos-mysql.sql 指令碼;

```sql mysql> create database nacos; Query OK, 1 row affected (0.03 sec)

mysql> use nacos;

mysql> source /usr/local/sql-scripts/nacos-mysql.sql;

mysql> show tables; +----------------------+ | Tables_in_nacos | +----------------------+ | config_info | | config_info_aggr | | config_info_beta | | config_info_tag | | config_tags_relation | | group_capacity | | his_config_info | | permissions | | roles | | tenant_capacity | | tenant_info | | users | +----------------------+ 12 rows in set (0.00 sec) ```

第三步: 修改 conf/application.properties 檔案,將其中MySQL配置的部分修改為如下內容:

```properties

** Config Module Related Configurations **

If use MySQL as datasource:

spring.datasource.platform=mysql

Count of DB:

db.num=1

Connect URL of DB:

db.url.0=jdbc:mysql://192.168.242.112:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=zhangsan db.password.0=Fawai@kuangtu6 ```

通過以上操作,此時仍以單機模式重啟nacos,

sh cd /usr/local/nacos/bin sh shutdown.sh sh startup.sh -m standalone

檢視啟動日誌,啟動日誌輸出檔案: /usr/local/nacos/logs/start.out

nacos啟動成功日誌

此時,訪問nacos後臺,發現之前我們的配置檔案 test.yml 消失了,這是因為我們切換了預設的derby儲存,換成了外部儲存MySQL。

重新建立一個 test.yml

這條記錄在配置的MySQL資料庫中可以查到:

mysql mysql> select * from config_info\G *************************** 1. row *************************** id: 1 data_id: test.yml group_id: DEFAULT_GROUP content: config: info: this is new version! md5: 57609a39c0477b74a7e5315c2acd062b gmt_create: 2022-07-07 07:22:41 gmt_modified: 2022-07-07 07:22:41 src_user: NULL src_ip: 192.168.242.1 app_name: tenant_id: c_desc: NULL c_use: NULL effect: NULL type: yaml c_schema: NULL 1 row in set (0.00 sec)

這樣就實現了Nacos資料持久化到外部儲存MySQL中。

叢集搭建-主機管理

Nacos叢集中各個環節(SLB、Nacos、MySQL)所需要的主機資訊分配如下:

| 序號 | IP地址(簡稱) | 部署服務 | | :--: | :--------------------: | :------: | | 1 | 192.168.242.112(112) | Nginx | | 2 | 192.168.242.112(112) | MySQL | | 3 | 192.168.242.129(129) | Nacos | | 4 | 192.168.242.130(130) | Nacos | | 5 | 192.168.242.131(131) | Nacos |

為了方便,Nginx和MySQL就不做高可用了,Nginx和MySQL部署在 192.168.242.112 上,另外三臺主機部署Nacos。

也可以在一臺伺服器上部署三個Nacos服務,通過埠來區分。

注意: 如果你是在一臺機器上用三個埠的服務來搭建nacos叢集,在修改埠的時候一定要有一定的偏移量(比如三個nacos分別設定成8848/8868/8888),不要設定成8848/8849/8850這樣, 因為Nacos2.0增加了9848,9849埠來進行GRPC通訊,這兩個埠是通過8848+1000以及8848+1001這種偏移量方式計算出來的,如果我們將叢集中的第二個埠設定成8849,那麼8849+1000就和第一個的8848+1001埠重合了!

所以我們在設定埠號的時候注意要避開,不要佔用埠。

我這裡為了模擬實際場景,我整了三臺部署Nacos的虛擬機器,由於在三臺機器上,我可以均以預設的8848埠部署。

叢集配置

在130/131這兩臺虛擬機器中,將 conf/application.properties 中MySQL的部分修改成一致的,然後分別修改三臺機器上的 nacos/cluster.conf 檔案。

先拷貝一份 cluster.conf :

sh cp cluster.conf.example cluster.conf

然後修改 cluster.conf 內容為:

```sh

ip:port

192.168.242.129:8848 192.168.242.130:8848 192.168.242.131:8848 ```

三臺Nacos服務均如此修改。

這樣,一個Nacos叢集就支稜起來了,啟動nacos叢集也相當的簡單,直接執行 bin/starup.sh 就可以了,nacos預設的啟動方式就是叢集方式啟動。

以叢集模式自動Nacos

這時,訪問 http://192.168.242.129:8848/nacos、http://192.168.242.130:8848/nacos、http://192.168.242.131:8848/nacos 均能看到nacos後臺,叢集節點:

叢集節點

Nginx做Nacos叢集的SLB(負載均衡)

訪問Nacos叢集,需要對外提供一個統一的ip地址,使用nginx做叢集的負載均衡。

Nginx安裝

這裡選擇 tengine (阿里版的nginx),安裝步驟:

1. 上傳 tengine-2.3.3.tar.gz 檔案到 /usr/local/warehouse

2. cd /usr/local/warehouse

3. tar -zxvf tengine-2.3.3.tar.gz

4. cd tengine-2.3.3/

5. ./configure --with-stream --prefix=/usr/local/nginx

6. make && make install

安裝過程可能出現的錯及解決辦法

```

錯誤為:./configure: error: the HTTP rewrite module requires the PCRE library.

安裝pcre-devel解決問題

yum -y install pcre-devel

還有可能出現:./configure: error: the HTTP cache module requires md5 functions from OpenSSL library

解決辦法:

yum -y install openssl openssl-devel ```

配置nacos叢集代理

這裡,只需要對nginx做如下配置即可:

```sh

編輯nginx.conf檔案

vi /usr/local/nginx/conf/nginx.conf ```

nacos代理配置:

```nginx stream { upstream nacos { server 192.168.242.129:8848; server 192.168.242.130:8848; server 192.168.242.131:8848; }

 server {
    listen  81;
    proxy_pass nacos;
 }

} ```

啟動nginx:

sh /usr/local/nginx/sbin/nginx

現在,直接訪問 http://192.168.242.112:81/nacos 地址就可以訪問Nacos叢集了:

image-20220707165120035

程式碼中單機Nacos切換成Nacos叢集模式

在前文例子 SpringCloudAlibabaTest 專案中,用到的Nacos均是單機模式下的Nacos,要切換到叢集模式,只需要將IP地址換成Nginx代理的ip地址 192.168.242.112:81 即可。

比如,將 cloud-nacos-config 專案配置修改如下,並啟動專案:

```yml spring: application: name: cloud-nacos-config

cloud: nacos: config: group: DEFAULT_GROUP server-addr: 192.168.242.112:81 config: import: - nacos:test.yml ```

訪問介面:

能夠得到叢集中配置的值!

Nacos客戶端服務註冊原始碼分析

到這裡,我們已經對Nacos的服務註冊發現、配置管理等功能進行了實際操作,也體驗到了它的強大。

我們可以跟著原始碼總結一下其中的一些核心點,最後能夠跟著原始碼來做出核心流程圖,當我們對核心功能的實現瞭解其原始碼後,就可能會借鑑到實際工作專案中,提升我們的程式設計技能和程式設計思想。

那麼這些Nacos有哪些核心功能呢?他們又是怎麼實現的?

前面搭建了真實的微服務專案環境,體驗了Nacos作為服務註冊、服務發現以及配置中心的功能,這些功能裡面包含了一下核心知識點:

  • 服務註冊: Nacos Client 會通過傳送REST請求的方式向 Nacos Server 註冊自己的服務,提供自身的元資料,比如ip地址、埠等資訊。Nacos Server接收到註冊請求後,就會把這些元資料資訊儲存在一個 雙層的記憶體Map 中。
  • 服務發現: 服務消費者(Nacos Client)在呼叫服務提供者的服務時,會發送一個REST請求給Nacos Server,獲取上面註冊的服務清單,並且快取在Nacos Client本地,同時會在Nacos Client本地開啟一個定時任務定時拉取服務端最新的登錄檔資訊更新到本地快取
  • 服務心跳: 在服務註冊後,Nacos Client 會維護一個 定時心跳 來持續通知 Nacos Server ,說明服務一直處於可用狀態,防止被剔除。 預設5s傳送一次心跳。
  • 服務健康檢查: Nacos Server 會開啟一個 定時任務 用來檢查註冊服務例項的健康情況,對於 超過15s沒有收到客戶端心跳的例項會將它的healthy屬性置為false (客戶端服務發現時不會發現),如果某個 例項超過30秒沒有收到心跳,直接剔除該例項 (被剔除的例項如果恢復傳送心跳則會重新註冊)。
  • 服務同步: Nacos Server叢集 之間會互相同步服務例項,用來保證服務資訊的 一致性

Nacos原始碼環境搭建

因為前面我們的Nacos版本選擇的是 2.0.3 ,所以下載原始碼的時候去下載對應版本的原始碼:

原始碼下載

如果直接拉取 http://github.com/alibaba/nacos.git ,下載的原始碼是最新版2.1.0。

下載下來匯入到Idea中,專案結構為:

Nacos原始碼結構

啟動後臺管理 nacos-console 模組的啟動類 Nacos.java ,如果直接啟動報如下錯誤:

原因是 Nacos 2.0 版本使用的是protocol buffer compiler編譯,這裡我們下載下來後使用Maven compile ,重新編譯一下就行了。

啟動的時候還需要加個引數,以單機模式啟動:

sh -Dnacos.standalone=true

如果不加這個引數,預設以叢集方式啟動,這種方式啟動需要修改 application.properties 中關於資料庫MySQL部分的配置(保證叢集資料一致性),否則啟動會報錯 Unable to start embedded Tomcat

看原始碼,只需要單機模式啟動就行了。在Idea中新增啟動引數如下:

配置單機模式自動

配置好之後就可以執行測試,和啟動普通的Spring Boot聚合專案一樣,啟動之後直接訪問:http://localhost:8848/nacos, 這個時候就能看到我們以前看到的對應客戶端頁面了,Nacos原始碼啟動完成。

原始碼啟動Nacos

Nacos客戶端服務註冊原始碼分析

從原始碼級別看Nacos是如何註冊例項的

Nacos原始碼模組中有一個 nacos-client ,直接看其中測試類 NamingTest

```java @Ignore public class NamingTest {

@Test
public void testServiceList() throws Exception {
    // 連線nacos server資訊
    Properties properties = new Properties();
    properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
    properties.put(PropertyKeyConst.USERNAME, "nacos");
    properties.put(PropertyKeyConst.PASSWORD, "nacos");

    //例項資訊封裝,包括基礎資訊和元資料資訊
    Instance instance = new Instance();
    instance.setIp("1.1.1.1");
    instance.setPort(800);
    instance.setWeight(2);
    Map<String, String> map = new HashMap<String, String>();
    map.put("netType", "external");
    map.put("version", "2.0");
    instance.setMetadata(map);

    //通過NacosFactory獲取NamingService
    NamingService namingService = NacosFactory.createNamingService(properties);
    //通過namingService註冊例項
    namingService.registerInstance("nacos.test.1", instance);
}

} ```

這就是 客戶端註冊 的一個測試類,它模仿了一個真實的服務註冊進Nacos的過程,包括 Nacos Server連線屬性封裝例項的建立例項屬性的賦值註冊例項,所以一段測試程式碼包含了服務註冊的核心程式碼。

設定Nacos Server連線屬性

Nacos Server連線資訊,儲存在Properties當中:

java Properties properties = new Properties(); properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848"); properties.put(PropertyKeyConst.USERNAME, "nacos"); properties.put(PropertyKeyConst.PASSWORD, "nacos");

這些資訊包括:

  • SERVER_ADDR :Nacos伺服器地址,屬性的PropertyKeyConst key為serverAddr
  • USERNAME :連線Nacos服務的使用者名稱,PropertyKeyConst key為username,預設值為nacos
  • PASSWORD :連線Nacos服務的密碼,PropertyKeyConst key為passwod,預設值為nacos
服務例項封裝

註冊例項資訊用 Instance 物件承載,註冊的例項資訊又分兩部分:例項基礎資訊元資料

Instance類-例項資訊欄位

基礎資訊欄位說明:

  • instanceId:例項的唯一ID;
  • ip:例項IP,提供給消費者進行通訊的地址;
  • port: 埠,提供給消費者訪問的埠;
  • weight:權重,當前例項的權重,浮點型別(預設1.0D);
  • healthy:健康狀況,預設true;
  • enabled:例項是否準備好接收請求,預設true;
  • ephemeral:例項是否為瞬時的,預設為true;
  • clusterName:例項所屬的叢集名稱;
  • serviceName:例項的服務資訊。

元資料:

java Map<String, String> map = new HashMap<String, String>(); map.put("netType", "external"); map.put("version", "2.0"); instance.setMetadata(map);

元資料 Metadata 封裝在HashMap中,這裡只設置了 netType 和 version 兩個資料,未設定的元資料通過Instance設定的預設值可以get到。

Instance 獲取元資料-心跳時間、心跳超時時間、例項IP被剔除的時間、例項ID生成器的方法:

```java /* * 獲取例項心跳間隙,預設為5s,也就是預設5秒進行一次心跳 * @return 例項心跳間隙 / public long getInstanceHeartBeatInterval() { return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL, Constants.DEFAULT_HEART_BEAT_INTERVAL); }

/**
 * 獲取心跳超時時間,預設為15s,也就是預設15秒收不到心跳,例項將會標記為不健康
 * @return 例項心跳超時時間
 */
public long getInstanceHeartBeatTimeOut() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
            Constants.DEFAULT_HEART_BEAT_TIMEOUT);
}

/**
 * 獲取例項IP被刪除的時間,預設為30s,也就是30秒收不到心跳,例項將會被移除
 * @return 例項IP被刪除的時間間隔
 */
public long getIpDeleteTimeout() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
            Constants.DEFAULT_IP_DELETE_TIMEOUT);
}

/**
 * 例項ID生成器,預設為simple
 * @return 例項ID生成器
 */
public String getInstanceIdGenerator() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
            Constants.DEFAULT_INSTANCE_ID_GENERATOR);
}

```

Instance獲取一些元資料預設值的方法

Nacos提供的元資料key:

```java public class PreservedMetadataKeys {

//心跳超時的key
public static final String HEART_BEAT_TIMEOUT = "preserved.heart.beat.timeout";
//例項IP被刪除的key
public static final String IP_DELETE_TIMEOUT = "preserved.ip.delete.timeout";
//心跳間隙的key
public static final String HEART_BEAT_INTERVAL = "preserved.heart.beat.interval";
//例項ID生成器key
public static final String INSTANCE_ID_GENERATOR = "preserved.instance.id.generator";

} ```

元資料key對應的預設值:

```java package com.alibaba.nacos.api.common;

import java.util.concurrent.TimeUnit;

/* * Constants. * * @author Nacos / public class Constants { //...略

//心跳超時,預設15s
public static final long DEFAULT_HEART_BEAT_TIMEOUT = TimeUnit.SECONDS.toMillis(15);
//ip剔除時間,預設30s未收到心跳則剔除例項
public static final long DEFAULT_IP_DELETE_TIMEOUT = TimeUnit.SECONDS.toMillis(30);
//心跳間隔。預設5s
public static final long DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5);
//例項ID生成器,預設為simple
public static final String DEFAULT_INSTANCE_ID_GENERATOR = "simple";

//...略

} ```

這些都是Nacos預設提供的值,也就是當前例項註冊時會告訴Nacos Server說:我的心跳間隙、心跳超時等對應的值是多少,你按照這個值來判斷我這個例項是否健康。

此時,註冊例項的時候,該封裝什麼引數,我們心裡應該有點數了。

通過NamingService介面進行例項註冊

NamingService 介面是Nacos命名服務對外提供的一個統一介面,其提供的方法豐富:

NamingService介面提供的方法

主要包括如下方法:

  • void registerInstance(...): 註冊服務例項
  • void deregisterInstance(...): 登出服務例項
  • List getAllInstances(...): 獲取服務例項列表
  • List selectInstances(...): 查詢健康服務例項
  • List selectInstances(....List clusters....): 查詢叢集中健康的服務例項
  • Instance selectOneHealthyInstance(...): 使用負載均衡策略選擇一個健康的服務例項
  • void subscribe(...): 服務訂閱
  • void unsubscribe(...): 取消服務訂閱
  • List getSubscribeServices(): 獲取所有訂閱的服務
  • String getServerStatus(): 獲取Nacos服務的狀態
  • void shutDown(): 關閉服務

這些方法均提供了過載方法,應用於不同場景和不同型別例項或服務的篩選。

回到服務註冊測試類中的第3步,通過NamingService介面註冊例項:

java //通過NacosFactory獲取NamingService NamingService namingService = NacosFactory.createNamingService(properties); //通過namingService註冊例項 namingService.registerInstance("nacos.test.1", instance);

再來看一下 NacosFactory 建立namingService的具體實現方法:

java /** * 建立NamingService例項 * @param properties 連線nacos server的屬性 */ public static NamingService createNamingService(Properties properties) throws NacosException { try { //通過反射機制來例項化NamingService Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService"); Constructor constructor = driverImplClass.getConstructor(Properties.class); return (NamingService) constructor.newInstance(properties); } catch (Throwable e) { throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e); } }

通過反射機制來例項化一個NamingService,具體的實現類是 com.alibaba.nacos.client.naming.NacosNamingService

NacosNamingService實現註冊服務例項

註冊程式碼中:

java namingService.registerInstance("nacos.test.1", instance);

前面已經分析到,通過反射呼叫的是 NacosNamingServiceregisterInstance 方法,傳遞了兩個引數:服務名和例項物件。具體方法在 NacosNamingService 類中如下:

java //服務註冊,傳遞引數服務名稱和例項物件 @Override public void registerInstance(String serviceName, Instance instance) throws NacosException { registerInstance(serviceName, Constants.DEFAULT_GROUP, instance); }

該方法完成了對例項物件的分組,即將物件分配到預設分組中 DEFAULT_GROUP

緊接著呼叫的方法 registerInstance(serviceName, Constants.DEFAULT_GROUP, instance) :

java //註冊服務 //引數:服務名稱,例項分組(預設DEFAULT_GROUP),例項物件 @Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { //檢查例項是否合法:通過服務心跳,如果不合法直接丟擲異常 NamingUtils.checkInstanceIsLegal(instance); //通過NamingClientProxy代理來執行服務註冊 clientProxy.registerService(serviceName, groupName, instance); }

這個 registerInstance 方法幹了兩件事:

1: checkInstanceIsLegal(instance) 檢查傳入的例項是否合法,通過檢查心跳時間設定的對不對來判斷,其原始碼如下

java //類NamingUtils工具類下 public static void checkInstanceIsLegal(Instance instance) throws NacosException { //心跳超時時間必須小於心跳間隔時間 //IP剔除的檢查時間必須小於心跳間隔時間 if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval() || instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) { throw new NacosException(NacosException.INVALID_PARAM, "Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'."); } }

2: 通過 NamingClientProxy 代理來執行服務註冊。

進入 clientProxy.registerService(serviceName, groupName, instance) 方法,發現有多個實現類(如下圖),那麼這裡對應的是哪個實現類呢?

我們繼續閱讀NacosNamingService原始碼,找到 clientProxy 屬性,通過構造方法可以知道 NamingClientProxy 這個代理介面的具體實現類是 NamingClientProxyDelegate

NamingClientProxyDelegate中實現例項註冊的方法

從上面分析得知,例項註冊的方法最終由 NamingClientProxyDelegate 中的 registerService(String serviceName, String groupName, Instance instance) 來實現,其方法為:

```java /* * 註冊服務 * @param serviceName 服務名稱 * @param groupName 服務所在組 * @param instance 註冊的例項 / @Override public void registerService(String serviceName, String groupName, Instance instance) throws NacosException { //這一句話幹了兩件事: //1.getExecuteClientProxy(instance) 判斷當前例項是否為瞬時物件,如果是瞬時物件,則返回grpcClientProxy(NamingGrpcClientProxy),否則返回httpClientProxy(NamingHttpClientProxy) //2.registerService(serviceName, groupName, instance) 根據第1步返回的代理型別,執行相應的註冊請求 getExecuteClientProxy(instance).registerService(serviceName, groupName, instance); }

//...

//返回代理型別 private NamingClientProxy getExecuteClientProxy(Instance instance) { //如果是瞬時物件,返回grpc協議的代理,否則返回http協議的代理 return instance.isEphemeral() ? grpcClientProxy : httpClientProxy; } ```

該方法的實現只有一句話:getExecuteClientProxy(instance).registerService(serviceName, groupName, instance); 這句話執行了2個動作:

1. getExecuteClientProxy(instance): 判斷傳入的例項物件是否為瞬時物件,如果是瞬時物件,則返回 grpcClientProxy(NamingGrpcClientProxy) grpc協議的請求代理,否則返回 httpClientProxy(NamingHttpClientProxy) http協議的請求代理;

2. registerService(serviceName, groupName, instance): 根據返回的clientProxy型別執行相應的註冊例項請求。

瞬時物件 就是物件在例項化後還沒有放到持久化儲存中,還在記憶體中的物件。而這裡要註冊的例項預設就是瞬時物件,因此在 Nacos(2.0版本) 中預設就是採用gRPC(Google開發的高效能RPC框架)協議與Nacos服務進行互動。下面我們就看 NamingGrpcClientProxy 中註冊服務的實現方法。

NamingGrpcClientProxy中服務註冊的實現方法

在該類中,實現服務註冊的方法原始碼:

java /** * 服務註冊 * @param serviceName 服務名稱 * @param groupName 服務所在組 * @param instance 註冊的例項物件 */ @Override public void registerService(String serviceName, String groupName, Instance instance) throws NacosException { NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName, instance); //快取當前例項,用於將來恢復 redoService.cacheInstanceForRedo(serviceName, groupName, instance); //基於gRPC進行服務的呼叫 doRegisterService(serviceName, groupName, instance); }

該方法一是要將當前例項快取起來用於恢復,二是執行基於gRPC協議的請求註冊。

快取當前例項的具體實現:

java public void cacheInstanceForRedo(String serviceName, String groupName, Instance instance) { //將Instance例項快取到ConcurrentMap中 //快取例項的key值,格式為 groupName@@serviceName String key = NamingUtils.getGroupedName(serviceName, groupName); //快取例項的value值,就是封裝的instance例項 InstanceRedoData redoData = InstanceRedoData.build(serviceName, groupName, instance); synchronized (registeredInstances) { //registeredInstances是一個 ConcurrentMap<String, InstanceRedoData>,key是NamingUtils.getGroupedName生成的key,value是封裝的例項資訊 registeredInstances.put(key, redoData); } }

快取例項的map的key

基於gRPC協議的請求註冊具體實現:

java //NamingGrpcClientProxy.java public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException { InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName, NamingRemoteConstants.REGISTER_INSTANCE, instance); requestToServer(request, Response.class); redoService.instanceRegistered(serviceName, groupName); }

java //NamingGrpcRedoService.java public void instanceRegistered(String serviceName, String groupName) { String key = NamingUtils.getGroupedName(serviceName, groupName); synchronized (registeredInstances) { InstanceRedoData redoData = registeredInstances.get(key); if (null != redoData) { redoData.setRegistered(true); } } }

綜上分析,Nacos的服務註冊流程:

Nacos服務註冊流程

實際微服務專案中是如何進行服務註冊的?

以前文建立的 cloud_nacos_provider 專案為例,引入了 spring-cloud-starter-alibaba-nacos-discovery 這個包,先來看一下這個jar的結構:

Spring Boot通過讀取 META-INF/spring.factories 裡面的監聽器類來做相應的動作,看一下客戶端的這個 spring.factories 檔案的內容:

properties org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\ com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\ com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\ com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\ com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\ com.alibaba.cloud.nacos.loadbalancer.LoadBalancerNacosAutoConfiguration,\ com.alibaba.cloud.nacos.NacosServiceAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration org.springframework.context.ApplicationListener=\ com.alibaba.cloud.nacos.discovery.logging.NacosLoggingListener

很顯然,Spring Boot自動裝配首先找到 EnableAutoConfiguration 對應的類來進行載入,這裡我們要看服務時怎麼註冊的,自然就能想到註冊服務對應的是 com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration 這個類。

該類自動註冊服務的方法:

java @Bean @ConditionalOnBean({AutoServiceRegistrationProperties.class}) public NacosAutoServiceRegistration nacosAutoServiceRegistration(NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) { //例項化一個NacosAutoServiceRegistration return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration); }

這裡例項化了一個 NacosAutoServiceRegistration 類,它就是例項註冊的核心:

java protected void register() { if (!this.registration.getNacosDiscoveryProperties().isRegisterEnabled()) { log.debug("Registration disabled."); } else { if (this.registration.getPort() < 0) { this.registration.setPort(this.getPort().get()); } //呼叫父類的register super.register(); } }

那麼NacosAutoServiceRegistration的父類是哪個呢?來看一下它的關係圖:

也就是說,NacosAutoServiceRegistration 繼承了 AbstractAutoServiceRegistrationAbstractAutoServiceRegistration 實現了監聽介面 ApplicationListener ,一般情況下,根據經驗,該型別的監聽類,都會實現 onApplicationEvent 這種方法,我們來看原始碼驗證一下:

```java public abstract class AbstractAutoServiceRegistration implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener { //...略

//實現監聽類的方法
public void onApplicationEvent(WebServerInitializedEvent event) {
    this.bind(event);
}

//具體實現
public void bind(WebServerInitializedEvent event) {
    ApplicationContext context = event.getApplicationContext();
    if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
        this.port.compareAndSet(0, event.getWebServer().getPort());
        //啟動
        this.start();
    }
}

public void start() {
    if (!this.isEnabled()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Discovery Lifecycle disabled. Not starting");
        }

    } else {
        if (!this.running.get()) {
            this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
            //呼叫註冊的方法
            this.register();
            if (this.shouldRegisterManagement()) {
                this.registerManagement();
            }

            this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
            this.running.compareAndSet(false, true);
        }

    }
}
//...略

} ```

也就是說,專案啟動的時候就會觸發該類,然後 bind() 呼叫 start() 然後呼叫 register() 方法。在 register() 方法處打個斷點,debug一下:

可以看到,配置檔案中的相關屬性被放到例項資訊中了。沒有配置的,nacos會給預設值,比如分組的預設值就是 DEFAULT_GROUP 等。

那麼Nacos客戶端將什麼資訊傳遞給伺服器,我們就明瞭了,比如nacos server的ip地址、使用者名稱,密碼等,還有例項資訊比如例項的ip、埠、權重等,例項資訊還包括元資料資訊(metaData)。

接著往下看,呼叫的register方法:

java protected void register() { //呼叫NacosServiceRegistry的register方法 this.serviceRegistry.register(this.getRegistration()); }

NacosServiceRegistry 中:

```java public void register(Registration registration) { if (StringUtils.isEmpty(registration.getServiceId())) { log.warn("No service to register for nacos client..."); } else { //例項化NamingService NamingService namingService = this.namingService(); //服務id、組資訊 String serviceId = registration.getServiceId(); String group = this.nacosDiscoveryProperties.getGroup(); //例項資訊封裝 Instance instance = this.getNacosInstanceFromRegistration(registration); try { //註冊例項 namingService.registerInstance(serviceId, group, instance); log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), instance.getPort()}); } catch (Exception var7) { if (this.nacosDiscoveryProperties.isFailFast()) { log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var7}); ReflectionUtils.rethrowRuntimeException(var7); } else { log.warn("Failfast is false. {} register failed...{},", new Object[]{serviceId, registration.toString(), var7}); } }

}

} ```

註冊例項呼叫的是NamingService的實現類 NacosNamingServiceregisterInstance 方法:

java public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { //檢查服務例項設定的心跳時間是否合法 NamingUtils.checkInstanceIsLegal(instance); String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName); if (instance.isEphemeral()) { BeatInfo beatInfo = this.beatReactor.buildBeatInfo(groupedServiceName, instance); this.beatReactor.addBeatInfo(groupedServiceName, beatInfo); } //服務註冊 this.serverProxy.registerService(groupedServiceName, groupName, instance); }

這裡就和前面直接從原始碼看服務的註冊過程連線上了,先檢查例項的心跳時間,然後呼叫gPRC協議的代理進行服務註冊:

最終呼叫傳送請求 /nacos/v1/ns/instance 實現註冊。

Nacos服務註冊流程總結

Nacos服務註冊流程

註冊步驟小結:

  1. 讀取Spring Boot裝載配置檔案 spring.factories,找到啟動類 NacosAutoServiceRegistration

  2. NacosAutoServiceRegistration 繼承 AbstractAutoServiceRegistration,它實現 ApplicationListener 介面;

  3. 實現ApplicationListener介面的 onApplicationEvent 方法,該方法呼叫 bind() ,然後呼叫 start() 方法;

  4. start()方法中呼叫register(),該方法呼叫 NacosServiceRegistry 的register方法;

  5. NacosServiceRegistry的register方法內部呼叫 NacosNamingServiceregisterInstance 方法;

  6. 根據例項的瞬時狀態選擇不同的proxy執行註冊,預設是 gRPC 協議的 NamingGrpcClientProxy 執行註冊;

  7. 完成例項註冊(POST請求 /nacos/v1/ns/instance)。

最後

本文主要內容是針對Spring Cloud Alibaba元件之註冊中心Nacos的介紹,從安裝使用到專案實戰,最後分析了一波客戶端註冊服務的原始碼。

後續將繼續分享Spring Cloud Alibaba的其他功能元件的操作,如 SentinelSeata 等,或許還會繼續分享一些核心功能的原始碼,請關注我吧,方便上車不迷路。

本次導航結束。

先贊後看,養成習慣。

舉手之勞,贊有餘香。

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿