注册中心和负载均衡
目录
- 注册中心和负载均衡
- 一、服务远程调用
- 1. RestTemplate
- 2. 服务调用关系
- 3. 远程调用的问题
- 二、注册中心
- 1. Eureka注册中心
- 1.1 搭建Eureka注册中心
- 1.2 服务注册
- 1.3 服务拉取
- 1.4 小结
- 2. nacos注册中心
- 2.1Nacos搭建
- 2.2 服务注册
- 2.3 服务拉取
- 2.4 服务分级存储模型
- 2.5 权重配置
- 2.6 环境隔离
- 2.7 临时实例和非临时实例
- 2.8 nacos配置管理
- 2.9 nacos集群搭建
- 三、Ribbon负载均衡
- 1. 负载均衡策略
- 2. 饥饿加载
一、服务远程调用
1. RestTemplate
假设有两个服务、两个数据库
查询订单服务:
@GetMapping("/order/{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
订单数据库中的订单表
查询用户服务
@GetMapping("/user/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
用户数据库中的用户表
启动服务,查询订单101:
http://localhost:8080/order/101,返回:
{
"id":101,
"price":699900,
"name":"Apple 苹果 iPhone 12 ",
"num":1,
"userId":1,
"user":null
}
查询用户 1:
http://localhost:8081/user/1,返回:
{
"id":1,
"username":"柳岩",
"address":"湖南省衡阳市"
}
显然,我们需要在查询订单的时候,根据用户id再去调用查询用户的服务。
RestTemplate可以帮助我们发起Http请求,调用其他服务的REST接口:
-
配置
@SpringBootConfiguration public class CustomConfiguration { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
-
使用
@Autowired注入后
public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2.调用REST接口查询数据 User user = restTemplate.getForObject("http://localhost:8081/user/" + order.getUserId(), User.class); // 3.封装数据 order.setUser(user); // 4.返回 return order; }
再次查询订单101:
{
"id":101,
"price":699900,
"name":"Apple 苹果 iPhone 12 ",
"num":1,
"userId":1,
"user":{
"id":1,
"username":"柳岩",
"address":"湖南省衡阳市"
}
}
2. 服务调用关系
- 服务提供者:暴露接口给其他微服务调用
- 服务消费者:调用其他微服务提供的接口
在刚刚的例子中,查询用户的服务可以称为服务提供者,查询订单的服务需要去调用它,那么查询订单的服务可以称为服务消费者。
假如一个服务B调用了服务A,服务C又调用了服务B,那么服务B是什么角色呢?
由此可见,服务提供者和消费者是相对的。
3. 远程调用的问题
刚刚的例子中有这样一行代码:
User user = restTemplate.getForObject("http://localhost:8081/user/" + order.getUserId(), User.class);
环境变更后(ip),怎么办?难道去改代码重新编译?还有,如果只能调用8081,集群意义何在?
问题1:消费者如何获取提供者的地址信息?
问题2:如果有多个提供者,消费者如何选择?
问题3:消费者如何得知提供者的健康状态?
注册中心解决了这些问题。
二、注册中心
注册中心是用来记录和管理服务的。
注册中心解决了之前服务调用的三个问题:
- 首先服务提供者需要在注册中心注册服务,注册中心记录服务。
- 服务消费者在注册中心拉取服务信息,负载均衡后,挑选一个服务使用。
- 向注册中心发送心跳包,若心跳异常则服务下线。
1. Eureka注册中心
1.1 搭建Eureka注册中心
搭建Eureka注册中心三步走即可:
-
引入Eureka依赖(服务端引入server依赖,客户端引入client)
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
-
编写启动类,加上@EnableEurekaServer注解
@SpringBootApplication @EnableEurekaServer public class EurekaServiceApplication { public static void main(String[] args) { SpringApplication.run(EurekaServiceApplication.class, args); } }
-
application.yml配置
server: port: 10086 spring: application: name: eurekaServer eureka: client: serviceUrl: defaultZone: http://127.0.0.1:10086/eureka/
这样就搭建了一个Eureka注册中心,访问ip+端口即可看到Eureka管理面板。
1.2 服务注册
注册一个服务
-
引依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
application.yml中配置
eureka: client: serviceUrl: #??????????? defaultZone: http://127.0.0.1:10086/eureka/
可以在启动类上加一个@EnableEurekaClient注解,不加暂时看起来也可以注册。然后直接启动,就可以在Eureka管理面板中看到一个新实例。
1.3 服务拉取
之前的代码是这样的:
User user = restTemplate.getForObject("http://localhost:8081/user/" + order.getUserId(), User.class);
现在我们不再把ip写死,而是去Eureka注册中心拉取服务,两步:
-
将ip改为服务名
User user = restTemplate.getForObject("http://userservice/user/" + order.getUserId(), User.class);
-
添加负载均衡注解@LoadBalanced
@SpringBootConfiguration public class CustomConfiguration { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
测试一下:
1.4 小结
2. nacos注册中心
nacos不仅可以作为注册中心,还有很多功能。
2.1Nacos搭建
下载nacos
下载对应系统版本的解压即可。
在/conf/application.properties下可以配置nacos的运行端口等。
在/bin目录下打开命令行,输入
startup.cmd -m standalone
单机启动nacos
访问控制台提供的url即可访问管理面板:
默认用户名密码都是nacos
2.2 服务注册
-
加入spring-cloud-alibaba的管理依赖
<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客户端依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
配置nacos注册中心地址
spring: cloud: nacos: server-addr: localhost:8848
启动后,即可在nacos管理面板中发现该服务。
2.3 服务拉取
把刚刚的两个服务从Eureka换成nacos后,测试。
有一个小小的坑,那就是nacos的服务名是要区分大小写的,Eureka是不区分的,所以要特别注意服务名,不然会请求失败。
2.4 服务分级存储模型
也就是可以给服务分组。这个模型是这样的:
可以通过discovery.cluster-name来配置
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SC
我们启动三个userservice实例,两个配置为SC,一个配置为SH,查看nacos变化:
设置这个有啥用呢?我们可以配置负载均衡规则来让ribbon优先调用和自己的discovery.cluster-name一样的服务集群。
配置:
userService:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
重启服务后请求order-service,多请求几次会发现,调用的都是clustername为SC的服务(order-service的clusterName也要设置为SC)。
2.5 权重配置
经过如上配置后,我们请求orderservice时,orderservice就会去找集群名一样的userservice,然后以轮询的方式请求,现在我们可以通过nacos管理面板来配置服务的权重,来控制服务被访问的频率。
- 权重一般配置为0~1之间
- 权重为0则服务不会被访问
2.6 环境隔离
nacos还可以配置namespace来进行环境隔离
不同namespace之间不能互相访问。
-
nacos管理面板创建命名空间,不填id的话会自动生成uuid
-
在application.yml中配置命名空间,例如修改orderservice的命名空间:
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: SC namespace: c8ae37f8-748e-4ada-8a9e-a9fb42c38116
重启实例后,请求orderservice,此时orderservice无法找到userservice。
2.7 临时实例和非临时实例
两者健康检测机制不同。
- 临时:定时发送心跳给nacos,不健康的临时实例会被nacos剔除
- 非临时:nacos主动发请求询问,不健康的非临时实例不会被nacos剔除
配置,ephemeral属性:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
ephemeral: false
nacos会主动推送消息给消费者告诉某个服务挂了,eureka不会。
2.8 nacos配置管理
服务很多时,配置复杂繁琐,统一配置管理解决了很多问题。
- 在nacos管理面板上创建配置文件:
-
为了在读取application.yml前读取nacos配置文件,我们需要创建一个bootstrap.yml文件(里面存放nacos的地址等信息),该文件会在application.yml前读取:
需要引入依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
创建bootstrap.yml(resource目录下):
spring: application: name: userService profiles: active: dev cloud: nacos: server-addr: localhost:8848 config: file-extension: yaml
-
写个接口测试一下(配置热更新加上@RefreshScope注解):
@Slf4j @RestController @RequestMapping("/user") @RefreshScope public class UserController { @Value("${pattern.dateformat}") @GetMapping("/now") public String now() { return new SimpleDateFormat(dateformat, Locale.CHINA).format(new Date()); } }
使用@ConfigurationProperties(prefix = "pattern")
也可以热更新
稍微解释一下刚刚bootstrap.yml里面配置的东西:
微服务会去nacos读取多个配置文件(如果有)
- [spring.application.name]-[profiles.active].yaml
- [spring.application.name].yaml
配置内容相同的部分,前者优先级大于后者(本地优先级最低),可以在nacos中再建一个配置文件试试,后者就可以存放公共配置,实现多环境配置共享。
2.9 nacos集群搭建
部署多个nacos,并用nginx做负载均衡。
五个步骤:
-
搭建MySQL集群并初始化数据库表:
sql:https://github.com/alibaba/nacos/blob/master/distribution/conf/mysql-schema.sql
-
下载解压nacos
-
配置nacos
在conf目录下创建cluster.conf配置文件并按照cluster.conf.example的内容配置节点。
# 配置nacos节点 127.0.0.1:8847 127.0.0.1:8849 127.0.0.1:8851
配置application.properties
spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=nacos db.password.0=nacos
-
启动nacos
三台nacos分别启动,在bin目录下运行startup.cmd
启动即可。
这里有个小坑:
Nacos2.0版本相比1.X新增了gRPC的通信方式,因此需要增加2个端口。新增端口是在配置的主端口(server.port)基础上,进行一定偏移量自动生成。
端口 与主端口的偏移量 描述 9848 1000 客户端gRPC请求服务端端口,用于客户端向服务端发起连接和请求 9849 1001 服务端gRPC请求服务端端口,用于服务间同步等 也就是在同一台机器上,如果搭建集群,建议使用8848,8850,8852,这种有间隔的端口,否则可能导致端口冲突。
-
nginx负载均衡
upstream nacos-cluster { server 127.0.0.1:8847; server 127.0.0.1:8849; server 127.0.0.1:8851; } server { listen 80; server_name localhost; location /nacos { proxy_pass http://nacos-cluster/nacos/; } }
启动nginx后访问localhost/nacos即可访问。
代码中将以前的8848改为80端口即可。
三、Ribbon负载均衡
1. 负载均衡策略
http://userservice/user/1 这个地址dns是解析不出来的,所以在order-service和user-service之间肯定有个东西能让他们通信,那就是Ribbon,Ribbon拿到这个地址后,根据配置的负载均衡策略去调用相应的服务实例。
Ribbon默认是ZoneAvoidanceRule策略,Eureka可配置的策略有:
配置策略有两种方式,代码方式和配置文件:
-
代码方式:
@Bean public IRule randomRule() { return new RandomRule(); }
-
配置文件:
userService: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
2. 饥饿加载
ribbon默认是懒加载,即客户端第一次请求服务时,ribbon才会去拉取服务列表,这样会导致第一次请求延迟大。我们可以设置饥饿加载策略来让ribbon在客户端创建时就拉取服务列表,降低第一次访问耗时。
在application.yml中配置:
ribbon:
eager-load:
enabled: true
clients:
- userservice
clients是一个数组,里面写需要饥饿加载的服务。