滴滴滴,上车了!
本次旅途,你将获取到如下知识:
注册中心是微服务架构中不可缺少的一环,用作 服务注册发现 。
为何使用注册中心?
假设有这样一个场景,我们乘坐公车需要确定自己的座位在哪里,才能入座,否则有可能那是别人的座位,等别人来做的时候把我撵起来那就尴尬了。正常的情况应该是有个售票员给我发个带有座位编号的票,然后我去对号入座,这样就可以快速找到自己的座位且没有被撵走的风险。
这里,售票员 其实就是 提供注册和发现 的一个中间联络员。座位的信息已经登记到售票员那里了,乘客来乘车,只需要找售票员就能快速找到自己的座位。
找车上座位模型
微服务架构体系中,各个微服务组件相互独立,但最终还要组合为一个整体作为一个软件系统服务于最终客户,在整个大系统内部,各个服务之间需要彼此通讯,彼此调用方法。
微服务架构内部发起通讯调用方法的一方就是 服务消费者,提供远程方法调用的服务称为 服务提供者。
而为了提高系统性能,一般会提供多个服务器作为 服务提供者,此时 服务消费者 找到 服务提供者 的过程,就类似于乘客上车找座位的过程。
Nacos微服务注册中心
因此,在微服务架构中都会引入 注册中心 ,这样就能使服务的消费者快速的找到它需要的服务提供者。注册中心实现了服务提供和服务消费的快速撮合功能。
Nacos 提供了一组简单易用的特性集,可以快速实现动态服务发现。
除了服务发现,在服务繁多的微服务架构体系中,配置 的集中化管理也非常重要,因为服务数量有很多,每次修改一个配置有可能需要跟进多个服务对其进行同步修改,然后再重启这些项目,那就麻烦了。配置中心 就用来完成配置的统一管理,修改一处,实时生效。
可以结合我之前的一篇介绍Apollo配置中心的文章一起食用:分布式配置中心之Apollo实战
Nacos 不仅能做微服务的注册中心,同时它还支持做配置中心。
Nacos 是 Spring Cloud Alibaba 的组件之一,支持服务的注册发现,支持分布式系统的外部化配置和配置的自动刷新。
现在该把 Nacos 环境支棱起来了。
本文 Nacos 安装环境:
下载当前官方的推荐版本:2.0.3
,下载地址:
官方稳定版本下载地址
将下载下来的 nacos-server-2.0.3.tar.gz
上传到服务器中(服务器IP地址:192.168.242.129,记住这个IP地址,后面将和MySQL和nginx所在服务器区别),解压:
# 解压
tar -zxvf nacos-server-2.0.3.tar.gz
# 启动
cd nacos/bin
sh startup.sh -m standalone
启动命令中 standalone
代表着单机模式运行,非集群模式。
可先在服务器端看nacos服务是否启动成功:
[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 源代码仓库:https://github.com/xblzer/SpringCloudAlibabaTest
因为Nacos既可以作为服务发现注册中心,也可以是配置中心,所以我这里也分两部分进行操作。
demo结构用 Maven父子工程
,Maven父工程导入 Spring Boot 、Spring Cloud 、Spring Cloud Alibaba 基础依赖,各个子工程作为module依赖父工程。
IDE:IntelliJ IDEA
创建一个普通的Maven工程,并删除IDE自动生成的文件夹和文件,只保留 pom.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<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>
</project>
父工程创建OK。
本文所选版本是:
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 依赖,这一点需要注意一下。
和创建普通Spring Boot项目一样,创建完成后,删除无用的文件,保留src和pom.xml。
因为是子工程,在pom中添加其父工程依赖:
<parent>
<groupId>com.xblzer</groupId>
<artifactId>SpringCloudAlibabaTest</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/>
</parent>
依赖中引入 spring-cloud-starter-alibaba-nacos-discovery
:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
然后在父工程的pom中添加子模块:
<modules>
<module>cloud-nacos-provider</module>
</modules>
Nacos服务提供者配置文件 application.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
就行了:
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
:
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 依赖:
<!-- 2021.0.1版本移除了netflix ribbon 使用LoadBalancer 必须引入LoadBalancer的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
引入依赖后,我们只需要在注入 RestTemplate 的时候加上 @LoadBalanced 注解即可。
RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,它提供了多种边界访问远程 Http 服务的方法,能够大大提高客户端的编写效率。
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
编写调用接口
在接口中调用服务提供者的接口 /test-port
:
@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 之间切换,可见实现了负载均衡。
Nacos不仅仅可以作为注册中心来使用,同时它支持作为配置中心,我们来看一下怎么用。
同样创建module,引入nacos config依赖:
<!-- 引入Nacos config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
将该子模块添加到父工程:
<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文件内容:
# 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配置的内容起到两个作用:
现在推荐使用 spring.config.import
方式引入配置,以上述 bootstrap.yml
的配置为例,spring.config.import
引入方式如下:
配置文件 application.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
来配置。file-exetension
为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension
来配置。目前只支持 properties
和 yaml
类型。生产环境中,Nacos的配置一般是集群模式部署,来满足高可用。
现在我们来想一个问题,前面我们配置的test.yml
在一台机器的nacos上,也就是这个配置在这台服务器(192.168.242.129)的nacos内部的数据库里存储着,一旦我们改成集群部署,这些数据怎么保证一致性呢?
看一下Nacos集群架构图先:
Nacos集群架构
前面我们操作的都是Nacos单节点,Nacos默认使用嵌入式数据库实现数据的存储,所以,如果启动多个默认配置下的Nacos节点,数据储存存在一致性问题。
为了解决这个问题,Nacos采用了集中存储方式来支持集群化部署,目前仅支持MySQL的存储。
下面我就根据这个集群架构来部署一套Nacos集群。该集群模式下,需要有nginx对Nacos做负载均衡,MySQL做存储。
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)中;
# 上传sql脚本文件到MySQL所在的112服务
scp nacos-mysql.sql root@192.168.242.112:/usr/local/sql-scripts/
使用SCP命令上传sql脚本到MySQL服务器
第二步: 在MySQL服务器上,创建 nacos
数据库,导入 nacos-mysql.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配置的部分修改为如下内容:
#*************** 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,
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> 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
:
cp cluster.conf.example cluster.conf
然后修改 cluster.conf
内容为:
# 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后台,集群节点:
集群节点
访问Nacos集群,需要对外提供一个统一的ip地址,使用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
这里,只需要对nginx做如下配置即可:
# 编辑nginx.conf文件
vi /usr/local/nginx/conf/nginx.conf
nacos代理配置:
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:
/usr/local/nginx/sbin/nginx
现在,直接访问 http://192.168.242.112:81/nacos 地址就可以访问Nacos集群了:
image-20220707165120035
在前文例子 SpringCloudAlibabaTest
项目中,用到的Nacos均是单机模式下的Nacos,要切换到集群模式,只需要将IP地址换成Nginx代理的ip地址 192.168.242.112:81 即可。
比如,将 cloud-nacos-config
项目配置修改如下,并启动项目:
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版本选择的是 2.0.3 ,所以下载源码的时候去下载对应版本的源码:
源码下载
如果直接拉取 https://github.com/alibaba/nacos.git ,下载的源码是最新版2.1.0。
下载下来导入到Idea中,项目结构为:
Nacos源码结构
启动后台管理 nacos-console 模块的启动类 Nacos.java ,如果直接启动报如下错误:
原因是 Nacos 2.0 版本使用的是protocol buffer compiler编译,这里我们下载下来后使用Maven compile ,重新编译一下就行了。
启动的时候还需要加个参数,以单机模式启动:
-Dnacos.standalone=true
如果不加这个参数,默认以集群方式启动,这种方式启动需要修改 application.properties
中关于数据库MySQL部分的配置(保证集群数据一致性),否则启动会报错 Unable to start embedded Tomcat 。
看源码,只需要单机模式启动就行了。在Idea中添加启动参数如下:
配置单机模式自动
配置好之后就可以运行测试,和启动普通的Spring Boot聚合项目一样,启动之后直接访问:http://localhost:8848/nacos,这个时候就能看到我们以前看到的对应客户端页面了,Nacos源码启动完成。
源码启动Nacos
Nacos源码模块中有一个 nacos-client ,直接看其中测试类 NamingTest :
@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连接信息,存储在Properties当中:
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类-实例信息字段
基础信息字段说明:
元数据:
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生成器的方法:
/**
* 获取实例心跳间隙,默认为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:
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对应的默认值:
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 接口是Nacos命名服务对外提供的一个统一接口,其提供的方法丰富:
NamingService接口提供的方法
主要包括如下方法:
这些方法均提供了重载方法,应用于不同场景和不同类型实例或服务的筛选。
回到服务注册测试类中的第3步,通过NamingService接口注册实例:
//通过NacosFactory获取NamingService
NamingService namingService = NacosFactory.createNamingService(properties);
//通过namingService注册实例
namingService.registerInstance("nacos.test.1", instance);
再来看一下 NacosFactory 创建namingService的具体实现方法:
/**
* 创建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实现注册服务实例
注册代码中:
namingService.registerInstance("nacos.test.1", instance);
前面已经分析到,通过反射调用的是 NacosNamingService 的 registerInstance 方法,传递了两个参数:服务名和实例对象。具体方法在 NacosNamingService 类中如下:
//服务注册,传递参数服务名称和实例对象
@Override
public void registerInstance(String serviceName, Instance instance) throws NacosException {
registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
}
该方法完成了对实例对象的分组,即将对象分配到默认分组中 DEFAULT_GROUP 。
紧接着调用的方法 registerInstance(serviceName, Constants.DEFAULT_GROUP, instance) :
//注册服务
//参数:服务名称,实例分组(默认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) 检查传入的实例是否合法,通过检查心跳时间设置的对不对来判断,其源码如下
//类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) 来实现,其方法为:
/**
* 注册服务
* @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中服务注册的实现方法
在该类中,实现服务注册的方法源码:
/**
* 服务注册
* @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协议的请求注册。
缓存当前实例的具体实现:
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协议的请求注册具体实现:
//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);
}
//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 文件的内容:
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 这个类。
该类自动注册服务的方法:
@Bean
@ConditionalOnBean({AutoServiceRegistrationProperties.class})
public NacosAutoServiceRegistration nacosAutoServiceRegistration(NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) {
//实例化一个NacosAutoServiceRegistration
return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration);
}
这里实例化了一个 NacosAutoServiceRegistration 类,它就是实例注册的核心:
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 继承了 AbstractAutoServiceRegistration ,AbstractAutoServiceRegistration 实现了监听接口 ApplicationListener ,一般情况下,根据经验,该类型的监听类,都会实现 onApplicationEvent 这种方法,我们来看源码验证一下:
public abstract class AbstractAutoServiceRegistration<R extends Registration> implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> {
//...略
//实现监听类的方法
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方法:
protected void register() {
//调用NacosServiceRegistry的register方法
this.serviceRegistry.register(this.getRegistration());
}
在 NacosServiceRegistry 中:
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的实现类 NacosNamingService 中 registerInstance 方法:
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/v1/ns/instance
)。本文主要内容是针对Spring Cloud Alibaba组件之注册中心Nacos的介绍,从安装使用到项目实战,最后分析了一波客户端注册服务的源码。
后续将继续分享Spring Cloud Alibaba的其他功能组件的操作,如 Sentinel 、 Seata 等,或许还会继续分享一些核心功能的源码
本次导航结束。