Spring-Cloud-Alibaba(3)-服务治理

服务治理介绍

先来思考一个问题
通过上一章的操作,我们已经可以实现微服务之间的调用。但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:

  • 一旦服务提供者地址变化,就需要手工修改代码
  • 一旦是多个服务提供者,无法实现负载均衡功能
  • 一旦服务变得越来越多,人工维护调用关系困难

那么应该怎么解决呢, 这时候就需要通过注册中心动态的实现服务治理。

什么是服务治理

服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现。

  • 服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。
  • 服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实
    例的访问。

RRd8Ag.jpghttps://imgtu.com/i/RRd8Ag

通过上面的调用图会发现,除了微服务,还有一个组件是服务注册中心,它是微服务架构非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:

  • 服务发现:

    • 服务注册:保存服务提供者和服务调用者的信息
    • 服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
  • 服务配置:

    • 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
    • 配置下发:主动将配置推送给服务提供者和服务调用者
  • 服务健康检测

    • 检测服务提供者的健康情况,如果发现异常,执行服务剔除

常见的注册中心

  • Zookeeper
    zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

  • Eureka
    Eureka是Springcloud Netflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭源

  • Consul

    Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。

  • Nacos

    Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 SpringCloud Alibaba 组件之一,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config。

nacos简介

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。从上面的介绍就可以看出,nacos的作用就是一个注册中心,用来管理注册上来的各个微服务。

nacos实战入门

本篇只讲解windows安装运行,Linux安装部署请参考博猪Docker系列中的docker安装部署Nacos.

接下来,我们就在现有的环境中加入nacos,并将我们的两个微服务注册上去。

搭建nacos环境

  • 安装nacos

下载地址: https://github.com/alibaba/nacos/releases
下载zip格式的安装包,然后进行解压缩操作

  • 启动nacos

切换目录

cd nacos/bin

命令启动

startup.cmd -m standalone

  • 访问nacos

打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是nacos/nacos

注册微服务

  • 在pom.xml中添加nacos的依赖
1
2
3
4
5
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 开启服务发现注解,在项目引导类ProductApplication上增加@EnableDiscoveryClient注解
  • 添加注册中心配置
1
2
3
4
5
cloud:
nacos:
discovery:
server-addr: 192.168.56.120:8848 # 注册中心nacos服务端地址及端口
namespace: 6d39b87a-55bb-4497-bec5-79bdd3b9789b # 命名空间,方便管理
  • 启动服务, 观察nacos的控制面板中是否有注册上来的微服务

服务调用

  • 修改订单Controller
  • 增加服务发现api和常量:
1
2
3
private static final String NACOS = "nacos";
@Autowired
private DiscoveryClient discoveryClient;
  • 增加私有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 通过服务发现接口
* @param pId 产品id
* @return
*/
private Product queryProductByDiscoveryClient(Integer pId) {
List<ServiceInstance> productInstances = discoveryClient.getInstances("shop-product");
if (CollectionUtils.isEmpty(productInstances)) {
log.error("产品服务为空!");
}
ServiceInstance defaultServiceInstance = productInstances.get(0);
String host = defaultServiceInstance.getHost();
int port = defaultServiceInstance.getPort();
return restTemplate.getForObject("http://" + host + ":" + port + "/product/info/" + pId, Product.class);
}
  • 修改保存订单方法
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
/**
* 保存指定产品订单
* @param pId
* @return
*/
@GetMapping("/save/{pId}")
public Order order(@PathVariable("pId") Integer pId, @RequestParam(name = "queryType") String queryType) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
Product product = null;
// 通过restTemplate调用商品微服务
if (REST_TEMPLATE.equals(queryType)) {
product = queryProductByRestTemplate(pId);
} else if (NACOS.equals(queryType)) {
product = queryProductByDiscoveryClient(pId);
}

log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUId(1);
order.setUserName("测试用户");
order.setPId(product.getPId());
order.setPName(product.getPName());
order.setPPrice(product.getPPrice());
order.setNumber(1);
orderService.saveOrder(order);
return order;
}

DiscoveryClient是专门负责服务注册和发现的,我们可以通过它获取到注册到注册中心的所有服务。

  • 启动测试

启动服务, 观察nacos的控制面板中是否有注册上来的订单微服务,然后通过访问消费者服务验证调用是否成功

R5qdHA.pnghttps://imgtu.com/i/R5qdHA

负载均衡

什么是负载均衡

通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。
根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡。
服务端负载均衡指的是发生在服务提供者一方,比如常见的Nginx负载均衡
而客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求。

R5LFDH.pnghttps://imgtu.com/i/R5LFDH

我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行。

自定义实现负载均衡

  • 通过idea再启动一个 shop-product 微服务,设置其端口为8082
  • 通过nacos查看微服务的启动情况

R5xV91.pnghttps://imgtu.com/i/R5xV91

  • 修改 shop-order 的代码,实现负载均衡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 通过服务发现接口
*
* @param pId 产品id
* @return
*/
private Product queryProductByDiscoveryClient(Integer pId) {
List<ServiceInstance> productInstances = discoveryClient.getInstances("shop-product");
if (CollectionUtils.isEmpty(productInstances)) {
log.error("产品服务为空!");
}
int index = new Random().nextInt(productInstances.size());
ServiceInstance defaultServiceInstance = productInstances.get(index);
String host = defaultServiceInstance.getHost();
int port = defaultServiceInstance.getPort();
String url = "http://" + host + ":" + port + "/product/info/" + pId;
log.info(">>>>>>>>>>>>>>>从nacos中获取到的微服务地址为:" + url);

return restTemplate.getForObject(url, Product.class);
}
  • 启动两个服务提供者和一个服务消费者,多访问几次消费者测试效果

W9yfHO.pnghttps://imgtu.com/i/W9yfHO

从上述图片我们可以看出我们自定义的负载均衡存在一下几点情况:

  • 对业务代码侵入性太高;
  • 请求具有不确定性;
  • 请求分发策略单一,更改难度较大

对于以上问题,SpringCloud 已经为我们提供了一个解决方案:Ribbon

基于Ribbon实现负载均衡

Ribbon是Spring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡

  • 在RestTemplate 的生成方法上添加@LoadBalanced注解
1
2
3
4
5
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
  • 增加负载均衡调用算法
1
2
3
4
5
6
7
8
/**
* RestTemplate 负载均衡风格查询
* @param pId 产品id
* @return
*/
private Product queryProductLoadBalancingByRibbon(Integer pId) {
return restTemplate.getForObject("http://shop-product/product/info/" + pId, Product.class);
}
  • 修改下单方法
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
private static final String RIBBON = "ribbon";
/**
* 保存指定产品订单
* @param pId
* @return
*/
@GetMapping("/save/{pId}")
public Order order(@PathVariable("pId") Integer pId, @RequestParam(name = "queryType") String queryType) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
Product product = null;
// 通过restTemplate调用商品微服务
if (REST_TEMPLATE.equals(queryType)) {
product = queryProductByRestTemplate(pId);
} else if (NACOS.equals(queryType)) {
product = queryProductByDiscoveryClient(pId);
} else if (RIBBON.equals(queryType)) {
product = queryProductLoadBalancingByRibbon(pId);
}

log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUId(1);
order.setUserName("测试用户");
order.setPId(product.getPId());
order.setPName(product.getPName());
order.setPPrice(product.getPPrice());
order.setNumber(1);
orderService.saveOrder(order);
return order;
}

Ribbon支持的负载均衡策略:
Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为com.netflix.loadbalancer.IRule , 具体的负载策略如下图所示:

策略名策略描述实现说明
BestAvailableRule选择一个最小的并发请求的server逐个考察Server,如果Server被 tripped了,则忽略,在选择其中 ActiveRequestsCount最小的server
AvailabilityFilteringRule过滤掉那些因为一直 连接失败的被标记为 circuit tripped的后 端server,并过滤掉 那些高并发的的后端 server(active connections 超过配 置的阈值)使用一个AvailabilityPredicate来包含 过滤server的逻辑,其实就就是检查 status里记录的各个server的运行状 态
WeightedResponseTimeRule根据相应时间分配一 个weight,相应时 间越长,weight越 小,被选中的可能性 越低。一个后台线程定期的从status里面读 取评价响应时间,为每个server计算 一个weight。Weight的计算也比较简 单responsetime 减去每个server自己 平均的responsetime是server的权 重。当刚开始运行,没有形成statas 时,使用roubine策略选择server。
RetryRule对选定的负载均衡策略机上重试机制。在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server
RoundRobinRule轮询方式轮询选server轮询index,选择index对应位置的server
RandomRule随机选择一个server在index上随机,选择index对应位置的server
ZoneAvoidanceRule复合判断server所在区域的性能和server的可用性选择server使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。

我们可以通过修改配置来调整Ribbon的负载均衡策略,具体代码如下:

1
2
3
shop-product: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

基于Feign实现服务调用

什么是Feign

Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。
Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果。

Feign的使用

创建新的模块shop-product-api

在这里博猪说一下博猪单独创建这个模块的意义或者好处在哪里。博猪也在项目初始化的时候说明了博猪特别不喜欢甚至讨厌所有的Java对象放在common模块里面,但是刚开始用着挺爽的,但是后期项目规模增大后,对象直接的影响或者说带给我们的干扰也挺多的,所以我习惯把feign相关对外提供的接口单独定义,并且字段相关尽量简化,因为feign底层也是http请求,所以尽可能减少http之间的请求时长。

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fegin组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

创建feign接口返回对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @ClassName ProductVO
* @Description TODO
* @Author will
* @Date 2021/7/11 18:33
*/
@Data
public class ProductVO implements Serializable {

private static final long serialVersionUID = 1L;

/** 主键 */
private Integer pId;
/** 商品名称 */
private String pName;
/** 商品价格 */
private Double pPrice;
/** 库存 */
private Integer stock;
}

创建Feign接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @ClassName ProductFeignClient
* @Description TODO
* @Author will
* @Date 2021/7/11 18:25
*/
@FeignClient(name = "shop-product",fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {

public static final String DEFAULT_FALLBACK_MSG = "服务不可用!";

/**
* 根据产品id查询产品详情
* @param productId
* @return
*/
@GetMapping("/api/queryProductInfoByProductId/{productId}")
public ProductVO queryProductInfoByProductId(@PathVariable Integer productId);

}

创建FeignFallback

接口调用异常,会默认跳转到此方法中,在这个方法中可做业务相关处理,比如增加日志等,方便开发和运维排查处理问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @ClassName ProductFeignClientFallback
* @Description TODO
* @Author will
* @Date 2021/7/11 18:29
*/
public class ProductFeignClientFallback implements ProductFeignClient {

@Override
public ProductVO queryProductInfoByProductId(Integer productId) {
System.out.println(DEFAULT_FALLBACK_MSG);
return null;
}
}

修改shop-product模块

增加依赖
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.letcoding</groupId>
<artifactId>shop-product-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclusion>
</exclusions>
</dependency>
开启Feign,在shop-order启动类中增加一下注释:
1
@EnableFeignClients
创建Feign接口实现
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
/**
* @ClassName ProductFeignClientImpl
* @Description 产品feign接口的实现
* @Author will
* @Date 2021/7/11 18:41
*/
@Slf4j
@RestController
public class ProductFeignClientImpl implements ProductFeignClient {

@Autowired
private ProductService productService;


@Override
public ProductVO queryProductInfoByProductId(Integer productId) {
Product product = productService.queryById(productId);
log.info("查询到商品:" + JSON.toJSONString(product));
if (product != null) {
ProductVO productVO = new ProductVO();
BeanUtils.copyProperties(product, productVO);
return productVO;
}
return null;
}
}

修改shop-order模块

去掉shop-product依赖,增加shop-product-api、Feign依赖
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.letcoding</groupId>
<artifactId>shop-product-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<!--fegin组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
修改shop-order启动引导类,增加feign扫描
1
@EnableFeignClients(basePackages = "com.letcoding.product")
修改OrderController
  • 添加产品feign接口
1
2
3
4
@Autowired
private ProductFeignClient productFeignClient;

private static final String FEIGN = "feign";
  • 增加feign接口相关实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* RestTemplate 负载均衡风格查询
* @param pId 产品id
* @return
*/
private Product queryProductLoadBalancingByFeign(Integer pId) {
ProductVO productVO = productFeignClient.queryProductInfoByProductId(pId);
if (productVO != null) {
Product product = new Product();
BeanUtils.copyProperties(productVO, product);
return product;
}
return null;
}
  • 修改订单保存方法
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

/**
* 保存指定产品订单
* @param pId
* @return
*/
@GetMapping("/save/{pId}")
public Order order(@PathVariable("pId") Integer pId, @RequestParam(name = "queryType") String queryType) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
Product product = null;
// 通过restTemplate调用商品微服务
if (FEIGN.equals(queryType)) {
product = queryProductLoadBalancingByFeign(pId);
}

log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUId(1);
order.setUserName("测试用户");
order.setPId(product.getPId());
order.setPName(product.getPName());
order.setPPrice(product.getPPrice());
order.setNumber(1);
orderService.saveOrder(order);
return order;
}

重启order微服务,查看效果


Spring-Cloud-Alibaba(3)-服务治理
https://github.com/yangxiangnanwill/yangxiangnanwill.github.io/2024/01/03/好好码代码吖/JAVA/Spring/Spring-Cloud-Alibaba(3)-服务治理/
作者
will
发布于
2024年1月3日
许可协议