第三章 服务治理

作者为冰河忠实粉丝,可以加入【冰河技术】知识星球获取源码,也可以关注【冰河技术】公众号,加入社群学习,学习氛围好,干货满满。
冰河技术.jpg

1 服务治理

前边提到过,服务治理,就是自动化的管理各个微服务,核心的功能就是服务的注册、发现和剔除。
如果系统采用了微服务的架构模式,随着微服务数量的不断增多,服务之间的调用关系会变得纵横交错,以纯人工手动的方式来管理这些微服务以及微服务之间的调用关系是及其复杂的,也是极度不可取的。
所以,需要引入服务治理的功能。服务治理也是在微服务架构模式下的一种最核心和最基本的模块,主要用来实现各个微服务的自动注册与发现。
引入服务治理后,微服务项目总体上可以分为三个大的模块:服务提供者、服务消费者和注册中心,三者的关系如下图所示。
服务治理.png

  1. 服务提供者会将自身提供的服务注册到注册中心,并向注册中心发送心跳信息来证明自己还存活,心跳信息中就会包含服务提供者自身的服务信息。
  2. 注册中心会存储服务提供者上报的信息,并通过服务提供者发送的心跳来更新服务提供者最后的存活时间,如果超过一段时间没有收到服务提供者上报的心跳信息,则注册中心会认为服务提供者不可用,会将对应的服务提供者从服务列表中剔除。
  3. 服务消费者会向注册中心订阅自身监听的服务,注册中心会保存服务消费者的信息,也会向服务消费者推送服务提供者的信息。
  4. 服务消费者从注册中心获取到服务提供者的信息时,会直接调用服务提供者的接口来实现远程调用。

这里需要注意的是:服务消费者一般会从注册中心中获取到所有服务提供者的信息,根据具体情况实现对具体服务提供者的实例进行访问。

2 注册中心

微服务实现服务治理的关键就是引入了注册中心,它是微服务架构模式下一个非常重要的组件,主要实现了服务注册与发现,服务配置和服务的健康检测等功能。它充当了服务提供者和服务消费者之间的中介。

2.1 服务注册与发现

  1. 服务注册:服务的提供者和消费者都可以向注册中心订阅微服务相关的配置信息。
  2. 服务发现:可以理解为服务订阅,服务调用者也是服务消费者,向注册中心订阅服务提供者的信息,注册中心会向服务消费者推送服务提供者的信息。

2.2 服务配置

  1. 服务订阅:注册中心提供保存服务提供者和服务消费者的相关信息。
  2. 服务下发:注册中心能够将微服务相关的配置信息主动推送给服务的提供者和消费者。

2.3 服务健康检测

注册中心会定期检测存储的服务列表中服务提供者的健康状况,例如服务提供者超过一定的时间没有上报心跳信息,则注册中心会认为对应的服务提供者不可用,就会将服务提供者踢出服务列表。

2.4 常见的注册中心

能够实现注册中心功能的组件有很多,但是常用的组件大概包含:Zookeeper、Eureka、Consul、Etcd、Nacos等。

  1. Zookeeper
    Zookeeper是Apache Hadoop的一个子项目,它是一个分布式服务治理框架,主要用来解决应用开发中遇到的一些数据管理问题,例如:分布式集群管理、元数据管理、分布式配置管理、状态同步和统一命名管理等。在高并发环境下,也可以通过Zookeeper实现分布式锁功能。
  2. Eureka
    Eureka是Netflix开源的SpringCloud中支持服务注册与发现的组件,已闭源。
  3. Consul
    Consul 是 HashiCorp 公司推出的开源产品,用于实现分布式系统的服务发现、服务隔离、服务配置,这些功能中的每一个都可以根据需要单独使用,也可以同时使用所有功能。
  4. Etcd
    etcd 是一个高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以优雅地处理网络分区期间的领导者选举,即使在领导者节点中也可以容忍机器故障。
  5. Nacos
    Nacos是阿里巴巴开源的一款更易于构建云原生应用的支持动态服务发现、配置管理和服务管理的平台,其提供了一组简单易用的特性集,能够快速实现动态服务发现、服务配置、服务元数据及流量管理,主要如下所示。
  • 服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如IP地址、端口等信 息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
  • 服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
  • 服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
  • 服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清 单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地存。
  • 服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。

3 搭建Nacos环境

3.1 安装Nacos

  1. 下载Nacos安装包,https://github.com/alibaba/nacos/releases,作者下载的是1.4.5版本。
  2. 解压后,用命令行进入Nacos的bin目录下,执行以下命令,已单机的方式启动Nacos。
# win
startup.cmd -m standalone
# mac
startup.sh -m standalone

注意:如果需要以单机的方式启动Nacos,则需要添加 -m standalone 参数,否则,Nacos会以集群的方式启动。

  1. 启动Nacos,在浏览器输入链接 http://localhost:8848/nacos 访问Nacos的管理界面,默认的用户名和密码都是Nacos。

image.png

  1. 登录进入后,默认的页面是配置管理-配置列表,后续我们各服务的配置可以放在这里。

image.png

  1. 进入服务管理-服务列表,列表中目前还没有任何服务,我们后续注册到Nacos的服务实例信息在这里可以看到。

image.png

3.2 集成Nacos注册中心

3.2.1 改造服务

  1. 在用户服务的pom.xml文件中添加nacos的服务注册与发现依赖,如下。
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 在用户服务的application.yml文件中添加Nacos注册中心的服务地址配置,如下。
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  1. 在用户微服务的启动类io.binghe.shop#UserStarter上标注@EnableDiscoveryClient注解,如下。

image.png

  1. 启动用户服务,点击服务列表的查询进行刷新,用户服务成功注册到Nacos。

image.png

  1. 以同样的方式改造产品服务、订单服务。

image.png

3.2.2 实现服务发现

DiscoveryClient是 Spring Cloud 提供的一个用于服务发现和注册的客户端组件。它可以用于在分布式系统中实现服务的自动注册和发现。

  1. 在OrderServiceImpl中注入DiscoveryClient类对象。
@Autowired
private DiscoveryClient discoveryClient;
  1. 创建动态获取服务地址方法
private String getServiceUrl(String serviceName){
	// 通过discoveryClient对象可以获取已注册到注册中心的服务信息。
	ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0);
	return serviceInstance.getHost() + ":" + serviceInstance.getPort();
}

实现方式是调用DiscoveryClient对象的getInstances()方法,并传入服务的名称,从Nacos注册中心中获取一个ServiceInstance类型的List集合,从List集合中获取第1个元素,也就是从List集合中获取到一个ServiceInstance对象,从ServiceInstance对象中获取到IP地址和端口号,并将其拼接成IP:PORT的形式。
3. 定义提供者名称
在shop-common服务下中ServerConstant类添加如下代码。

public static final String USER_SERVER = "server-user";
public static final String PRODUCT_SERVER = "server-product";

注意:变量的值需要与用户、产品服务下的application.yml文件中的如下配置的值相同,如下。

spring:
  application:
    name: server-user
  1. 将硬编码部分改为动态获取
//从Nacos服务中获取用户服务与商品服务的地址
String userUrl = this.getServiceUrl(ServerConstant.USER_SERVER);
String productUrl = this.getServiceUrl(ServerConstant.PRODUCT_SERVER);

//##################此处省略N行代码########################

// 请求用户服务部分代码
User user = restTemplate.getForObject("http://" + userUrl + "/user/get/" + orderParams.getUserId(), User.class);
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}

// 请求产品服务部分代码
Product product = restTemplate.getForObject("http://" + productUrl + "/product/get/" + orderParams.getProductId(), Product.class);
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

//##################此处省略N行代码########################

// 请求产品服务扣减库存部分代码
Result<Integer> result = restTemplate.getForObject("http://" + productUrl + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

3.3 测试扣减库存接口

  1. 库存不足情况

image.png

  1. 库存充足情况

image.png

4 实现服务调用的负载均衡

负载均衡就是将原本由一台服务器处理的请求根据一定的规则分担到多台服务器上进行处理。
负载均衡根据发生的位置,可以分为服务端负载均衡和客户端负载均衡。

4.1 服务端负载均衡

服务端负载均衡指的是在服务端处理负载均衡的逻辑,如下图所示。服务端负载均衡.png
负载均衡在服务端进行处理,当客户端访问服务端的服务A时,首先访问到服务端的负载均衡器,由服务端的负载均衡器将客户端的请求均匀的分发到服务端部署的两个服务A上。

4.2 客户端负载均衡

客户端负载均衡指的是在客户端处理负载均衡的逻辑,如下图所示。

客户端负载均衡.png

负载均衡逻辑在客户端一侧,客户端应用调用服务端的应用A时,在向服务端发送请求时,就已经经过负载均衡的逻辑处理,并直接向服务端的某个服务发送请求。

4.3 前期准备

4.3.1 启动多服务

目前,用户服务与产品服务端口分别为8060、8070,我们使用idea在分别配置8061端口为用户服务2,配置8071端口为产品服务2,需要在VM options一栏后面添加JVM启动参数-Dserver.port=8061 | 8071如下图所示。
image.png
5个服务都启动后,在Nacos查看服务注册情况,如下图所示。
image.png

4.4 实现自定义负载均衡

在整个项目中,订单微服务作为客户端存在,由订单微服务调用用户微服务和商品微服务,所以,这里采用的是客户端负载均衡的模式。

4.4.1 改造getServiceUrl方法

private String getServiceUrl(String serviceName){
	// 获取服务列表
    List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
    // 通过随机函数生成实例下标
	int index = new Random().nextInt(instances.size());
    ServiceInstance serviceInstance = instances.get(index);
    String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
    log.info("负载均衡后的服务地址为:{}", url);
    return url;
}

在getServiceUrl()方法中,通过服务的名称在Nacos中获取到所有的服务实例列表,然后使用随机函数随机生成一个0到服务列表长度减1的整数,而这个随机生成的整数恰好在服务实例列表的下标范围内,然后以这个整数作为下标获取服务列表中的某个服务实例。从而以随机的方式实现了负载均衡,并从获取到的服务实例中获取到服务实例所在服务器的IP地址和端口号,拼接成IP:PORT的形式返回。

4.4.2 测试效果

多次请求库存扣减接口http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,在订单服务输出的日志信息如下所示。

负载均衡后的服务地址为:192.168.0.27:8061
负载均衡后的服务地址为:192.168.0.27:8071
负载均衡后的服务地址为:192.168.0.27:8060
负载均衡后的服务地址为:192.168.0.27:8070
负载均衡后的服务地址为:192.168.0.27:8060
负载均衡后的服务地址为:192.168.0.27:8071
负载均衡后的服务地址为:192.168.0.27:8061
负载均衡后的服务地址为:192.168.0.27:8070

初步实现了订单服务调用用户服务和商品服务的负载均衡效果。

4.5 使用Ribbon实现负载均衡

Ribbon是SpringCloud提供的一个能够实现负载均衡的组件,使用Ribbon能够轻松实现微服务之间调用的负载均衡,以提高系统的可靠性和可用性。
Ribbon主要有以下几个特点:

  1. 客户端负载均衡:Ribbon是一个客户端负载均衡器,它将负载均衡逻辑放在了客户端,而不是服务端。这种方式可以使客户端具有更大的灵活性和可定制性。
  2. 多种负载均衡算法:Ribbon支持多种负载均衡算法,包括轮询、随机、加权随机、最少连接、加权最少连接等。
  3. 定制化配置:Ribbon提供了丰富的配置选项,你可以根据需要进行定制化配置。例如,你可以设置超时时间、连接池大小、重试次数等参数。

4.5.1 改造LoadBalanceConfig

  1. 在订单服务 LoadBalanceConfig 类的 restTemplate()方法上添加@LoadBalanced注解,如下所示。
@Configuration
public class LoadBalanceConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

4.5.2 改造saveOrder方法

首先删除getServiceUrl方法,将getForObject中的请求地址(userUrl,productUrl)参数替换为服务名,如下所示。


User user = restTemplate.getForObject("http://" + ServerConstant.USER_SERVER + "/user/get/" + orderParams.getUserId(), User.class);
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}

Product product = restTemplate.getForObject("http://" + ServerConstant.PRODUCT_SERVER + "/product/get/" + orderParams.getProductId(), Product.class);
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

//##################此处省略N行代码########################

Result<Integer> result = restTemplate.getForObject("http://" + ServerConstant.PRODUCT_SERVER + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
 if (result.getCode() != HttpCode.SUCCESS){
     throw new RuntimeException("库存扣减失败");
}

4.5.3 测试效果

在多次请求库存扣减接口http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,产品服务打印日志如下。

获取到的商品信息为:{"id":1001,"proName":"华为","proPrice":2399.00,"proStock":84}
获取到的商品信息为:{"id":1002,"proName":"小米","proPrice":1999.00,"proStock":99}
获取到的商品信息为:{"id":1001,"proName":"华为","proPrice":2399.00,"proStock":83}

产品服务2打印日志如下。

更新商品库存传递的参数为: 商品id:1001, 购买数量:1
更新商品库存传递的参数为: 商品id:1002, 购买数量:1
更新商品库存传递的参数为: 商品id:1001, 购买数量:1

启动的每个用户服务和商品服务都会打印相关的日志,说明使用Ribbon实现了负载均衡功能。

4.6 使用Feign实现负载均衡效果

Fegin是SpringCloud提供的一个HTTP客户端,但只是一个声明式的伪客户端,它能够使远程调用和本地调用一样简单。阿里巴巴开源的Nacos能够兼容Ribbon,而Fegin又集成了Ribbon,所以,使用Fegin也能够实现负载均衡。

4.6.1 改造订单服务

  1. 在订单服务的pom.xml文件中添加Fegin相关的依赖,如下所示。
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 在订单微服务的启动类cn.mawenda.shop.ShopOrderApplication 上添加 @EnableFeignClients 注解,如下所示。
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = { "cn.mawenda.shop.order.mapper" })
@EnableDiscoveryClient
@EnableFeignClients
public class ShopOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShopOrderApplication.class, args);
    }
}
  1. 在订单微服务工程shop-order下新建cn.mawenda.shop.order.feign包,并在该包下新建RemoteUserService接口与RemoteProductService接口,并分别在各接口上标注@FeignClient(ServerConstant.USER_SERVER)与@FeignClient(ServerConstant.PRODUCT_SERVER)注解,其中注解的value属性为用户微服务的服务名称,代码如下所示。
// 调用用户服务接口
@FeignClient(ServerConstant.USER_SERVER)
public interface RemoteUserService {
    
    @GetMapping(value = "/user/get/{uid}")
    User getUser(@PathVariable("uid") Long uid);
}

// 调用产品服务接口
@FeignClient(ServerConstant.PRODUCT_SERVER)
public interface RemoteProductService {
    
    @GetMapping(value = "/product/get/{pid}")
    Product getProduct(@PathVariable("pid") Long pid);

    @GetMapping(value = "/product/update_count/{pid}/{count}")
    Result<Integer> updateCount(@PathVariable("pid") Long pid, @PathVariable("count") Integer count);
}

  1. 注入用户服务、产品服务接口
@Autowired
private RemoteUserService remoteUserService;
@Autowired
private RemoteProductService remoteProductService;
  1. 改造saveOrder方法部分代码,如下所示
User user = remoteUserService.getUser(orderParams.getUserId());
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}

Product product = remoteProductService.getProduct(orderParams.getProductId());
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

//##################此处省略N行代码########################

Result<Integer> result = remoteProductService.updateCount(orderParams.getProductId(),orderParams.getCount());
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

4.6.2 测试效果

重启订单服务,并多次请求库存扣减接口 http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,各服务的日志和上次测试效果一样,作者不在此展示了。

5.总结

  1. Feign可以通过接口与注解的是定义对其他微服务进行调用,不需要手动构建HTTP请求,使这部分代码更清晰简洁,提高了可读性和可维护性。
  2. Feign内部整合了Ribbon,可以实现对服务的负载均衡和自动服务发现,不需要通过指定的URL与端口,通过服务名就可以实现对服务的远程调用,解决掉了硬编码问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值