文章摘要
GPT 4
此内容根据文章生成,仅用于文章内容的解释与总结
投诉

如何在SpringCloud中使用Nacos

面试官提问,怎么在你的服务中使用Nacos?

下载安装&使用

使用window直接在Nacos官网下载就可以了,或者使用docker拉取nacos的镜像,我使用的是windows安装。

安装Nacos后在bin目录下使用cmd,执行命令startup.cmd -m standalone,以单机的模式启动nacos。打开浏览器中默认的账号和密码都是nacos。

springcloud引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!--nacos管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

方式一:不使用Nacos做统一配置管理,在application.yml配置nacos的地址

服务中配置nacos的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: userservice # 服务名称
datasource:
url: jdbc:mysql://localhost:3306/tb_user
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
.......

方式二:使用Nacos做统一配置管理,使用bootstrap.yml,引导类文件。

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: userservice
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
config:
file-extension: yaml # 文件后缀名

Nacos负载均衡的原理

在 SpringCloud 中,Nacos 或者 eureka 作为服务注册与发现中心能够提供服务列表,而@LoadBalanced注解则为微服务提供负载均衡。因此讨论负载均衡是基于@LoadBalanced这个注解去讨论的,而服务注册与发现中心作为集群的管理站(提供服务拉取),两者的关系十分密切。

Ribbon

@LoadBalanced注解在旧版本中是基于ribbon实现的,先看看ribbon的实现原理。

LoadBalancerInterceptor类

LoadBalancerInterceptor类是@LoadBalanced注解的底层实现,先看一下源码

LoadBalancerInterceptor源码

可以看到,这个类是实现了一个接口,叫 ClientHttpRequestInterceptor,继续跟进编辑器,可以看到这个接口就是负责拦截请求的。比如使用restTemplate发起一个请求,请求就会被拦截下来。

拦截请求

LoadBalancerInterceptor类实现了ClientHttpRequestInterceptor接口,那么就要重写接口的方法了,这个方法就是intercept()。

重写interrupt方法

接下来分析这个方法是如何实现的。

实现原理

首先基于一个场景:在做订单查询的时候,需要把用户的信息也一并展示出来

请求结果

配置

orderController是正常写,在orderService的地方使用restTemplate发起请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper;

@Autowired
private RestTemplate restTemplate;

public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用RestTemplate发起http请求,查询用户
// 2.1.url路径
String url = "http://userservice/user/" + order.getUserId();
// 2.2.发送http请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}

userService服务名称正常写: name: userservice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server:
port: 8081
spring:
application:
name: userservice # 服务名称
datasource:
url: jdbc:mysql://localhost:3306/tb_user
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
shardingsphere:
sharding:
default-database-strategy:
tables:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS

order的启动类中也注册了restTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
//@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
public class OrderApplication {

public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}

/**
* 创建RestTemplate并注入Spring容器
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}

/* @Bean
public IRule randomRule() {
return new RandomRule();
}*/
}

解释源码

现在我在浏览器发起一个请求:userservice/user/1,此时

1
final URI originalUri = request.getURI();

结果就是 http://userservice/user/1。

1
String serviceName = originalUri.getHost();

得到服务名称userservice。此时进行服务拉取,比如nacos,eureka。拿到服务名称后交给后边去执行:

1
2
return this.loadBalancer.execute(serviceName,
this.requestFactory.createRequest(request, body, execution));

跟进execute方法

跟进execute方法

此处没有方法体,源码中应该存在方法体,方法体中存在execute方法(原因:根据实际代码debug可能会有,或者源码不一样,总之继续跟进),继续跟进,此时来到了RibbonLoadBalancerClient的execute方法

ribbon

继续跟进execute方法,发现以下代码:

1
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);

拿着服务名称(userservice,也就是参数serviceid),生成ILoadBalancer类的实例,源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*
*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.loadbalancer;

import java.util.List;

/**
* Interface that defines the operations for a software loadbalancer. A typical
* loadbalancer minimally need a set of servers to loadbalance for, a method to
* mark a particular server to be out of rotation and a call that will choose a
* server from the existing list of server.
*
* @author stonse
*
*/
public interface ILoadBalancer {

/**
* Initial list of servers.
* This API also serves to add additional ones at a later time
* The same logical server (host:port) could essentially be added multiple times
* (helpful in cases where you want to give more "weightage" perhaps ..)
*
* @param newServers new servers to add
*/
public void addServers(List<Server> newServers);

/**
* Choose a server from load balancer.
*
* @param key An object that the load balancer may use to determine which server to return. null if
* the load balancer does not use this parameter.
* @return server chosen
*/
public Server chooseServer(Object key);

/**
* To be called by the clients of the load balancer to notify that a Server is down
* else, the LB will think its still Alive until the next Ping cycle - potentially
* (assuming that the LB Impl does a ping)
*
* @param server Server to mark as down
*/
public void markServerDown(Server server);

/**
* @deprecated 2016-01-20 This method is deprecated in favor of the
* cleaner {@link #getReachableServers} (equivalent to availableOnly=true)
* and {@link #getAllServers} API (equivalent to availableOnly=false).
*
* Get the current list of servers.
*
* @param availableOnly if true, only live and available servers should be returned
*/
@Deprecated
public List<Server> getServerList(boolean availableOnly);

/**
* @return Only the servers that are up and reachable.
*/
public List<Server> getReachableServers();

/**
* @return All known servers, both reachable and unreachable.
*/
public List<Server> getAllServers();
}

public List getServerList 就拿到的服务列表,服务列表意思就是所有提供userservice的服务(学习的时候使用不同端口号来模拟不同集群节点或者是不同ip的服务器节点),这就是为什么需要nacos或者eureka,因为这俩货可以提供服务列表。

接下来就是负载均衡:

1
Server server = getServer(loadBalancer, hint);

跟进getServer方法

chooseServer?跟进去,发现调用父类

找父类以后发现提供规则Rule

跟进它发现是IRule接口,ctrl+H看一下实现类:

可以发现,IRule接口的实现类决定了负载均衡的实现规则。

所以决定负载均衡的实现有IRule接口定义的规则。

IRule

irule

实现流程图

实现流程图

Nacos根据集群实现负载均衡

如果不设置负载均衡新规则则会采用默认的轮询方式选择服务,进而导致不优先访问本地集群的服务但是跨集群访问服务的情况。

所以需要消费者的yml文件中配置负载均衡的规则。NacosRule规则为集群优先规则。

Nacos服务实例的权重设置

采用NacosRule的负载规则以后,服务会优先选择同集群的提供者提供服务。但是访问的实例是随机的。

由于在实际部署中,希望性能好的设备能够承担更多的请求来提高服务的速度,因此需要控制访问频率来达到控制性能高的设备接受请求越多的效果。

Spring Cloud LoadBalancer

随着springcloud的版本升级,负载均衡的底层实现由ribbon变成了Spring Cloud LoadBalancer,实现原理放一篇csdn的博客,原理和上边基本相同,不过博客将具体使用的类讲得比较清楚,作为最新版本实现所以注重一下Spring Cloud LoadBalancer的总结。

Spring Cloud:负载均衡 - Spring Cloud Loadbalancer原理_spring-cloud-starter-loadbalancer-CSDN博客

ReactorLoadBalancer< ServiceInstance >

可以通过实现ReactorLoadBalancer<ServiceInstance>接口来创建自定义的负载均衡器。然后,你需要在Spring容器中以Bean的形式注册这个自定义的负载均衡器,并确保它在适当的时候被用于服务调用。不过,这种方法需要更深入地了解Spring Cloud LoadBalancer的工作原理,并且可能需要通过编程方式在服务消费者中显式地引用这个负载均衡器。

实际开发的算法比较困难,这里提一嘴(AIGC)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.cloud.client.ServiceInstance;  
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.Request;
import org.springframework.cloud.loadbalancer.core.Response;
import reactor.core.publisher.Mono;

public class MyCustomLoadBalancer implements ReactorLoadBalancer<ServiceInstance> {

@Override
public Mono<Response<ServiceInstance>> choose(Request<ServiceInstance> request) {
// 这里实现你的负载均衡逻辑
// 例如,我们可以简单地随机选择一个服务实例
List<ServiceInstance> instances = request.getServerList();
if (instances.isEmpty()) {
return Mono.empty();
}

// 随机选择一个实例(这里仅为示例,实际中可能需要更复杂的逻辑)
ServiceInstance instance = instances.get(new Random().nextInt(instances.size()));
return Mono.just(new Response<>(instance));
}
}

Nacos VS Eureka

nacos和eureka都作为注册中心,服务提供者将服务列表注册到注册中心,而服务消费者定时将服务拉取(pull)到本地缓存。

然而,nacos会将服务的提供者划分成「临时实例」和「非临时实例」。默认情况下,所有实例都是临时实例。

临时实例中,nacos采用心跳检测,临时实例每隔一段时间会向nacos发送请求检查服务是否宕机,如果宕机,nacos会直接把服务剔除。

非临时实例,nacos主动向服务发送请求,检测服务是否宕机。如果宕机,nacos是不会剔除服务的,仅仅只是将服务标记为不健康。

在消费者的地方,如果服务宕机了,eureka采用的是pull,来更新消费者本地缓存,变相的通知消费者,有服务宕机不能再使用了,而nacos不同,nacos会主动push消息给消费者更新本地缓存,通知可能有服务宕机。因此eureka的更新效率低于nacos。

Nacos还支持统一配置管理,eureka不支持,因此基于springcloud的开发用的都是nacos,而eureka都不做选择。

此外,nacos支持AP,CP模式,默认情况下,nacos集群默认采用AP模式,存在临时实例时采用CP模式,而eureka仅仅支持AP模式。

AP、CP模式将在Seata中介绍具体的内容,包括base理论和CAP定理等。

总结

负载均衡

ribbon

LoadBalancerInterceptor的execute方法 => LoadBalancerClient接口的execute方法 =>RibbonLoadBalancerClient类的execute方法 => 方法中ILoadBalancer的getServerList可以拉取服务列表,ILoadBalancer的chooseServer方法跟进后,Server类中的方法调用IRule的接口,以及实现类去实现负载原理。

NacosRule

基于ribbon的实现。在集群中可以修改除了轮询或者随机的方式。

Spring Cloud LoadBalancer:

Spring Cloud LoadBalancer同样提供了LoadBalancerClient接口,但其实现类(如BlockingLoadBalancerClient、ReactorLoadBalancer<ServiceInstance>等)是基于Spring Cloud LoadBalancer的新实现。

和ribbon的用的类和接口差不多,LoadBalancerClient,不过底层重写负载均衡的策略改为了ReactorLoadBalancer<ServiceInstance>等。

Nacos VS Eureka

nacos对实例划分临时实例和非临时实例,且对应不同的检查机制。

nacos对消费者提供push操作更新消费者本地缓存的服务列表。

nacos对CAP模式有AP,CP的选择。

nacos支持统一配置管理。