文章目录
1.Sentinel基本介绍
1.1.Sentinel 是什么
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的流量控制框架。它以流量为切入点, 从流量控制、熔断降级等多个维度来保护服务的稳定性。
1.2.Sentinel的主要功能
Sentinel的主要功能就是容错,主要体现为下面这两个方面:
-
流量控制
-
监控QPS:调用该接口的QPS达到阈值的时候进行控制
(不被上游服务压垮)
-
监控线程数:给每个接口分配独立线程数,出现故障时能将问题隔离在内部而不扩散
(不被下游服务拖垮)
-
-
熔断降级
某个资源出现慢调用比例或异常比例超出阈值的时候,则暂时切断对下游服务的调用,避免级联故障
总之一句话,我们需要做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能:
1.3.Sentinel 的历史
- 2012 年,Sentinel 诞生,主要功能为入口流量控制。
- 2013-2017 年,Sentinel 在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel 也因此积累了大量的流量归整场景以及生产实践。
- 2018 年,Sentinel 开源,并持续演进。
- 2019 年,Sentinel 朝着多语言扩展的方向不断探索,推出 C++ 原生版本。
- 2020 年,推出 Sentinel Go 版本,继续朝着云原生方向演进。
2.Sentinel入门
2.1.抛异常方式
sentinel_consumer
> **pom.xml**
<!--sentinel核心依赖-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>
controller
@RequestMapping(value = "/hello")
public String hello() {
Entry entry = null;
try {
//SphU:执行规则检查,获取资源失败时会抛BlockException异常
entry = SphU.entry("/consumer/hello");
return "Hello Sentienl!!!";//被保护的逻辑
} catch (BlockException e) {
//资源访问被阻止
e.printStackTrace();
return "接口被限流了, exception: " + e;
}finally {
// SphU.entry(xxx) 需要与 entry.exit() 成对出现,否则会导致调用链记录异常
if (entry != null) {
entry.exit();
}
}
}
/**
* 定义限流/流控规则
*/
@PostConstruct//当前类的构造函数执行之后执行
public void initFlowQpsRule() {
//1.创建存放限流规则的集合
List<FlowRule> rules = new ArrayList<FlowRule>();
//2.创建限流规则
FlowRule rule1 = new FlowRule();
rule1.setResource("/consumer/hello");//定义资源,名称唯一
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);//定义限流规则类型
rule1.setCount(2); // QPS控制在2以内
//3.将限流规则存放到集合中
rules.add(rule1);
//4.加载限流规则
FlowRuleManager.loadRules(rules);
}
测试
- 正常访问:http://127.0.0.1/consumer/hello
- 高并发访问:http://127.0.0.1/consumer/hello
2.2.注解方式
上述通过try-catch
风格的API可以实现限流,但是对代码侵入性太高,推荐使用注解的方式来实现。
sentinel_consumer
pom.xml
<!--sentinel注解依赖-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>
controller
@RequestMapping(value = "/hello2")
@SentinelResource(value="/consumer/hello2",blockHandler = "blockHandlerMethod")
public String hello2() {
return "Hello Sentienl2!!!";//被保护的逻辑
}
/**
*资源访问被阻止的兜底方法
*/
public String blockHandlerMethod(BlockException e){
return "接口被限流了, exception: " + e;
}
@PostConstruct
public void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource("/consumer/hello2");//注意修改资源名称
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setCount(2);
rules.add(rule1);
FlowRuleManager.loadRules(rules);
}
测试
- 正常访问:http://127.0.0.1/consumer/hello2
- 高并发访问:http://127.0.0.1/consumer/hello2
2.3.Sentinel的安装和启动
Sentinel 提供一个轻量级的控制台, 它提供资源实时监控以及规则管理等功能。
- 下载地址:https://github.com/alibaba/Sentinel/releases
- 执行命令:
java -jar sentinel-dashboard-1.8.1.jar
- 浏览器访问:http://localhost:8080,默认账号密码:sentinel/sentinel
2.4.Sentinel接入控制台
sentinel_consumer
pom.xml
<!--sentinel核心依赖-->
<!--<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>-->
<!--sentinel注解依赖-->
<!--<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>-->
<!-- sentinel启动器 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
application.yml
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080 #指定sentinel的地址
controller
/**
*被保护的逻辑
*/
@RequestMapping(value = "/hello3")
public String hello3() {
return "Hello Sentienl3!!!";
}
测试
- 流量监控:sentinel是懒加载,需要浏览器访问:http://127.0.0.1/consumer/hello3
- 新增流控规则
- 高并发访问:http://127.0.0.1/consumer/hello3
3.Sentinel流量控制
3.1流控规则说明
- 资源名:唯一名称,默认请求路径
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,指定对哪个微服务进行限流 ,默认default(不区分来源,全部限制)
- 阈值类型:
- QPS:当调用该接口的QPS达到了阈值的时候,进行限流;
- 线程数:当调用该接口的线程数达到阈值时,进行限流
- 单机阈值:次数
- 是否集群:不需要集群
- 流控模式:
- 直接:接口达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)[api级别的针对来源]
- 流控效果
- 快速失败:直接失败
- Warm Up:即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值
- 排队等待
3.2.流控规则
3.2.1.阈值类型
(1).QPS
-
说明
QPS:代表每秒的访问次数,只要访问次数到达一定的阈值,则进行限流操作
-
案例
- 新增流控规则
- 浏览器高并发访问:http://127.0.0.1/consumer/getUserById/1
(2).线程数
-
说明
线程数:代表的是每秒内访问该api接口的线程数,如果该接口的操作比较长,当排队的线程数到达阈值的时候,进行限流操作,相反的如果接口的操作很快,即是每秒内的操作很快,同样不会进行限流操作
-
案例
1.新增流控规则
2.jmeter模拟高并发
3.浏览器访问:http://127.0.0.1/consumer/getUserById/1
3.2.2.流控模式
(1).直接
-
说明
对当前资源的流量控制
(2).关联
-
说明
当关联的资源达到阈值时,限流自己。如支付接口达到阈值时限流订单接口。
-
案例
- 新增关联资源
@RequestMapping(value="/test")
public String test(){
return "test";
}
- 新增流控规则
- jmeter模拟高并发:http://127.0.0.1:/consumer/test
- 浏览器访问:http://127.0.0.1/consumer/getUserById/1
3.2.3.流控效果
(1).快速失败
-
说明
限流的时候直接提示
(2).warm up
-
说明
即QPS 从 阈值 / 3 开始,经预热时长逐渐升至设定的 QPS 阈值,下图中刚开始的时候阈值只有3,当经过5s后,阈值才慢慢提高到9;这种情况主要是为了保护系统,例如在秒杀系统;
-
案例
- 新增流控规则
- 5秒前访问:http://127.0.0.1/consumer/getUserById/1
- 5秒后访问:http://127.0.0.1/consumer/getUserById/1
(3).排队等待
-
说明
即/consumer/getUserById/1每秒1次请求,超过的话就排队等待,等待的超时时间为10ms,目的是为了匀速处理请求,保证服务的均匀性,而不是一会处理大量的请求,一会有没有请求可处理。
-
案例
- 新增流控规则
- 打印请求时间
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
//打印请求时间
System.out.println("排队等待效果:"+new Date());
return userFeign.getUserById(id);
}
- 高并发访问:http://127.0.0.1/consumer/getUserById/1
3.3.热点规则
3.3.1.什么是热点参数流控
-
热点即经常访问的数据,比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
-
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上。
3.3.2.实现流控
- 添加参数
@RequestMapping(value = "/getUserById/{id}")
//不加@SentinelResource注解注解则不会触发热点参数流控
@SentinelResource(value = "getUserById", blockHandler = "blockHandlerMethod")
public User getUserById(@PathVariable Integer id) {
return userFeign.getUserById(id);
}
//资源访问被阻止的备选逻辑
public User blockHandlerMethod(Integer id, BlockException e){
return new User(id,"this is blockHandlerMethod",0);
}
- 新增流控规则
- 高并发访问:http://127.0.0.1/consumer/getUserById/1
**注意:**使用@SentinelResource
注解时,sentinel不在使用默认行为
3.3.3.参数例外项
- 新增流控规则
- 5秒内超过2次访问:http://127.0.0.1/consumer/getUserById/2
- 5秒内超过3次访问:
3.4.系统规则
3.4.1.什么是系统规则
系统规则和流控规则不一样,流控规则是针对方法设定的,系统规则是针对一个应用设定的;发生系统规则中配置的情况的时候,会把整个应用都断掉,所有的接口对不能对外提供服务了,这个设计很少用,因为粒度太大了,用 Sentinel 一般都是做细粒度的维护,如果设置了系统规则,可能自己都不知道怎么回事,系统就用不了了;
3.4.2.系统限流的 5 中预置类型
- LOAD:只有在 Linux 系统的机器上才会生效,可以根据当前操作系统的负载,来决定是否触发保护;
- RT:这个应用上,所有请求的平均响应时间,如果超过某个值,就停止新的请求;
- 线程数:这个应用上,所有的请求消耗的线程数加起来,如果超过某个值,就停止新的请求;
- 入口 QPS:这个应用上,所有接口的 QPS 加起来,如果超过某个值,就停止新的请求;
- CPU 使用率:CPU 的使用率,如果超过一个百分比,就停止新的请求;
3.4.3.实现流控
- 新增流控规则
- 高并发访问:http://127.0.0.1/consumer/getUserById/1
3.5.授权规则
3.5.1.什么是授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
-
若配置白名单,则只有请求来源位于白名单内时才可通过;
-
若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。
3.5.2.实现流控
- 获取来源标识
package com.bjpowernode.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
//sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求头资源的
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
/**
* Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析访问来源。
* @param request
* @return
*/
@Override
public String parseOrigin(HttpServletRequest request) {
//当前 来源标识 放在了请求参数里面,可以放到的地方有很多,比如参数/请求头/session/等等
String origin = request.getParameter("origin");
System.out.println("origin:"+origin);
return origin;
}
}
- 新增流控规则
- 黑名单规则:
- 白名单规则:
4.Sentinel熔断降级
4.1.熔断规则说明
熔断规则就是设置当满足什么条件的时候,暂时切断对下游服务的调用。那么怎么去判断资源是否处于稳定状态呢?
- Sentinel提供了三个衡量条件:
- 慢调用比例
属性 | 说明 |
---|---|
最大RT | 需要设置的阈值,超过该值则为慢调用 |
比例阈值 | 慢调用占所有的调用的比率,范围:[0~1] |
熔断时长 | 在这段时间内发生熔断、拒绝所有请求 |
最小请求数 | 即允许通过的最小请求数,在该数量内不发生熔断 |
- 异常比例
属性 | 说明 |
---|---|
异常比例阈值 | 异常比例=发生异常的请求数÷请求总数取值范围:[0~1] |
熔断时长 | 在这段时间内发生熔断、拒绝所有请求 |
最小请求数 | 即允许通过的最小请求数,在该数量内不发生熔断 |
- 异常数
属性 | 说明 |
---|---|
异常数 | 请求发生异常的数量 |
熔断时长 | 在这段时间内发生熔断、拒绝所有请求 |
最小请求数 | 即允许通过的最小请求数,在该数量内不发生熔断 |
- 服务熔断状态切换:
4.2.慢调用比例
- 新增熔断策略
- 控制请求时间
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
System.out.println("熔断降级效果:"+new Date());
//控制请求时间
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userFeign.getUserById(id);
}
- 高并发访问:http://127.0.0.1:9091/consumer/getUserById/1
- 两次访问间隔5秒
4.3.异常比例
- 新增熔断策略
- 请求报异常
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
System.out.println("熔断降级效果:"+new Date());
//请求报异常
int a = 6/0;
return userFeign.getUserById(id);
}
- 浏览器访问:http://127.0.0.1:9091/consumer/getUserById/1
- 熔断后两次访问间隔5秒
4.4.异常数
- 新增熔断策略
- 请求报异常
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
System.out.println("熔断降级效果:"+new Date());
//请求报异常
int a = 6/0;
return userFeign.getUserById(id);
}
- 浏览器访问5次以上:http://127.0.0.1:9091/consumer/1
- 熔断后两次访问间隔60秒
5.Sentinel的blockHandler
5.1.blockHandler
当服务经过sentinel流控或熔断的时候,使用的是Sentinel默认的兜底方法,但是系统默认的方法并没有体现我们的业务要求。
所以Sentinel的@SentinelResource
提供了blockHandler
属性,让我们可以另外定义一个方法来替服务被限制时的返回数据。
5.2.@SentinelResource注解说明
- 其主要属性如下::
属性 | 作用 |
---|---|
value | 资源名称 |
blockHandler | 处理BlockException(Sentinel的配置)的兜底方法,要求: 1.返回类型与原方法一致 2.参数类型与原方法一致,并在最后加BlockException类型的参数 |
blockHandlerClass | 存放blockHandler方法的类,要求: 1.对应的方法必须static修饰 |
5.3.同一个类中定义
@RestController
@RequestMapping(value = "/consumer")
public class ConsumerController {
@Autowired
private UserFeign userFeign;
@RequestMapping(value = "/getUserById/{id}")
@SentinelResource(value ="getUserById",
blockHandler="blockHandlerMethod")
public User getUserById(@PathVariable Integer id) {
int a = 6/0;
return userFeign.getUserById(id);
}
//BlockException异常时调用
public static User blockHandlerMethod(Integer id, BlockException e) {
return new User(0, "接口被流控或者熔断了:"+e, 0);
}
}
5.4.外置类中定义
- 创建存放blockHandler方法的类
package com.bjpowernode.exception;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.bjpowernode.pojo.User;
public class BlockHandlerClass {
//BlockException异常时调用
public static User blockHandlerMethod(Integer id, BlockException e) {
return new User(0, "接口被流控或者熔断了:"+e, 0);
}
}
- 使用外置类中定义的方法
@RestController
@RequestMapping(value = "/consumer")
public class ConsumerController {
@Autowired
private UserFeign userFeign;
@RequestMapping(value = "/getUserById/{id}")
@SentinelResource(value ="getUserById",
blockHandler="blockHandlerMethod",blockHandlerClass = BlockHandlerClass.class)
public User getUserById(@PathVariable Integer id) {
int a = 6/0;
return userFeign.getUserById(id);
}
//BlockException异常时调用
//public static User blockHandlerMethod(Integer id, BlockException e) {
// return new User(0, "接口被流控或者熔断了:"+e, 0);
//}
}
5.5.测试
- 新增流控规则
- 熔断后访问:http://127.0.0.1/consumer/getUserById/1
6.Sentinel全局异常处理
6.1.BlockExceptionHandler接口
默认情况下,BlockExceptionHandler 有一个默认的 DefaultBlockExceptionHandler 实现类,返回 Block 字符串提示。这个类是默认处理异常阻塞的,代码如下:
我们可以实现该接口对返回数据进行重写, 从而符合我们的业务要求
6.2.BlockException接口
BlockException 是一个异常抽象基类,其有 5 个实现类,刚好对应 Sentinel 的 5 种流量控制异常,如下图所示:
6.3.实现BlockExceptionHandler
@Component
public class GloabBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
response.setContentType("application/json;charset=utf-8");
BaseResult data = null;
if (e instanceof FlowException) {
data = new BaseResult(-1, "限流异常...");
} else if (e instanceof DegradeException) {
data = new BaseResult(-2, "降级异常...");
}else if (e instanceof ParamFlowException) {
data = new BaseResult(-3, "参数限流异常...");
}else if (e instanceof AuthorityException) {
data = new BaseResult(-4, "授权异常...");
}else if (e instanceof SystemBlockException) {
data = new BaseResult(-5, "系统负载异常...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
class BaseResult {
private int status;
private String msg;
private Object data;
public BaseResult() {
}
public BaseResult(int status, String msg) {
this.status = status;
this.msg = msg;
}
public BaseResult(int status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
6.4.测试
- 新增流控规则
- 高并发访问:http://127.0.0.1/consumer/getUserById/1
7.Sentinel整合Feign
7.1.为什么整合Feign
前文的熔断降级是对客户端(调用方)的保护,而我们的微服务远程调用都是基于Feign来完成的,因此我们可以将Feign与Sentinel整合,在Feign里面实现服务熔断。
7.2.sentinel_consumer
application.yml
feign:
sentinel:
enabled: true #开启Feign对Sentinel的支持
7.3.sentinel_feign
7.3.1.FallbackFactory
- 创建降级逻辑
/**
*一旦Feign远程调用服务失败,就会进入当前类同名方法,执行容错逻辑
*/
@Component
public class UserFeignFallback implements FallbackFactory<UserFeign> {
private Logger log = LoggerFactory.getLogger(UserFeignFallback.class);
@Override
public UserFeign create(Throwable t) {
return new UserFeign() {
@Override
public User getUserById(Integer id) {
return new User(id,"feign调用失败:"+t,0);
}
};
}
}
7.3.2.feign
- 为feign接口指定降级逻辑
@FeignClient(value="sentinel-provider",fallbackFactory = UserFeignFallback.class)
@RequestMapping(value = "/provider")
public interface UserFeign {
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable("id") Integer id);
}
7.3.3.sentinel - 新增熔断规则
7.5.测试
- 关闭服务端
- 熔断前访问:
- 熔断后访问:
8.Sentinel持久化
8.1.为什么要持久化?
在上一篇,我们讲解了Sentinel的整合与使用,但是有个很明显的问题,就是一旦服务重启,当前配置的针对某个接口的规则就丢掉了,然后就需要重新再配一遍,这就很坑爹了,如果开发中需要配置的接口太多,这样岂不是让人疯掉。
因此需要一个地方来保存dashboard中配置的规则,Sentinel提供了多种持久化的方案,可以集成redis、mysql、nacos等。
8.2.sentinel_consumer
8.2.1.pom.xml
<!--sentinel持久化启动器-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
8.2.2.application.yml
spring:
application:
name: sentinel-consumer
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr} #nacos连接地址
namespace: sentinel
groupId: SENTINEL_GROUP #nacos连接的分组名称
dataId: ${spring.application.name}-flow-rules #读取配置文件的名称
rule-type: flow #配置文件内容为flow
ds2:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr} #nacos连接地址
namespace: sentinel
groupId: SENTINEL_GROUP #nacos连接的分组名称
dataId: ${spring.application.name}-degrade-rules #读取配置文件的名称
rule-type: degrade #配置文件内容为degrade
8.3.sentinel
8.3.1.改造Sentinel 源代码
- 下载
下载地址:https://github.com/alibaba/Sentinel/tree/master/sentinel-dashboard
- idea打开sentinel-dashboard工程
- 修改application.properties
nacos.address=192.168.209.129:8848
- 打jar包
8.3.2.新增规则
8.4.nacos - 新建命名空间
8.5.测试
- 浏览器访问:http://127.0.0.1/consumer/getUserById/1?origin=order
- nacos持久化流控规则