前言
前面我们使用了 Eureka 实现了服务注册和服务发现,以及负载均衡,但是由于2018年6月 Eureka 2.0 版本宣布闭源,也就意味着 Eureka 不会再更新了,虽然现在 Eureka 1.0 版本还有很多企业在使用,但是终究有一天会跟不上技术的变化。而同年的7月份,阿里宣布开源了 Nacos,并快速成为国内最受关注的开源产品。作为 Eureka 的替代,Nacos 已经成为了国内开发者的首选。
这篇文章我们来学习如何使用 Nacos 实现服务注册/服务发现和负载均衡。
Nacos 安装
Windows环境
官方推荐我们使用更为稳定的2.2.3版本,所以我们就下载2.2.3版本:https://github.com/alibaba/nacos/releases/tag/2.2.3
解压完成之后就会形成下面的文件结构:
其中bin目录下面是对应的 Windows 和 Linux 环境下的启动和停止脚本:
conf 目录下面则是 nacos 的配置文件:
target 目录下存放的是 nacos 应用的 jar 包:
启动nacos
修改配置文件
因为我们学习阶段都是在一台电脑上使用的,而 nacos 的默认启动方式是集群,如果我们没有配置集群就启动 nacos 的话就会出现报错:
当我们双击 startup.cmd 的时候就会出现下面的界面:
通过查看生成的 logs 文件夹下面的 nacos.log 日志文件我们大概可以看到出现了什么问题:
这就问题就是 nacos 默认的启动模式是集群,而我们却没有配置集群,所以我们在单机模式下启动 nacos 就需要修改配置文件中的启动模式为单机模式:
右键 startup.cmd 选择以记事本的形式打开,然后将 mode 修改为 standalone:
保存之后再启动 startup.cmd:
出现这样的提示就表示启动成功。
注意:nacos 默认使用的端口是 8848,如果想要切换使用的端口,可以再conf/application.properties 配置文件中修改使用的端口号。
然后我们通过 IP:端口号/nacos 的方式就可以访问到 nacos 的主页。
上面是我们在 Windows 环境下安装并启动 nacos,那么接下来看看在 Linux 环境下如何安装并启动 nacos。
Linux
我们将下载之后的压缩文件直接拖入到 xshell 中:
上传完成压缩包之后,我们就需要对这个压缩包进行解压缩操作,加压缩操作需要使用到 unzip 命令,如果没有下载该命令的话就需要先下载 unzip命令:apt-get install unzip
。下载完成 unzip 命令之后,使用 unzip xxx.zip
来进行解压缩:
解压缩之后形成的目录结构:
跟 Windows 一样,启动文件都是在 bin 目录下,但是 Linux 的启动和关闭文件是以 .sh 结尾的文件:
还是一样,我们在 Linux 环境下还是以单机模式的形式启动的 nacos,所以还是需要启动模式为单机模式:
vim startup.sh进入文件,然后使用 i 进入 Insert 插入模式,修改 mode 为 standalone,然后使用 ESC 退出 Insert 模式,输入冒号:,再输入 wq 保存并退出。
然后在 Ubuntu 环境下使用 bash startup.sh -m standalone
,如果我们不想修改配置文件,而是每一次都手动指定启动的模式也可以。
启动之后,我们同样是使用 IP:8848/nacos 看服务是否启动成功:
Nacos 的使用
Nacos 的使用和 Eureka 的使用区别不大,主要的差异在于:
- Eureka 需要自己搭建一个服务,Nacos 不用自己搭建服务,组件已经准备好了,我们只需要启动就好了,也就是前面使用的 startup.cmd/startup.sh 启动项。
- 对应依赖和配置不同
参考操作:https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-discovery
首先我们需要在父项目的 pom 文件中引入 Spring Cloud Alibaba 的依赖:
<properties>
<spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version>
</properties>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Spring Cloud 和 Spring Cloud Alibaba 的版本对应关系:
然后在子项目中引入 nacos 依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
引入负载均衡依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
配置Nacos地址
我们这里直接配置服务器 Nacos 的地址:
spring:
application:
name: product-service
cloud:
nacos:
discovery:
server-addr: 服务器IP:8848
配置负载均衡:
@Configuration
public class BeanConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
远程调用:
public OrderInfo selectOrderById(Integer id) {
OrderInfo orderInfo = orderMapper.selectOrderById(id);
String url = "http://product-service/product/" + orderInfo.getProductId();
ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
启动服务:
观察 nacos 管理页面,可以发现 order-service 和 product-service 都注册在 nacos 上了:
测试订单接口:
负载均衡也跟 Eureka 是一样的,先启动多个实例,然后发送多个请求:
Nacos 负载均衡
前面我们使用的是 Spring Cloud LoadBalancer,而 Nacos 也可以实现负载均衡。并且 Nacos 的负载均衡相较于 Spring Cloud LoadBalancer 来说,Nacos 支持多种负载均衡策略,包括权重,同机房,同地域,同环境等。
服务线下
但某一个节点上的接口性能较差时,我们可以第一时间对该节点进行下线:
我们将其中一个实例进行下线处理:
然后再次发送请求,看看这个下线的机器是否还能够分配到请求:
可以看到 9091 端口号启动的实例确实实现了下线的操作。
配置权重
在前面的 Spring Cloud LoadBalancer 中存在的负载均衡策略中,实现了平均的负载均衡策略和随机的负载均衡策略,而 Nacos 的负载均衡还可以实现为每个实例分配不同的权重,从而实现能力越高的实例处理更多的请求,能力较低的实例处理较少的请求。
配置完成权重之后,我们再次发送请求,看看这个配置的权重是否生效了:
可以看到我们虽然为 9091 实例配置了权重不为1,但是他处理的请求的数量还是和其他实例的数量相同,这是为什么呢?
这是因为 Spring Cloud Balance 组件自身有负载均衡配置方式,所以不支持 Nacos 的权重属性配置。我们需要开启 Nacos 的负载均衡策略,让权重配置生效。
我们在服务使用方的配置文件中添加下面的配置项:
spring:
cloud:
loadbalancer:
nacos:
enabled: true
添加下面配置项之后,我们重启服务调用方,然后再次发送请求看看权重配置是否生效:
可以看到 9091 这个服务处理的请求明显少于另外两个实例,说明我们的权重配置生效了。但是需要注意的是,这个权重配置是整体流量生效,局部流量不是严格按照设置的比例进行分配的。
如果我们在修改权重的时候出现了类似下面这样的错误:
caused:errCode: 500,errMsg: do metadata operation failed;com.alibaba.nacos.consistency.exception.ConsistencyException:
The Raft Group [naming_instance_metadata] did not find the Leader node;
caused: The Raft Group [naming_instance_metadata] did not find the Leader node;
原因是:Nacos 采用 raft 算法来计算 Leader,并且会记录前一次启动的集群地址。当服务器 IP 改变时,会导致 raft 记录的集群地址失效,进而导致选 Leader 出现问题。(网络环境发生变化时,IP 地址也会发生变化)
处理方法:删除 Nacos 根目录下面的 data 文件夹下面的 protocol 文件夹即可
同集群优先访问
Nacos 把同一个机房内的实例,划分为一个集群,同集群优先访问,在一定程度上也可以理解为同机房优先访问。
在微服务架构中,一个服务通常有多个实例共同提供服务,这些实例可以部署在不同的机器上,也可以分布在不同的机房。而微服务访问的时候,会尽量优先访问同一机房的服务,当本机房的实例不可用时,才会访问其他机房的实例。
那么在 Nacos 中我们如何做到配置集群呢,也很简单,我们只需要在实例的配置文件中指定该实例所属的集群就可以了:
spring:
cloud:
nacos:
discovery:
server-addr: 110.41.51.65:10020
cluster-name: SH #集群名字
当我们配置完成集群名称之后,再在 nacos 中心就可以观察到当前服务有哪些集群以及集群中有哪些实例:
然后我们配置服务使用方,也就是 order-service 的集群为 HB,然后配置负载均衡,启动之后调用服务,看看 nacos 是否实现了同集群(同机房)优先访问。
可以看到 nacos 确实实现了同集群优先访问,然后我们将 HB 集群的两个 product-service 给下线,看看会是怎么样的?
可以看到,当同集群的服务都不可用的时候,才会访问其他集群的实例。
Nacos 健康检查
Nacos 作为注册中心,需要感知到服务的健康状态,才能为服务调用方提供良好的服务,那么 Nacos 是如何知道服务的健康状态呢?
Nacos 有两种健康检查机制:
客户端主动上报机制:
- 客户端通过心跳上报方式告知服务端(Nacos注册中心)健康状态,默认心跳时间间隔5秒
- nacos 会在超过15秒为收到心跳包后将实例设置为不健康状态,超过30秒将实例删除
服务端反向探测机制:
- nacos 主动探知客户端健康状态,默认间隔为20秒
- 健康状态检查失败后实例会被标记为不健康,不会被立即删除
Nacos 中的健康检查机制不能主动设置,健康检查机制是和 Nacos 的服务实例类型强相关的。
那么 Nacos 的服务实例的类型有哪些呢?
Nacos 服务实例类型
Nacos 的服务实例分为临时节点和非临时节点:
- 临时节点:如果实例宕机超过一定时间,会从服务列表剔除这个实例,默认类型就是临时节点
- 非临时节点:如果实例宕机,不会从服务列表剔除,也可以叫永久实例
Nacos 对临时实例,采取的是客户端主动上报机制,对于非临时实例,采取服务器端反向探测机制。
服务实例默认是临时实例,那么如何设置实例为永久实例呢?
我们只需要在需要配置为永久实例的配置文件中添加下面的配置就可以了:
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为⾮临时实例
我们呢停止服务然后观察一下非临时实例的健康状态:
可以看到就算永久实例下线了,Nacos 注册中心也不会删除该实例的相关信息。
如果我们修改完成实例的状态之后,重启 Nacos 的时候报错的话,就需要删除 nacos 目录下面的 /data/protocol/raft 信息,那么为什么会报错呢?
Nacos 会记录每个服务实例的 IP 和端口号,当发现 IP 和端口号都没有发生变化时,nacos 不允许服务实例类型发生变化,比如从临时实例变为非临时实例,或者从非临时实例变成临时实例。
Nacos 环境隔离
在企业开发中,项目分为开发环境、测试环境和开发环境,而在不同的环境中,服务调用者也应该访问的是与调用者相同环境的实例。
那么在 Nacos 中是如何实现环境隔离的呢?
默认情况下,所有的服务都在同一个 namespace 中,也就是 public。
我们点击左侧的命名空间,然后点击新建命名空间:
在创建完成命名空间之后,我们就可以在实例的配置文件中通过 spring.cloud.nacos.discovery.namespace
来配置实例的命名空间。
这个值就是命名空间的 ID:
我们将 order-service 的命名空间设置为 dev,然后启动服务,测试看看两个不同的命名空间的服务是否能够访问:
这样就实现了空间隔离。然后我们也将 product-service 的命名空间设置为 dev 看看 order-service 是否可以调用 product-service:
Nacos 配置中心
除了注册中心和负载均衡之外,Nacos 还是一个配置中心,具备配置管理的功能。
为什么会存在配置中心呢,当我们项目刚刚上线的时候,可能对应的是测试环境,那么数据库、redis等其他的账号密码等和生产环境的是不一样的,那么就需要将实例中的配置文件中的相关配置都进行修改,那么这就比较麻烦了。
配置中心就是对这些配置项进行统一管理,通过配置中心,可以集中查看,修改和删除配置,无需再逐个修改配置文件。
在服务启动的时候,会从配置中心读取配置项的内容,进行初始化。当配置修改时,会通知微服务,实现配置的更新加载。
配置管理的命名空间和服务列表的命名空间是隔离的,两个是分别设置的,默认是 public,也就是服务管理命名空间 ≠ 配置管理的命名空间。
nacos 的配置当前只支持 YAML 和 properties 格式。
然后我们如何在项目中获取配置中心中的配置呢?
首先我们需要添加 Nacos Config 依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- SpringCloud 2020.*之后版本需要引⼊bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
配置 bootstrap.properties,我们这里还是使用 YAML 的格式:
spring:
application:
name: product-service
cloud:
nacos:
config:
server-addr: x.x.x.x:8848
这里 spring.application.name 需要和我们 nacos 中配置的 Data ID 保持一致:
编写测试代码:
package org.example.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/config")
@RestController
public class NacosController {
@Value("${nacos.tmp.num}")
private Integer nacosNum;
@RequestMapping("/get")
public Integer get() {
return nacosNum;
}
}
这样我们就可以在项目中获取到 nacos 配置中心中的配置项,然后我们修改一下配置项的值,看看项目中是否可以得到修改之后的配置项的值:
访问之后发现获取到的还是之前的值,这是为什么呢,要想及时的获取到更新之后的值,需要在类上添加 @RefreshScope
注解:
package org.example.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/config")
@RefreshScope
@RestController
public class NacosController {
@Value("${nacos.tmp.num}")
private Integer nacosNum;
@RequestMapping("/get")
public Integer get() {
return nacosNum;
}
}
修改值:
这样就可以不用重启服务,就可以及时的获取到更改之后的配置项了。
Nacos 配置管理的命名空间和服务列表的命名空间是分别设置的,默认是 public。
Nacos 命名空间配置是在 bootstrap.properties 中进行配置的。spring.cloud.nacos.config.namespace=ffb50796-29ca-4f91-bfd0-f51f262fffee
这个值就是我们配置对应的命名空间的 ID:
我们在 namespace 为 dev 的命名空间下还是创建一个配置:
然后启动程序看看,这个获取的配置的值是 public 中的值还是 dev 命名空间下的值:
Data ID
在 Nacos 中,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-extension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型,默认为 properties。
当微服务启动的时候,会从 Nacos 读取多个配置文件:
- p r e f i x − {prefix}- prefix−{spring.profiles.active}.${file-extension},如 product-service-dev.properties
- p r e f i x . {prefix}. prefix.{file-extension},如product-service.properties
- ${prefix},如product-service
我们在 bootstarp.yml 文件中添加 spring.properties.active
值:
spring:
profiles:
active: dev
然后启动项目观察日志:
可以看到,依次加载了:product-service-dev.properties、product-service.properties和product-service,那么如果这三个文件中存在相同的配置项,那么我们读取到的值到底是哪个文件中的值呢?
可以看到这三个配置问价的优先级是:product-service-dev.properties > product-service.properties > product-service。