服务治理介绍
先来思考一个问题
通过上一章的操作,我们已经可以实现微服务之间的调用。但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:
- 一旦服务提供者地址变化,就需要手工修改代码
- 一旦是多个服务提供者,无法实现负载均衡功能
- 一旦服务变得越来越多,人工维护调用关系困难
那么应该怎么解决呢, 这时候就需要通过注册中心动态的实现服务治理。
什么是服务治理
服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现。
- 服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。
- 服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实
例的访问。
https://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环境
下载地址: https://github.com/alibaba/nacos/releases
下载zip格式的安装包,然后进行解压缩操作
切换目录
cd nacos/bin
命令启动
startup.cmd -m standalone
打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是nacos/nacos
注册微服务
1 2 3 4 5
| <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 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
|
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
|
@GetMapping("/save/{pId}") public Order order(@PathVariable("pId") Integer pId, @RequestParam(name = "queryType") String queryType) { log.info(">>客户下单,这时候要调用商品微服务查询商品信息"); Product product = null; 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的控制面板中是否有注册上来的订单微服务,然后通过访问消费者服务验证调用是否成功
https://imgtu.com/i/R5qdHA
负载均衡
什么是负载均衡
通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。
根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡。
服务端负载均衡指的是发生在服务提供者一方,比如常见的Nginx
负载均衡
而客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求。
https://imgtu.com/i/R5LFDH
我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行。
自定义实现负载均衡
- 通过idea再启动一个 shop-product 微服务,设置其端口为8082
- 通过nacos查看微服务的启动情况
https://imgtu.com/i/R5xV91
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
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); }
|
- 启动两个服务提供者和一个服务消费者,多访问几次消费者测试效果
https://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
|
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";
@GetMapping("/save/{pId}") public Order order(@PathVariable("pId") Integer pId, @RequestParam(name = "queryType") String queryType) { log.info(">>客户下单,这时候要调用商品微服务查询商品信息"); Product product = null; 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> <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
|
@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
|
@FeignClient(name = "shop-product",fallback = ProductFeignClientFallback.class) public interface ProductFeignClient {
public static final String DEFAULT_FALLBACK_MSG = "服务不可用!";
@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
|
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
启动类中增加一下注释:
创建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
|
@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> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
|
修改shop-order
启动引导类,增加feign扫描
1
| @EnableFeignClients(basePackages = "com.letcoding.product")
|
修改OrderController
1 2 3 4
| @Autowired private ProductFeignClient productFeignClient;
private static final String FEIGN = "feign";
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
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
|
@GetMapping("/save/{pId}") public Order order(@PathVariable("pId") Integer pId, @RequestParam(name = "queryType") String queryType) { log.info(">>客户下单,这时候要调用商品微服务查询商品信息"); Product product = null; 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微服务,查看效果