1. 服务治理–Nacos
1.1 基本概念
我们把服务提供者的网络地址 (ip,端口)等硬编码到了代码中,这种做法存在许多问题:
- 一旦服务提供者地址变化,就需要手工修改代码
- 一旦是多个服务提供者,无法实现负载均衡功能
- 一旦服务变得越来越多,人工维护调用关系困难
这时候就需要通过注册中心
动态的实现`服务治理·
1.1.1 服务治理介绍
服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现

1.1.2 服务注册中心
- 服务发现:
- 服务注册:保存服务提供者和服务调用者的信息
- 服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
- 服务配置:
- 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
- 配置下发:主动将配置推送给服务提供者和服务调用者
- 服务健康检测
- 检测服务提供者的健康情况,如果发现异常,执行服务剔除
常见的注册中心
Zookeeper
zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
Eureka
Eureka是Springcloud Netflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭源
Consul
Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value 存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以;安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。
Nacos
Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 Spring Cloud Alibaba 组件之一,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config。
1.2 搭建环境
安装nacos
下载地址: https://github.com/alibaba/nacos/releases
下载zip格式的安装包,然后进行解压缩操作
启动nacos
1 2 3 4
| #切换目录 cd nacos/bin #命令启动 startup.cmd -m standalone
|
访问nacos
打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是nacos/nacos
1.3 注册与调用微服务
常用步骤
注册
:
- 添加依赖
- 主类上添加@EnableDiscoveryClient注解
- 配置文件中添加nacos服务地址
调用
:
- 注入DiscoveryClient discoveryClient
- 从nacos中获取服务地址
- 通过restTemplate调用微服务
1.3.1 将商品微服务注册到naco
添加依赖
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
主类上添加@EnableDiscoveryClient注解
1 2 3 4 5 6 7
| @SpringBootApplication @EnableDiscoveryClient public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class,args); } }
|
配置文件中添加nacos服务地址
1 2 3 4 5
| spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848
|
1.3.2 将订单微服务注册到nacos
添加依赖
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
|
主类上添加@EnableDiscoveryClient注解
1 2 3 4 5 6 7 8 9 10 11 12
| @SpringBootApplication @EnableDiscoveryClient public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class,args); } @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
|
配置文件中添加nacos服务地址
1 2 3 4 5
| spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848
|
1.3.3 调用商品微服务
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
| @RestController @Slf4j public class OrderController { @Autowired private RestTemplate restTemplate; @Autowired private OrderService orderService; @Autowired private DiscoveryClient discoveryClient; @GetMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);
List<ServiceInstance> instances = discoveryClient.getInstances("service-product"); ServiceInstance instance = instances.get(0); String url = serviceInstance.getHost() + ":" +serviceInstance.getPort(); Product product = restTemplate.getForObject("http://" + url + "/product/" + pid, Product.class);
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1);
orderService.createOrder(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order; } }
|
2 服务配置–Nacos Config
2.1 服务配置中心介绍
首先我们来看一下,微服务架构下关于配置文件的一些问题:
配置文件相对分散
在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散在各个微服务中,不好统一配置和管理
配置文件无法区分环境
微服务项目可能会有多个环境,例如:测试环境、预发布环境、生产环境
每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要我们去各个微服务下手动维护,这比较困难
配置文件无法实时更新
我们修改了配置文件之后,必须重新启动微服务才能使配置生效,这对一个正在运行的项目来说是非常不友好的
基于上面这些问题,我们就需要配置中心
的加入来解决这些问题
配置中心的思路
- 首先把项目中各种配置全部都放到一个集中的地方进行统一管理,并提供一套标准的接口
- 当各个服务需要获取配置的时候,就来配置中心的接口拉取自己的配置
- 当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新
业界常见的服务配置中心
Apollo
Apollo是由携程开源的分布式配置中心
特点有很多,比如:配置更新之后可以实时生效,支持灰度发布功能,并且能对所有的配置进行版本管理、操作审计等功能,提供开放平台API
Disconf
Disconf是由百度开源的分布式配置中心
它是基于Zookeeper来实现配置变更后实时通知和生效的
SpringCloud Config
这是Spring Cloud中带的配置中心组件
它和Spring是无缝集成,使用起来非常方便,并且它的配置存储支持Git
不过它没有可视化的操作界面,配置的生效也不是实时的,需要重启或去刷新
Nacos
这是SpingCloud alibaba技术栈中的一个组件,前面我们已经使用它做过服务注册中心
其实它也集成了服务配置的功能,我们可以直接使用它作为服务配置中心。
2.2 Nacos Config入门
使用步骤
- 搭建nacos环境
- 微服务引入nacos依赖
- 在微服务中添加nacos config的配置
- 在nacos中添加配置
1.搭建nacos环境
启动nacos能正常访问即可
2.在微服务中引入nacos依赖
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
|
3.在微服务中添加nacos config的配置
注意
:不能使用原来的application.yml作为配置文件,而是新建一个bootstrap.yml作为配置文件
配置文件优先级(由高到低):
bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
1 2 3 4 5 6 7 8 9 10
| spring: application: name: service-product cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml profiles: active: dev
|
4.在nacos中添加配置
Data ID: service-product-dev.yaml (要与微服务中的bottstrap.yml内容一致)
配置内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| server: port: 8081 spring: application: name: service-product datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true username: root password: root jpa: properties: hibernate: hbm2ddl: auto: update dialect: org.hibernate.dialect.MySQL5InnoDBDialect cloud: nacos: discovery: server-addr: 127.0.0.1:8848
|
2.3 配置动态刷新
在nacos中的service-product-dev.yaml配置项中添加下面配置:
1 2
| config: appName: product
|
两种注入方式
硬编码注入
1 2 3 4 5 6 7 8 9 10 11
| @RestController public class NacosConfigController {
@Autowired private ConfigurableApplicationContext applicationContext;
@GetMapping("/nacos-config-test1") public String nacosConfigTest1() { return applicationContext.getEnvironment().getProperty("config.appName");; } }
|
注解方式(推荐)
1 2 3 4 5 6 7 8 9 10 11 12
| @RestController @RefreshScope public class NacosConfigController {
@Value("${config.appName}") private String appName;
@GetMapping("/nacos-config-test2") public String nacosConfigTest2() { return appName; } }
|
2.4 配置共享
2.4.1 同一个微服务的不同环境之间共享配置
步骤
- 新建一个
spring.application.name
的公共配置文件
- 在公共配置文件中填入公共配置
- 新建
spring.application.name
+环境
配置文件
- 在环境文件中单独配置
新建服务名称的公共配置文件
新建一个名为service-product.yaml配置存放商品微服务的公共配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| server: port: 8081 spring: application: name: service-product datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true username: root password: root jpa: properties: hibernate: hbm2ddl: auto: update dialect: org.hibernate.dialect.MySQL5InnoDBDialect cloud: nacos: discovery: server-addr: 127.0.0.1:8848
|
新建名为service-product-dev.yaml和service-product-test.yaml配置存放测试环境的配置
添加测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RefreshScope public class NacosConfigController {
@Value("${config.env}") private String env;
@GetMapping("/nacos-config-test3") public String nacosConfigTest3() { return env; } }
|
更改测试环境
只需修改bootstrap.yml中的配置,将active设置成test
2.4.2 不同微服务中间共享配置
步骤
- 定义公共配置类
- 修改bootstrap.yml
定义公共配置类
在nacos中定义一个DataID为all-service.yaml的配置,用于所有微服务共享
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true username: root password: root jpa: properties: hibernate: hbm2ddl: auto: update dialect: org.hibernate.dialect.MySQL5InnoDBDialect cloud: nacos: discovery: server-addr: 127.0.0.1:8848
|
service-product.yaml
1 2 3 4 5
| server: port: 8081 spring: application: name: service-product
|
service-product-dev.yaml
修改bootstrap.yml
1 2 3 4 5 6 7 8 9 10 11 12
| spring: application: name: service-product cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml shared-dataids: all-service.yaml refreshable-dataids: all-service.yaml profiles: active: dev
|
2.5 nacos的几个概念
命名空间(Namespace)
命名空间可用于进行不同环境的配置隔离。一般一个环境划分到一个命名空间
在config下配置namespace: 空间标识即可(一般是一串密钥)
配置分组(Group)
配置分组用于将不同的服务可以归类到同一分组。一般将一个项目的配置分到一组
配置集(Data ID)
在系统中,一个配置文件通常就是一个配置集。一般微服务的配置就是一个配置集
3. 实现服务调用的负载均衡
3.1 负载均衡概念
通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行
根据负载均衡发生位置的不同,一般分为服务端负载均衡
和客户端负载均衡
3.2 自定义实现负载均衡
通过idea再启动一个 shop-product 微服务,设置其端口为8082

利用随机数实现负载均衡
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
| @RestController @Slf4j public class OrderController { @Autowired private RestTemplate restTemplate; @Autowired private OrderService orderService; @Autowired private DiscoveryClient discoveryClient; @GetMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);
List<ServiceInstance> instances = discoveryClient.getInstances("service-product"); int index = new Random().nextInt(instances.size()); ServiceInstance instance = instances.get(index);
Product product = restTemplate.getForObject("http://" + instance.getHost() + ":" + instance.getPort() + "/product/" + pid, Product.class);
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1);
orderService.createOrder(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order; }
|
3.3 基于Ribbon实现负载均衡
Ribbon是Spring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡
在RestTemplate 的生成方法上添加@LoadBalanced注解
1 2 3 4 5 6 7 8 9 10 11 12 13
| @SpringBootApplication @EnableDiscoveryClient public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class,args); } @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new 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 25 26 27 28 29 30 31 32 33 34 35 36 37
| @RestController @Slf4j public class OrderController { @Autowired private RestTemplate restTemplate; @Autowired private OrderService orderService; @Autowired private DiscoveryClient discoveryClient; @GetMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid); String url = "service-product"; Product product = restTemplate.getForObject("http://" + url + "/product/" + pid, Product.class); log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1);
orderService.createOrder(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order; } }
|
3.4 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
| service-product: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
|
问题
由于使用了RestTemplate固定了地址的写法,存在一些问题
- 代码可读性不好
- 编程风格不统一
3.5 基于Feign实现服务调用
Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务 一样简单,只需要创建一个接口并添加一个注解即可。
Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果。
Feign使用的基本步骤
- 引入依赖
- 在主类上添加Fegin的注解@EnableFeignClients
- 创建一个service,并使用Fegin实现微服务调用
引入依赖
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
|
在主类上添加Fegin的注解@EnableFeignClients
1 2 3 4 5 6 7 8 9 10 11 12 13
| @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class,args); } @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
|
创建一个service,并使用Fegin实现微服务调用
1 2 3 4 5 6
| @FeignClient("service-product") public interface ProductService {
@GetMapping(value = "/product/{pid}") Product findByPid(@PathVariable("pid") Integer pid); }
|
调用
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
| @RestController @Slf4j public class OrderController { @Autowired private OrderService orderService; @Autowired private ProductService productService; @GetMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);
Product product = productService.findByPid(pid);
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1);
orderService.save(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order; } }
|
4. Sentinel–服务容错
4.1 高并发带来的问题
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用
如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
4.2 模拟高并发场景
服务端模拟网络延迟
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
| @RestController @Slf4j public class OrderController { @Autowired private OrderService orderService; @Autowired private ProductService productService; @GetMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);
Product product = productService.findByPid(pid);
try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); }
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order)); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return order; }
@GetMapping("/order/message") public String message() { return "测试高并发"; } }
|
修改配置文件tomcat的并发数
1 2 3 4
| server: port: 8091 tomcat: max-threads: 10
|
使用jmeter进行压力测试
下载地址https://jmeter.apache.org/
修改配置,并启动软件
进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN,然后点击jmeter.bat 启动软件
添加线程组
配置线程并发数
设置线程数20 循环100次
添加Http取样
配置取样,并启动测试
访问message方法观察效果
结论:
此时会发现, 由于order方法囤积了大量请求, 导致message方法的访问出现了问题,这就是服务雪崩的雏形
4.3 服务雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用
如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是 服务故障的“雪崩效应”
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽
我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"
4.4 常见容错方案与组件
常见的容错思路有隔离、超时、限流、熔断、降级
隔离
它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。
常见的隔离方式有:线程池隔离和信号量隔离
超时
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程
限流
限流就是限制系统的输入和输出流量已达到保护系统的目的
为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的
熔断
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用
这种牺牲局部,保全整体的措施就叫做熔断
服务熔断一般有三种状态:
熔断关闭状态(Closed)
服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
熔断开启状态(Open)
后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法
半熔断状态(Half-Open)
尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率
如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
降级
降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案
常见的容错组件
Hystrix
Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止 级联失败,从而提升系统的可用性与容错性
Resilience4J
Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推 荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和 prometheus等多款主流产品进行整合
Sentinel
Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定
三个组件在各方面的对比
|
Sentinel |
Hystrix |
resilience4j |
隔离策略 |
信号量隔离(并发线程数限流) |
线程池隔离/信号量隔离 |
信号量隔离 |
熔断降级策略 |
基于响应时间、异常比率、异常数 |
基于异常比率 |
基于异常比率、响应时间 |
实时统计实现 |
滑动窗口(LeapArray) |
滑动窗口(基于 RxJava) |
Ring Bit Buffer |
动态规则配置 |
支持多种数据源 |
支持多种数据源 |
有限支持 |
扩展性 |
多个扩展点 |
插件的形式 |
接口的形式 |
基于注解的支持 |
支持 |
支持 |
支持 |
限流 |
基于 QPS,支持基于调用关系的限流 |
有限的支持 |
Rate Limiter |
流量整形 |
支持预热模式、匀速器模式、预热排队模式 |
不支持 |
简单的 Rate Limiter 模式 |
系统自适应保护 |
支持 |
不支持 |
不支持 |
控制台 |
提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 |
简单的监控查看 |
不提供控制台,可对接其它监控系统 |
4.5 Sentinel概念和功能
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错
的综合性解决方案
它以流量为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性
流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据
任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的
我们需要根据系统的处理能力对流量进行控制
Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状
熔断降级
当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障
Sentinel 对这个问题采取了两种手段
通过并发线程数进行限制
Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响
当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积
当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝
堆积的线程完成任务后才开始继续接收请求
通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源
当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复
Sentinel 和 Hystrix 的区别
两者的原则是一致的, 都是当一个资源出现问题时, 让其快速失败, 不要波及到其它服务 但是在限制的手段上, 确采取了完全不一样的方法:
Hystrix 采用的是线程池隔离的方式, 优点是做到了资源之间的隔离, 缺点是增加了线程 切换的成本。
Sentinel 采用的是通过并发线程的数量和响应时间来对资源做限制
系统负载保护
Sentinel 同时提供系统维度的自适应保护能力
当系统负载较高的时候,如果还持续让请求进入可能会导致系统崩溃,无法响应
在集群环境下,会把本应这台机器承载的流量转发到其它的机器上去
如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
Sentinel 具有以下特征
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景
例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用 应用等
完备的实时监控:Sentinel 提供了实时的监控功能
通过控制台可以看到接入应用的单台机器秒级数据, 甚至 500 台以下规模的集群的汇总运行情况
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块
例如与 Spring Cloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel
完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口
您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等
Sentinel 分为两个部分
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等 应用容器。
4.6 微服务集成Sentinel
安装Sentinel控制台
下载jar包,解压到文件夹
https://github.com/alibaba/nacos/releases
启动控制台
1 2
| # 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目) java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
|
引入依赖即可
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
|
修改配置文件,加入控制台配置
1 2 3 4 5 6
| spring: cloud: sentinel: transport: port: 9999 dashboard: localhost:8080
|
通过浏览器访问localhost:8080 进入控制台 ( 默认用户名密码是 sentinel/sentinel )
4.7 Sentinel规则
4.7.1 流控规则
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标
当达到指定的阈值时 对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性
资源名:唯一名称,默认是请求路径,可自定义
针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制
阈值类型/单机阈值:
- QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流
- 线程数:当调用该接口的线程数达到阈值的时候,进行限流
是否集群:暂不需要集群
直接流控模式
直接流控模式是最简单的模式,当指定的接口达到限流条件时开启限流
关联流控模式
关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流
链路流控模式
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流
它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细
在ServiceImpl中对指定的方法加上@SentinelResource注解让Sentinel识别
1 2 3 4
| @SentinelResource("message") public void message() { System.out.println("message"); }
|
多个requestMapping需要调用时单独可以设置入口requstMapping的规则
配置流控效果
- 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
- Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的 1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景
- 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃
4.7.2 降级规则
降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:
平均响应时间
当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态
如果接下来 1s 内持续进入 5 个请求,它们的 RT都持续超过这个阈值
那么在接下的时间窗口 (以 s 为单位)之内,就会对这个方法进行服务降级
注意
: Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要 变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
异常比例
当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态
即在接下的时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回
异常比率的阈值范围是 [0.0, 1.0]
异常数
当资源近 1 分钟的异常数目超过阈值之后会进行服务降级
注意由于统计时间窗口是分钟级别的,若时间窗口小于 60s,则结束熔断状态后仍可能再进入熔断状态
4.7.3 热点规则
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上
1 2 3 4 5
| @GetMapping("/order/message3") @SentinelResource("message3") public String message3(String name,Integer age) { return "message3"+name+age; }
|
参数索引表示对第几个参数进行限流(从0开始)
在热点规则里面还可以额外限制参数规则
4.7.4 授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行
这时候可以使用 Sentinel 的来源访问控制的功能
来源访问控制根据资源的请求来源(origin)限制资源是否通过:
- 若配置白名单,则只有请求来源位于白名单内时才可通过
- 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过
流控应用
其实这个位置要填写的是来源标识,Sentinel提供了 RequestOriginParser 接口来处理来源
只要Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析访问来源
比如:
获取serviceName的信息,对其进行判定
如果选择黑名单,流控应用填上pc那么当传递参数里面有serviceName=pc时就无法访问
1 2 3 4 5 6 7 8
| @Component public class RequestOriginParserDefinition implements RequestOriginParser{ @Override public String parseOrigin(HttpServletRequest request) { String serviceName = request.getParameter("serviceName"); return serviceName; } }
|
4.7.5 系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU 使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效
Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护
系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般 是 CPU cores * 2.5
RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒
线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护
入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护
CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护
4.8 自定义异常返回
实现UrlBlockHandle添加到容器即可
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
| @Component public class ExceptionHandlerPage implements UrlBlockHandler {
@Override public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException { httpServletResponse.setContentType("application/json;charset=utf-8"); ResponseData responseData=null; if (e instanceof FlowException){ responseData = new ResponseData(-1, "接口被限流了"); }else if (e instanceof DegradeException){ responseData = new ResponseData(-2, "接口被降级了"); } httpServletResponse.getWriter().write(JSON.toJSONString(responseData)); } } @Data @AllArgsConstructor @NoArgsConstructor class ResponseData{ private int code; private String message; }
|
4.9 @SentinelResource的使用
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护
同时还能通过@SentinelResource来指定出现异常时的处理策略
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。其主要参数如下:
属性 |
作用 |
value |
资源名称 |
entryType |
entry类型,标记流量的方向,取值IN/OUT,默认是OUT |
blockHandler |
处理BlockException的函数名称,函数要求: 1. 必须是 public 2.返回类型 参数与原方法一致 3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 blockHandlerClass ,并指定blockHandlerClass里面的方法 |
blockHandlerClass |
存放blockHandler的类,对应的处理函数必须static修饰 |
fallback |
用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求: 1. 返回类型与原方法一致 2. 参数类型需要和原方法相匹配 3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定fallbackClass里面的方法 |
fallbackClass |
存放fallback的类。对应的处理函数必须static修饰 |
defaultFallback |
用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求: 1. 返回类型与原方法一致 2. 方法参数列表为空,或者有一个 Throwable 类型的参数 3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定 fallbackClass 里面的方法 |
exceptionsToIgnore |
指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入 fallback逻辑,而是原样抛出 |
exceptionsToTrace |
需要trace的异常 |
限流和降级后的处理方法定义在方法中
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
| @Service @Slf4j public class OrderServiceImpl3 { int i = 0;
@SentinelResource( value = "message",//定义资源名称 blockHandler = "blockHandler",//指定发生BlockException时进入的方法 fallback = "fallback"//指定发生Throwable时进入的方法 ) public void message() { i++; if (i % 3 == 0) { throw new RuntimeException(); } System.out.println("message"); }
public String blockHandler(BlockException ex) { log.error("{}", ex); return "接口被限流或者降级了..."; }
public String fallback(Throwable throwable) { log.error("{}", throwable); return "接口发生异常了..."; } }
|
限流和降级方法外置到单独的类中
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
| @Service @Slf4j public class OrderServiceImpl3 { int i = 0;
@SentinelResource( value = "message",//定义资源名称 blockHandlerClass = OrderServiceImpl3BlockHandlerClass.class, blockHandler = "blockHandler",//指定发生BlockException时进入的方法 fallbackClass = OrderServiceImpl3FallbackClass.class, fallback = "fallback"//指定发生Throwable时进入的方法 ) public void message() { i++; if (i % 3 == 0) { throw new RuntimeException(); } System.out.println("message"); } }
@Slf4j public class OrderServiceImpl3BlockHandlerClass { public static String blockHandler(BlockException ex) { log.error("{}", ex); return "接口被限流或者降级了..."; } }
@Slf4j public class OrderServiceImpl3FallbackClass { public static String fallback(Throwable throwable) { log.error("{}", throwable); return "接口发生异常了..."; } }
|
4.10 Sentinel规则持久化
通过Dashboard来为每个Sentinel客户端设置各种各样的规则
但是这里有一个问题,就是这些规则默认是存放在内存中,极不稳定,所以需要将其持久化
本地文件数据源会定时轮询文件的变更,读取规则
这样我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则
以本地文件数据源为例,推送过程如下图所示

首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的 规则保存到本地的文件中
1.编写处理类
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
| @Component public class FilePersistence implements InitFunc {
@Value("${spring.application.name}") private String application; public void setApplication(String name){ application=name; } @Override public void init() throws Exception { String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + application; String flowRulePath = ruleDir + "/flow-rule.json"; String degradeRulePath = ruleDir + "/degrade-rule.json"; String systemRulePath = ruleDir + "/system-rule.json"; String authorityRulePath = ruleDir + "/authority-rule.json"; String paramFlowRulePath = ruleDir + "/param-flow-rule.json"; this.mkdirIfNotExits(ruleDir); this.createFileIfNotExits(flowRulePath); this.createFileIfNotExits(degradeRulePath); this.createFileIfNotExits(systemRulePath); this.createFileIfNotExits(authorityRulePath); this.createFileIfNotExits(paramFlowRulePath); ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>( flowRulePath, flowRuleListParser ); FlowRuleManager.register2Property(flowRuleRDS.getProperty()); WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>( flowRulePath, this::encodeJson ); WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS); ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>( degradeRulePath, degradeRuleListParser ); DegradeRuleManager.register2Property(degradeRuleRDS.getProperty()); WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>( degradeRulePath, this::encodeJson ); WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS); ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>( systemRulePath, systemRuleListParser ); SystemRuleManager.register2Property(systemRuleRDS.getProperty()); WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>( systemRulePath, this::encodeJson ); WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS); ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>( authorityRulePath, authorityRuleListParser ); AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty()); WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>( authorityRulePath, this::encodeJson ); WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS); ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>( paramFlowRulePath, paramFlowRuleListParser ); ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty()); WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>( paramFlowRulePath, this::encodeJson ); ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS); }
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<FlowRule>>() { } ); private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<DegradeRule>>() { } ); private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<SystemRule>>() { } ); private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<AuthorityRule>>() { } ); private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<ParamFlowRule>>() { } );
private void mkdirIfNotExits(String filePath) throws IOException { File file = new File(filePath); if (!file.exists()) { file.mkdirs(); } }
private void createFileIfNotExits(String filePath) throws IOException { File file = new File(filePath); if (!file.exists()) { file.createNewFile(); } }
private <T> String encodeJson(T t) { return JSON.toJSONString(t); } }
|
2.添加配置
在resources下创建配置目录 META-INF/services ,然后添加文件 com.alibaba.csp.sentinel.init.InitFunc
在文件中添加配置类的全路径
1
| cn.jyw.shop.order.config.FilePersistence
|
4.11 Feign整合Sentinel(容错类)
远程调用出现问题时使用的类
步骤:
- 引入依赖
- 配置文件
- 创建容错类
- 指定容错类
引入依赖
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
|
配置文件中开启Feign对Sentine的支持
1 2 3
| feign: sentinel: enabled: true
|
创建容错类
1 2 3 4 5 6 7 8 9 10
| @Component public class ProductServiceFallBack implements ProductService { @Override public Product findByPid(Integer pid) { Product product = new Product(); product.setPid(-1); return product; } }
|
为被容器的接口指定容错类
1 2 3 4 5 6 7 8
|
@FeignClient(value = "service-product",fallback = ProductServiceFallBack.class) public interface ProductService {
@GetMapping(value = "/product/{pid}") Product findByPid(@PathVariable("pid") Integer pid); }
|
修改controller
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
| @GetMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);
Product product = productService.findByPid(pid); if (product.getPid() == -1) { Order order = new Order(); order.setPname("下单失败"); return order; }
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1);
orderService.save(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order; }
|
停止所有 shop-product 服务,重启 shop-order 服务,访问请求,观察容错效果
容错工厂类
能获取容错时产生的异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Component public class ProductServiceFallBack implements FallbackFactory<ProductService> { @Override public ProductService create(Throwable throwable) { return new ProductService() { @Override public Product findByPid(Integer pid) { throwable.printStackTrace(); Product product = new Product(); product.setPid(-1); return product; } }; } }
|
对应的接口上的注解由fallback变成fallbackFactory
注意: fallback和fallbackFactory只能使用其中一种方式