一、初始Sentinel
1.雪崩问题及解决方案
1.1 什么是雪崩
微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
1.2 怎么解决雪崩问题
解决雪崩问题的常见方式有四种:
• 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待• 舱壁模式:限定每个业务能使用的线程数,避免耗尽整个 tomcat 的资源,因此也叫线程隔离。• 熔断降级:由 断路器 统计业务执行的异常比例,如果超出阈值则会 熔断 该业务,拦截访问该业务的一切请求。• 流量控制:限制业务访问的 QPS ,避免服务因流量的突增而故障。
超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
流量控制:限制业务访问的QPS,避免服务因流量的突增而故障
1.3 总结
2.服务保护技术对比
3.认识Sentinel
3.1 Sentinel介绍
Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:https://sentinelguard.io/zh-cn/index.html
Sentinel 具有以下特征:
3.2 Sentinel安装
sentinel官方提供了UI控制台,方便我们对系统做限流设置。大家可以在GitHub下载。
3.3 Sentinel的启动
使用 java -jar sentinel-dashboard-1.8.1.jar 命令,端口号默认为8080
如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:
3.4 访问Sentinel
在浏览器输入localhost:8080 (localhost:端口号)即可看到控制台页面,默认的账户和密码都是sentinel
4. 引入微服务项目
要使用Sentinel肯定要结合微服务,这里我们使用SpringCloud实用篇中的cloud-demo工程。没有的小伙伴可以在前面的博客中找到: http://t.csdn.cn/zIezthttp://t.csdn.cn/zIezt
5. 微服务整合Sentinel
我们在order-service中整合Sentinel,并且连接Sentinel的控制台,步骤如下
5.1 引入sentinel依赖
<!-- 引入Sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
5.2 配置控制台地址
#配置Sentinel控制台地址---默认端口号为8080
spring.cloud.sentinel.transport.dashboard=localhost:8100
5.3 测试
访问微服务的任意端点,触发sentinel监控
二、流量控制
1.簇点链路
簇点链路:就是项目内的调用链路,链路中被监控的每个接口就是一个资源。默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
2.快速入门
点击资源/order/{orderId}后面的流控按钮,就可以弹出表单。表单中可以添加流控规则,如下图所示:
其含义是限制 /order/{orderId}这个资源的单机QPS为1,即每秒只允许1次请求,超出的请求会被拦截并报错。
2.1 测试
流控规则入门案例
需求:给 /order/{orderId}这个资源设置流控规则,QPS不能超过 5。然后利用jemeter测试。
1.设置流控规则:
2.jemeter测试:
3. 流控模式
在添加限流规则时,点击高级选项,可以选择三种流控模式:
3.1流控模式----直接
3.2 流控模式---关联

当/update资源访问量触发阈值时,就会对/save资源限流,避免影响/update资源
测试
需求:
• 在 OrderController 新建两个端点: /product/save 和 /product/update ,无需实现业务• 配置流控规则,当 /order/ update 资源被访问的 QPS 超过 5 时,对 /order/query 请求限流
第一步:controller层配置
第二步:测试两个接口并实时注册到Sentinel
第三步: 设置流控模式---关联
当update达到阈值上限,限制save的执行
第四步:测试
满足下面条件可以使用关联模式:
3.3 流控模式----链路
链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
例如有两条请求链路:
如果只希望统计从/test2进入到/common的请求,则可以这样配置:
测试:
需求:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。
步骤:
1. 在 OrderService 中添加一个 queryGoods 方法,不用实现业务2. 在 OrderController 中,改造 /order/query 端点,调用 OrderService 中的 queryGoods 方法3. 在 OrderController 中添加一个 /order/save 的端点,调用 OrderService 的 queryGoods 方法4. 给 queryGoods 设置限流规则,从 /order/query 进入 queryGoods 的方法限制 QPS 必须小于 2
•Sentinel默认只标记Controller中的方法为资源,如果要标记其它层的方法,需要利用@SentinelResource注解,示例:
Sentinel默认会将Controller方法做context整合,导致链路模式的流控失效,需要修改application.yml,添加配置
#关闭上下文件---注册@SentinelResource("queryGoods")
spring.cloud.sentinel.web-context-unify=false
第一步: controller层
第二步:service层
第三步: 在配置文件中注册该注解
第四步: 设置流控模式----链路
两个请求同时调用该链路,只对saveOrder做设置
第五步: 测试
3.4 总结
流控模式有哪些?
4.流控效果
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
4.1 快速失败
4.2 流控效果----warm up
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 threshold / coldFactor,持续指定时长后,逐渐提高到threshold值。而coldFactor的默认值是3.
例如,我设置QPS的threshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10.
测试:
需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用warm up效果,预热时长为5秒
第一步:设置流控效果----warm Up
第二步: 测试
4.3 流控效果----排队等待
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待超过2000ms的请求会被拒绝并抛出异常
测试:
需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s
第一步: 设置流控效果----排队等待
第二步: 测试
4.4 总结
流控效果有哪些?
• 快速失败: QPS 超过阈值时,拒绝新的请求• warm up : QPS 超过阈值时,拒绝新的请求; QPS 阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机。• 排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝
5.热点规则
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值
在热点参数限流的高级选项中,可以对部分参数设置例外配置:
结合上一个配置,这里的含义是对0号的long类型参数限流,每1秒相同参数的QPS不能超过5,有两个例外:
5.1 测试
给/order/getOrder/{orderId}这个资源添加热点参数限流,规则如下:
• 默认的热点参数规则是每 1 秒请求量不超过 2• 给 102 这个参数设置例外:每 1 秒请求量不超过 4• 给 103 这个参数设置例外:每 1 秒请求量不超过 10
第一步:在controller层的getOrder方法上添加注解
不要忘记在配置文件中开启注解,若以开启则无需再操作
#关闭上下文件---注册@SentinelResource("queryGoods") spring.cloud.sentinel.web-context-unify=false
第二步:添加热点规则
第二步: 测试
三、隔离和降级
虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。
1.Feign整合Sentinel
SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
1.1 开启Feign的Sentinel整合功能
修改微服务尾部名为Order的配置文件,开启Feign的Sentinel功能
覆盖非官方
1.2 给FeignClient编写失败后的降级逻辑
①方式一:FallbackClass,无法对远程调用的异常做处理
②方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
步骤一:在微服务项目中定义类,实现FallbackFactory:
package com.wt.feign;
import com.wt.entity.Product;
import com.wt.utils.CommonResult;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component //将该类交于spring容器
public class ProductFeignFactory implements FallbackFactory<ProductFeign> {
@Override
public ProductFeign create(Throwable throwable) {
ProductFeign productFeign = new ProductFeign() {
//这个是getById的兜底方案
@Override
public Product getById(Integer pid) {
log.error("远程调用商品出现问题,执行了兜底方法");
Product product = new Product();
product.setPname("异常"+throwable.getMessage());
System.out.println("-------------");
return product;
}
};
return productFeign;
}
}
步骤二:在f微服务项目中的ProductFeign接口中使用UserClientFallbackFactory:
package com.wt.feign;
import com.wt.entity.Product;
import com.wt.utils.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
//1.开启openfeign和sentinel的整合
//2.创建FallbackFactory工厂类
//3.在feignClient指定fallbackFactory工厂类的反射
/**
* @Author wt
* @Date 2022/8/23 15:47
* @PackageName:com.wt.feign
* @ClassName: ProductFeign
* @Description:
* @Version 1.0
*/
@FeignClient(value = "springcloud04-skywalking-product",fallbackFactory = ProductFeignFactory.class)
public interface ProductFeign {
@GetMapping("/product/getById/{pid}")
public Product getById(@PathVariable Integer pid);
}
1.3 测试
第一步:配置整合策略
第二步:测试
1.4 总结
Sentinel支持的雪崩解决方案:
• 线程隔离(仓壁模式)• 降级熔断Feign整合Sentinel的步骤:
• 在 application.yml 中配置: feign.sentienl.enable =true• 给 FeignClient 编写 FallbackFactory 并注册为 Bean• 将 FallbackFactory 配置到 FeignClient
2. 线程隔离
线程隔离有两种方式实现:
在添加限流规则时,可以选择两种阈值类型:
2.1 线程池隔离测试
需求:给 UserClient的查询用户接口设置流控规则,线程数不能超过 2。然后利用jemeter测试。
第一步:配置线程隔离
设置最大线程数为2
第二步: 测试
2.2 总结
线程隔离的两种手段是?
• 信号量隔离• 线程池隔离信号量隔离的特点是?
• 基于计数器模式,简单,开销小线程池隔离的特点是?
• 基于线程池模式,有额外开销,但隔离控制更强
3. 熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
3.1 熔断策略----慢调用
断路器熔断策略有三种:慢调用、异常比例、异常数
解读:统计最近10000ms内的请求,如果请求量超过5次,并且异常比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
测试:
第一步: 在controller层添加延迟策略
在正常运行过程中,执行buy方法不会超过20ms,所以需要添加延迟策略,我们使用buy远程调用product的方法,所以在其对应方法上加延迟
@GetMapping("/getById/{pid}")
public Product getById(@PathVariable Integer pid) {
if (pid==1){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return productService.getById(pid);
}
第二步:设置熔断策略---慢调用
如果在10秒内请求超过5次,且请求中有50%的最大RT超过100ms,则触发熔断,熔断持续10秒,10s后进入半熔断
第三步: 测试
如果触发熔断,则正常的请求也会被阻拦---因为我们在pid为1时设置了延时,而pid为其它是正常执行的,可以再开一个进行对比---查看pid为1的触发熔断后,正常的有没有也被拦截
3.2 异常比例或异常数
•异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。例如:
解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
测试:
需求:给 UserClient的查询用户接口设置降级规则,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5s
提示:为了触发异常统计,我们需要修改UserService中的业务,抛出异常:
第一步: 在controller层添加异常策略
@GetMapping("/getById/{pid}")
public Product getById(@PathVariable Integer pid) {
if (pid==1){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else if (pid==2){
int c=10/0;
}
return productService.getById(pid);
}
第二步:设置熔断策略---异常比例
在10秒内,如果请求数超过最小请求数5,且有50%的请求都出现异常,则触发熔断,熔断时间为10秒,10秒后进入半熔断
第三步: 测试
如果触发熔断,则正常的请求也会被阻拦---因为我们在pid为2时设置了异常,而pid为其它是正常执行的,可以再开一个进行对比---查看pid为2的触发熔断后,正常的有没有也被拦截
3.3 总结
Sentinel熔断降级的策略有哪些?
• 慢调用比例:超过指定时长的调用为慢调用,统计单位时长内慢调用的比例,超过阈值则熔断• 异常比例:统计单位时长内异常调用的比例,超过阈值则熔断• 异常数:统计单位时长内异常调用的次数,超过阈值则熔断
四、授权规则
1.授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
例如,我们限定只允许从网关来的请求访问order-service,那么流控应用中就填写网关的名称
第一步
第二步
2. 自定义异常
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:
而BlockException包含很多个子类,分别对应不同的场景:
2.1 定义自定义异常类
我们在order-service中定义类,实现BlockExceptionHandler接口:
package com.wt.handler;
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;
/**
* @Author wt
* @Date 2022/8/25 17:24
* @PackageName:com.wt.handler
* @ClassName: MyException
* @Description: TODO
* @Version 1.0
*/
@Component
public class MyException implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, 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;
}
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(status);
httpServletResponse.getWriter().println("{\"msg\":"+msg+",\"status\":"+status+"}");
}
}
2.2 总结
获取请求来源的接口是什么?
• RequestOriginParser处理BlockException的接口是什么?
• BlockExceptionHandler
五、规则持久化
1.规则管理模式
Sentinel的控制台规则管理有三种模式:
1.1 原始模式
原始模式:控制台配置的规则直接推送到Sentinel客户端,也就是我们的应用。然后保存在内存中,服务重启则丢失
1.2 pull模式
pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。
1.3 push模式
push模式:控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。
1.4 总结
Sentinel的三种配置管理模式是什么?
• 原始模式:保存在内存• pull 模式:保存在本地文件或数据库,定时去读取• push 模式:保存在 nacos ,监听变更实时更新
2.实现push模式
2.1 添加push类
package com.wt.config;
import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;
import com.alibaba.csp.sentinel.datasource.*;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Value;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class FilePersistence implements InitFunc {
@Value("${spring.application.name}")
private String appcationName;
@Override
public void init() throws Exception {
String ruleDir = "./sentinel-rules/" + appcationName;
System.out.println(ruleDir+"~~~~~~~~~~~~~");
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.2 添加配置
在resources下创建配置目录 META-INF/services ,然后添加文件
com.alibaba.csp.sentinel.init.InitFunc
在文件中添加配置类的全路径---全路径随着自己push类所在路径而改变
com.wt.config.FilePersistence
2.3 测试
执行成功则会出现以下目录