目录
什么是雪崩问题?
1、微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。
如何避免因瞬间高并发流量而导致服务故障?1、流量控制
如何避免因服务故障引起的雪崩问题?1、超时处理
2、线程隔离3、降级熔断
流控模式有哪些?
1、直接:对当前资源限流
2、关联:高优先级资源触发阈值,对低优先级资源限流。3、链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流
流控效果有哪些?
1、快速失败:QPS超过阈值时,拒绝新的请求
2、warm up:QPS超过阈值时,拒绝新的请求;QPS阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机。
3、排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝
Sentinel支持的雪崩解决方案:
1、线程隔离(仓壁模式)
2、降级熔断
Feign整合Sentinel的步骤:
1、在application.yml中配置:feign.sentienl.enable=true
2、给Feignclient编写FallbackFactory并注册为Bean
3、将FallbackFactory配置到FeignClient
线程隔离的两种手段是?
1、信号量隔离
2、线程池隔离
信号量隔离的特点是?
1、基于计数器模式,简单,开销小
线程池隔离的特点是?
1、基于线程池模式,有额外开销,但隔离控制更强
sentinel熔断降级的策略有哪些?
1、慢调用比例:超过指定时长的调用为慢调用,统计单位时长内慢调用的比例,超过阈值则熔断
2、异常比例:统计单位时长内异常调用的比例,超过阈值则熔断
3、常数:统计单位时长内异常调用的次数,超过阈值则熔断
获取请求来源的接口是什么?
1、Request0riginParser
处理BlockException的接口是什么?1、BlockExceptionHandler
sentinel的三种配置管理模式是什么?
1、原始模式:保存在内存
2、pull模式:存在本地文件或数据库,定时去读取3、push模式:保存在nacos,监听变更实时更新
微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
解决雪崩问题的常见方式有四种:?
1、超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待2、舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
3、熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。4、流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。
Sentinel是阿里巴巴开源的一款微服务流量控制组件。
Sentinel具有以下特征:
1、丰富的应用场景:Sentinel承接了阿里巴巴近10年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
2、完备的实时监控:Sentinel同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至500台以下规模的集群的汇总运行情况。
3、广泛的开源生态:Sentinel提供开箱即用的与其它开源框架/库的整合模块,例如与Spring Cloud、Dubbo、gRPC的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
4、完善的SPI扩展点:Sentinel提供简单易用、完善的SPI扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
将该文件下载并拷贝到一个非中文路径的目录
sentinel-dashboard-1.8.1https://pan.baidu.com/s/13FRnAXR7QTN5BexD5Zb5qQ?pwd=gil1
在该目录下cmd进入控制台运行以下命令
java -jar sentinel-dashboard-1.8.1.jar
然后访问localhost:8080,输入账号和密码,都是sentinel
如果要修改sentinel的默认端口、账号、密码,可以通过下列配置:
该命令启动时,使用的端口就成了8090了
java -jar sentinel-dashboard-1.8.1.jar -Dserver.port=8090
配置项 | 默认值 | 说明 |
server.port | 8080 | 服务端口 |
sentinel.dashboard.auth.username | sentinel | 默认用户名 |
sentinel.dashboard.auth.password | sentinel | 默认密码 |
下载该代码,用以前的那个cloud-demo也可以,不过可能需要修改一些代码:
1、所有yml文件的nacos:8848改成localhost:8848
2、所有yml文件的mysql:3306改成localhost:3306
3、有一个服务的端口是8080,这会与sentinel的端口发生冲突,需要改成其他端口
cloud-demohttps://pan.baidu.com/s/1_a4YzJr6R_EsRK6Z9tievQ?pwd=7rg1
把nacos和刚下载的sentinel启动后,再把该项目的三个服务启动,否则会报错?
1、引入sentinel依赖
在order-service的pom文件,导入依赖
<!--引入sentinel依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2.2.5.RELEASE</version> </dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--eureka客户依赖-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--feign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--引入HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<!--引入feign统一的api-->
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
<!--引入sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、配置控制台地址
sentinel: transport: dashboard: localhost:8080 #sentinel控制台地址
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 1234
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice #order的微服务名称
cloud:
nacos:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080 #sentinel控制台地址
datasource:
flow:
nacos:
server-addr: localhost:8848 #nacos地址
dataId: orderservice-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow #规则类型。还可以是:degrade,authority,param-flow
# discovery:
# cluster-name: HZ #集群名称
## namespace: 8279562b-ce89-420a-b765-f8b2adfdbe49 #命名空间id
# ephemeral: false #是否为临时实例
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
#eureka:
# client:
# service-url: #eureka地址信息1
# defaultZone: http://127.0.0.1:10086/eureka
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则:随机。范围:userservice服务
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: #指定饥饿加载的服务
- userservice
#feign:
# client:
# config:
# default: #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
# loggerLevel: FULL #日志级别
feign:
httpclient:
enable: true #支持httpClient的开关
max-connections: 200 #最大连接数
max-connections-per-route: 50 #单个路径的最大连接数
3、访问微服务的任意端点,触发sentinel监控?
先访问服务的数据
再刷新查看sentinel控制台的内容
簇点链路:就是项目内的调用链路,链路中被监控的每个接口就是一个资源。默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
?
点击资源/order/[orderld}后面的流控按钮,就可以弹出表单。表单中可以添加流控规则,如下图所示:
其含义是限制/order/{orderld}这个资源的单机QPS为5,即每秒只允许5次请求,超出的请求会被拦截并报错。
例如:现在限制为5,我在一秒内访问10次,就会有5个通过,5 个拒绝
在添加限流规则时,点击高级选项,可以选择三种流控模式:
直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流
关联模式:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
使用场景:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是有限支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。
当/order/update资源访问量触发阈值时,就会对/order/query资源限流,避免影响/order/update资源。
在orderController.java添加代码
@GetMapping("/query") public String queryOrder(){ //查询订单 System.out.println("查询订单"); return "查询订单成功"; }@GetMapping("/update") public String updateOrder(){ return "更新订单成功"; }
package cn.itcast.order.web;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
//根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
@GetMapping("/query")
public String queryOrder(){
//查询订单
System.out.println("查询订单");
return "查询订单成功";
}
@GetMapping("/update")
public String updateOrder(){
return "更新订单成功";
}
}
重启orderservice,并访问localhost:8088/order/update和localhost:8088/order/query
再刷新sentinel控制台,会发现新增了:/order/update和/order/query两条资源名
给谁限流,就给谁加流控。要给query限流,就给它加流控
当/order/update资源访问量触发阈值时,就会对/order/query资源限流,避免影响/order/update资源。
?
此时会看到流控规则新增了一条
此时,每秒访问10次update,都是成功的,但此时的query被限流了,无法访问?
链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
例如:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。
步骤:
1、在orderService中添加一个queryGoods方法,不用实现业务
public void queryGoods(){ System.err.println("查询商品"); }
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用
User user = userClient.findById(order.getUserId());
// 3.封装user到order
order.setUser(user);
// 4.返回
return order;
}
public void queryGoods(){
System.err.println("查询商品");
}
// @Autowired
// private RestTemplate restTemplate;
//
// public Order queryOrderById(Long orderId) {
// // 1.查询订单
// Order order = orderMapper.findById(orderId);
// // 2.利用RestTemplate发送http请求,查询用户
// // 2.1.url路径
// String url = "http://userservice/user/" + order.getUserId();
// // 2.2.发送http请求,实现远程调用
// User user = restTemplate.getForObject(url, User.class);//第一个参数是路径,第二个参数是返回的类=类型
// // 3.封装user到order
// order.setUser(user);
// // 4.返回
// return order;
// }
}
2、在orderController中,改造/order/query端点,调用orderService中的queryGoods方法
3、在orderController中添加一个/order/save的端点,调用orderService的queryGoods方法
@GetMapping("/query") public String queryOrder(){ //查询商品 orderService.queryGoods(); //查询订单 System.out.println("查询订单"); return "查询订单成功"; }@GetMapping("/save") public String saveOrder(){ //查询商品 orderService.queryGoods(); //查询订单 System.out.println("新增订单"); return "新增订单成功"; }
package cn.itcast.order.web;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
//根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
@GetMapping("/query")
public String queryOrder(){
//查询商品
orderService.queryGoods();
//查询订单
System.out.println("查询订单");
return "查询订单成功";
}
@GetMapping("/save")
public String saveOrder(){
//查询商品
orderService.queryGoods();
//查询订单
System.out.println("新增订单");
return "新增订单成功";
}
@GetMapping("/update")
public String updateOrder(){
return "更新订单成功";
}
}
注意:因为sentinel只会监控controller的内容,但是我的controller都会访问orderService的内容,我需要监控service的内容。此时需要两步:
第一步:需要利用@SentinelResource注解
@SentinelResource("goods")
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用
User user = userClient.findById(order.getUserId());
// 3.封装user到order
order.setUser(user);
// 4.返回
return order;
}
@SentinelResource("goods")
public void queryGoods(){
System.err.println("查询商品");
}
// @Autowired
// private RestTemplate restTemplate;
//
// public Order queryOrderById(Long orderId) {
// // 1.查询订单
// Order order = orderMapper.findById(orderId);
// // 2.利用RestTemplate发送http请求,查询用户
// // 2.1.url路径
// String url = "http://userservice/user/" + order.getUserId();
// // 2.2.发送http请求,实现远程调用
// User user = restTemplate.getForObject(url, User.class);//第一个参数是路径,第二个参数是返回的类=类型
// // 3.封装user到order
// order.setUser(user);
// // 4.返回
// return order;
// }
}
第二步:?Sentinel默认会将Controller方法做context整合,导致链路模式的流控失效,需要修改application.yml添加配置
web-context-unify: false #关闭context整合
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 1234
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice #order的微服务名称
cloud:
nacos:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080 #sentinel控制台地址
web-context-unify: false #关闭context整合
datasource:
flow:
nacos:
server-addr: localhost:8848 #nacos地址
dataId: orderservice-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow #规则类型。还可以是:degrade,authority,param-flow
# discovery:
# cluster-name: HZ #集群名称
## namespace: 8279562b-ce89-420a-b765-f8b2adfdbe49 #命名空间id
# ephemeral: false #是否为临时实例
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
#eureka:
# client:
# service-url: #eureka地址信息1
# defaultZone: http://127.0.0.1:10086/eureka
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则:随机。范围:userservice服务
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: #指定饥饿加载的服务
- userservice
#feign:
# client:
# config:
# default: #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
# loggerLevel: FULL #日志级别
feign:
httpclient:
enable: true #支持httpClient的开关
max-connections: 200 #最大连接数
max-connections-per-route: 50 #单个路径的最大连接数
重启服务,再访问localhost:8088/order/query和localhost:8088/order/save,刷新sentinel控制台。
可以看到save和query变成了两个独立的资源名。这是因为刚才的第二步的原因
这两个goods其实是同一个,所以随便点一个,进行流控。
4、给queryGoods设置限流规则,从/order/query进入queryGoods的方法限制QPs必须小于2
监控从/order/query进来的goods,限制每秒2个。但是从/order/save进来的goods没有限制。
?
每秒给?/order/query和/order/save各访问4个。可以看到每秒有6个通过,2个拒绝
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
1、快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
2、warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
3、排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是threshold / coldFactor,持续指定时长后,逐渐提高到threshold值。而coldFactor的默认值是3
例如:给/order/{orderld}这个资源设置限流,最大QPs为10,利用warm up效果,预热时长为5秒
可以看到新增了一条规则
每秒访问10个,可以看到刚开始只能访问3个,其余的被拒绝,慢慢的可以访问到10个
当请求超过QPs阈值时,快速失败和warm up会拒绝新的请求并抛出异常。而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
比如:QPS=5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待超过2000ms的请求会被拒绝并抛出异常
例如:给/order/{orderld}这个资源设置限流,最大QPs为10,利用排队的流控效果,超时时长设置为5s
给上次配的warm up进行编辑,使用排队等待
可以看到warm up的规则发生了改变,变成了排队等待
每秒访问15个,可以看到,并没有出现拒绝,最多10个,因为进行了排队等待,但是响应时间挺长的
之前的限流是统计访问某个资源的所有请求,判断是否超过QPs阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPs阈值。
例如:给/order/{orderld}这个资源添加热点参数限流,规则如下:
1、默认的热点参数规则是每1秒请求量不超过2
2、给102这个参数设置例外:每1秒请求量不超过43、给103这个参数设置例外:每1秒请求量不超过10
注意:热点参数限流对默认的SpringMVC资源无效。需要加注解,起名为hot
@SentinelResource("hot")
package cn.itcast.order.web;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@SentinelResource("hot")
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
@GetMapping("/query")
public String queryOrder(){
//查询商品
orderService.queryGoods();
//查询订单
System.out.println("查询订单");
return "查询订单成功";
}
@GetMapping("/save")
public String saveOrder(){
//查询商品
orderService.queryGoods();
//查询订单
System.out.println("新增订单");
return "新增订单成功";
}
@GetMapping("/update")
public String updateOrder(){
return "更新订单成功";
}
}
重启服务,并访问localhost:8088/order/101,此时可以看到该资源名
按要求配置
每秒给三个请求发送5个,可以看到,最高的时候为11,因为一个限制了2,一个限制了4,一个限制了10,所以是2+4+5=11
虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。
SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
1、修改OrderService的application.yml文件,开启Feign的Sentinel功能
feign: sentinel: enabled: true #开启Feign对sentinel的支持
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 1234
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice #order的微服务名称
cloud:
nacos:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080 #sentinel控制台地址
web-context-unify: false #关闭context整合
datasource:
flow:
nacos:
server-addr: localhost:8848 #nacos地址
dataId: orderservice-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow #规则类型。还可以是:degrade,authority,param-flow
# discovery:
# cluster-name: HZ #集群名称
## namespace: 8279562b-ce89-420a-b765-f8b2adfdbe49 #命名空间id
# ephemeral: false #是否为临时实例
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
#eureka:
# client:
# service-url: #eureka地址信息1
# defaultZone: http://127.0.0.1:10086/eureka
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则:随机。范围:userservice服务
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: #指定饥饿加载的服务
- userservice
#feign:
# client:
# config:
# default: #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
# loggerLevel: FULL #日志级别
feign:
httpclient:
enable: true #支持httpClient的开关
max-connections: 200 #最大连接数
max-connections-per-route: 50 #单个路径的最大连接数
sentinel:
enabled: true #开启Feign对sentinel的支持
2、给FeignClient编写失败后的降级逻辑
方式一:FallbackClass,无法对远程调用的异常做处理
方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
步骤一:在feing-api的模块中的cn.itcast.feign的clients新建fallback包,新建UserClientFallbackFactory.java
package cn.itcast.feign.clients.fallback;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
@Override
public User findById(Long id) {
log.error("查询用户异常",throwable);
return new User();
}
};
}
}
步骤二:在feing-api模块中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean
@Bean public UserClientFallbackFactory userClientFallbackFactory(){ return new UserClientFallbackFactory(); }
package cn.itcast.feign.config;
import cn.itcast.feign.clients.fallback.UserClientFallbackFactory;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfiguration {
@Bean
public Logger.Level loglevel(){
return Logger.Level.BASIC;
}
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
}
步骤三:在feing-api模块中的UserClient接口中使用UserClientFallbackFactory
注意:在cloud-demo的pom文件中,找到<spring-cloud.version></spring-cloud.version>,把它写上<spring-cloud.version>Hoxton.SR8</spring-cloud.version>。不然会因为版本问题报错
package cn.itcast.feign.clients;
import cn.itcast.feign.clients.fallback.UserClientFallbackFactory;
import cn.itcast.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
重启服务,并访问localhost:8088/order/101,再刷新sentinel控制台。可以看到GET的这个链路,这就是Feign的请求路径
线程隔离有两种方式实现:
线程池隔离
优点:支持主动超时、支持异步调用
缺点:线程的额外开销比较大
场景:低扇出
信号量隔离(Sentinel默认采用)优点:轻量级、无额外开销
缺点:不支持主动超时、不支持异步调用
场景:高频调用、高扇出
在添加限流规则时,可以选择两种阈值类型:
QPS:就是每秒的请求数,在快速入门中已经演示过
线程数:是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现舱壁模式
例如:给UserClient的查询用户接口设置流控规则,线程数不能超过2。
对GET://http://userservice/user/{id}添加流控管理
?
一瞬间发送10次请求,可以看到前2个请求正常,但是后面的请求都是空而没有报错。因为UserClientFallbackFactory.java里已经写了降级逻辑,当访问被拒绝时,返回空的内容。
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,
如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器熔断策略有三种:慢调用、异常比例、异常数
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
例如:给UserClient的查询用户接口设置降级规则,慢调用的RT阈值为50ms,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5
提示:为了触发慢调用规则,我们需要修改UserService中的业务,增加业务耗时
在user-service的模块中,UserController.java里,修改为该代码
@GetMapping("/{id}") //请求头 变量名为Truth 可以不传 public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth",required = false) String truth) throws InterruptedException { if(id == 1){ //休眠,触发熔断 Thread.sleep(60); } return userService.queryById(id); }
package cn.itcast.user.web;
import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.text.DateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/user")
//@RefreshScope
public class UserController {
@Autowired
private UserService userService;
// @Value("${pattern.dateformat}")
// //注解读取该配置
// private String dateformat;
@Autowired
private PatternProperties properties;//注入新的java类
@GetMapping("prop")
public PatternProperties properties(){
return properties;
}
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));//通过该配置改变时间格式
}
/**
* 路径: /user/110
*
* @param id 用户id
* @return 用户
*/
@GetMapping("/{id}") //请求头 变量名为Truth 可以不传
public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth",required = false) String truth) throws InterruptedException {
if(id == 1){
//休眠,触发熔断
Thread.sleep(60);
}
return userService.queryById(id);
}
}
重启服务,访问localhost:8088/order/101,把以前的GET://http://userservice/user/{id}流控规则删掉。
给GET://http://userservice/user/{id}配置降级
可以看到新增的规则
一秒内请求5次localhost:8088/order/101,之后再请求localhost:8088/order/102会发现数据为空
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
例如:给UserClient的查询用户接口设置降级规则,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5S
提示:为了触发异常统计,我们需要修改UserService中的业务,抛出异常:
@GetMapping("/{id}") //请求头 变量名为Truth 可以不传 public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth",required = false) String truth) throws InterruptedException { if(id == 1){ //休眠,触发熔断 Thread.sleep(60); }else if(id == 2){ throw new RuntimeException("故意出错,触发熔断"); } return userService.queryById(id); }
package cn.itcast.user.web;
import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.text.DateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/user")
//@RefreshScope
public class UserController {
@Autowired
private UserService userService;
// @Value("${pattern.dateformat}")
// //注解读取该配置
// private String dateformat;
@Autowired
private PatternProperties properties;//注入新的java类
@GetMapping("prop")
public PatternProperties properties(){
return properties;
}
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));//通过该配置改变时间格式
}
/**
* 路径: /user/110
*
* @param id 用户id
* @return 用户
*/
@GetMapping("/{id}") //请求头 变量名为Truth 可以不传
public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth",required = false) String truth) throws InterruptedException {
if(id == 1){
//休眠,触发熔断
Thread.sleep(60);
}else if(id == 2){
throw new RuntimeException("故意出错,触发熔断");
}
return userService.queryById(id);
}
}
重启服务,访问localhost:8088/order/101,把以前的GET://http://userservice/user/{id}流控规则删掉。
给GET://http://userservice/user/{id}配置降级
访问5次localhost:8088/order/102,触发熔断规则,再访问localhost:8088/order/103,会发现数据也为空
?
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
1、白名单:来源(origin)在白名单内的调用者允许访问
2、黑名单:来源(origin)在黑名单内的调用者不允许访问
例如,我们尝试从request中获取一个名为origin的请求头,作为origin的值
在order-service模块中的cn.itcast.order中新增一个sentinel包,新增HeaderOriginParser.java
package cn.itcast.order.sentinel;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import io.netty.util.internal.StringUtil;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
//1、获取请求头
String origin = request.getHeader("origin");
//2、非空判断
if(StringUtils.isEmpty(origin)){
origin = "blank";
}
return origin;
}
}
需要在gateway服务中,利用网关的过滤器添加名为gateway的origin头
- AddRequestHeader=origin,gateway #添加情求头
server:
port: 10010 #网关端口
spring:
application:
name: gateway #服务名称
cloud :
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
routes: #网关路由配置
- id: user-service #路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 #路由的目标地址 http就是固定地址
uri: lb://userservice #路由的目标地址lb就是负载均衡,后面跟服务名称
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** #这个是按照路径匹配,只要以/user/开头就符合要求
# filters:
# - AddRequestHeader=Truth,Itcast is freaking awesome! #添加请求头
- id: order-service #路由id,自定义,只要唯一即可
uri: lb://orderservice #路由的目标地址lb就是负载均衡,后面跟服务名称
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/order/** #这个是按照路径匹配,只要以/user/开头就符合要求
# - After=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] #要求访问时间在这个之后
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] #要求访问时间在这个之后
default-filters: #默认过滤器,会对所有的路由请求都生效
- AddRequestHeader=Truth,Itcast is freakina awesome! #添加情求头
- AddRequestHeader=origin,gateway #添加情求头
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
重启服务,访问localhost:8088/order/101给/order/{orderld}配置授权规则
访问?localhost:8088/order/103报错,访问localhost:8088/order/103?authorization=admin成功
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口
在order-service模块中的cn.itcast.order中的sentinel包,新增SentinelExceptionHandler.java
package cn.itcast.order.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
重启服务,?访问?localhost:8088/order/103,给/order/{orderId}添加流控规则
访问2次localhost:8088/order/103,出现限流
把刚配的限流规则删掉,新增授权规则
?
这次为没有权限?
sentinel的控制台规则管理有三种模式:
1、原始模式:Sentinel的默认模式,将规则保存在内存,重启服务会丢失
2、pull模式
3、push模式
pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。
push模式:控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。
push模式实现最为复杂,依赖于nacos,并且需要修改Sentinel控制台源码。
1、引入依赖
在order-service中引入sentinel监听nacos的依赖
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--eureka客户依赖-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--feign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--引入HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<!--引入feign统一的api-->
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
<!--引入sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置nacos地址
在order-service中的application.yml文件配置nacos地址及监听的配置信息
spring: cloud: sentinel: datasource: flow: nacos: server-addr: localhost:8848 #nacos地址 dataId: orderservice-flow-rules groupId: SENTINEL_GROUP rule-type: flow #规则类型。还可以是:degrade,authority,param-flow
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 1234
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice #order的微服务名称
cloud:
nacos:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080 #sentinel控制台地址
web-context-unify: false #关闭context整合
datasource:
flow:
nacos:
server-addr: localhost:8848 #nacos地址
dataId: orderservice-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow #规则类型。还可以是:degrade,authority,param-flow
# discovery:
# cluster-name: HZ #集群名称
## namespace: 8279562b-ce89-420a-b765-f8b2adfdbe49 #命名空间id
# ephemeral: false #是否为临时实例
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
#eureka:
# client:
# service-url: #eureka地址信息1
# defaultZone: http://127.0.0.1:10086/eureka
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则:随机。范围:userservice服务
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: #指定饥饿加载的服务
- userservice
#feign:
# client:
# config:
# default: #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
# loggerLevel: FULL #日志级别
feign:
httpclient:
enable: true #支持httpClient的开关
max-connections: 200 #最大连接数
max-connections-per-route: 50 #单个路径的最大连接数
sentinel:
enabled: true #开启Feign对sentinel的支持
重启服务
下载该文件,并把它放到跟sentinel-dashboard-1.8.1同一目录下
sentinel-dashboard.jarhttps://pan.baidu.com/s/1MLvwJiHHrntl9vals3yFNQ?pwd=egh4
在该目录下cmd进入控制台运行以下命令
java -jar -Dnacos.addr=localhost:8848 sentinel-dashboard.jar
访问loaclhost:8848,进入nacos,访问localhost:8088/order/103,?刷新sentinel控制台,可以看到新的列表
在这里新增的流控规则会作用到nacos,点新增
?
此时刷新nacos,可以看到新的配置
访问localhost:8088/order/103,发现成功限流
再重启服务,看sentinel控制台的规则是否被重置
可以看到规则还在,规则持久化完成?
代码文件,点击下载https://pan.baidu.com/s/1sdroaa2GxZ_L3nq9BKqLzQ?pwd=i0et
?