一、初识 Sentinel
1. 雪崩问题及解决方案
微服务调用链路中的某个服务故障,
引起整个链路中的所有微服务都不
可用,这就是雪崩
常见解决方式有四种:
① 超时处理:设定超时时间,请求超
过一定时间没有响应就返回错误信
息,不会无休止等待
② 舱壁模式:限定每个业务能使用的
线程数,避免耗尽整个 tomcat 的资
源,因此也叫线程隔离
③ 熔断降级:由断路器统计业务执行
的异常比例,如果超出阈值则会熔断
该业务,拦截访问该业务的一切请求
④ 流量控制:限制业务访问的 QPS,
避免服务因流量的突增而故障
2. 服务保护技术对比
3. Sentinel 介绍和安装
Sentinel 是阿里巴巴开源的一款微服务
流量控制组件
https://sentinelguard.io/zh-cn/index.html Sentinel
具有以下特征:
① 丰富的应用场景
② 完备的实时监控
③ 广泛的开源生态
④ 完善的 SPI 扩展点
(2) 安装 Sentinel 控制台:
① 准备好 sentinel-dashboard-1.8.1.jar
② 执行 java -jar sentinel-dashboard-1.8.1.jar
③ 访问:localhost:8080 即可看到控制台页面,
默认的账户和密码都是 sentinel
(3) 修改 Sentinel 的默认端口、账户、密码
java -jar sentinel-dashboard-1.8.1.jar -Dserver.port=8080
4. 微服务整合 Sentinel
在 order-service 中整合 Sentinel,并且连接
Sentinel 的控制台,步骤如下:
(1) 引入 sentinel 依赖:
<!--sentinel-->
<dependency><groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
(2) 配置控制台地址
修改 application.yaml 文件,添加下面内容:
server:port: 8088
spring:cloud: sentinel:transport:dashboard: localhost:8080
(3) 访问微服务的任意端点,触发 sentinel 监控
访问 http://localhost:8088/order/101
二、限流规则
1. 快速入门
簇点链路:就是项目内的调用链路,链路
中被监控的每个接口就是一个
资源
默认 sentinel 会监控 SpringMVC 的每一
个端点 (Endpoint),因此 SpringMVC 的
每一个端点 (Endpoint) 就是调用链路中的
一个资源。流控、熔断等都是针对簇点链
路中的资源来设置的,因此我们可以点击
对应资源后面的按钮来设置规则
点击资源 /order/{orderId} 后面的流控
按钮,就可以弹出表单,表单中可以
添加流控规则
图中含义:限制 /order/{orderId} 这个资源
的单机 QPS 为 1,即每秒只允许1次请求,
超出的请求会被拦截并报错
2. 流控模式
在添加限流规则时,点击高级选项,可以选
择三种流控模式:
① 直接:统计当前资源的请求,触发阈
值时对当前资源直接限流,也是默认
的模式
② 关联:统计与当前资源相关的另一个
资源,触发阈值时,对当前资源限流
③ 链路:统计从指定链路访问到本资源
的请求,触发阈值时,对指定链路限
流
(2) 关联
满足下面条件可以使用关联模式:
① 两个有竞争关系的资源
② 一个优先级较高,一个优先级较低
当 /write 资源访问量触发阈值时,就会对
/read 资源限流,避免影响 /write 资源
(3) 链路
例如有两条请求链路:
/test1 → /common
/test2 → /common
如果只希望统计从 /test2 进入到 /common
的请求,则可以这样配置
① Sentinel 默认只标记 Controller 中的方
法为资源,如果要标记其他方法,需要
利用 @SentineResource 注解
@SentineResource("goods")
public void queryGoods() {System.err.println("查询商品");
}
② Sentinel 默认会将 Controller 方法做
context 整合,导致链路模式的流控失
效
需要修改 application.yml,添加配置:
spring:cloud:sentinel:web-context-unify:false # 关闭 context 整合
3. 流控效果
流控效果是指请求达到流控阈值时应该
采取的措施
包括三种:
① 快速失败:达到阈值后,新的请求会被
立即拒绝并抛出 FlowException 异常
(默认)
② warm up:预热模式,对超出阈值的请
求同样是拒绝并抛出异常,但这种模式
阈值会动态变化,从一个较小值逐渐增
加到最大阈值
③ 排队等待:让所有的请求按照先后次序
排队执行,两个请求的间隔不能小于指
定时长
4. 热点参数限流
之前的限流是统计访问某个资源的所有请求,
判断是否超过 QPS 阈值,而热点参数限流
是分别统计参数值相同的请求,判断是否超
过 QPS 阈值
(1) 配置示例:
对 hot 这个资源的 0 号参数 (第一个参数)
做统计,每 1 秒相同参数值的请求数不能
超过 5
(2) 在高级选项中,可以对部分参数设置
例外配置:
结合上一个配置,这里的含义是对 0 号
的 long 类型参数限流,每 1 秒相同参数
的 QPS 不能超过 5
有两个例外:
① 如果参数值是 100,则每 1 秒允许的
QPS 为 10
② 如果参数值是 101,则每 1 秒允许的
QPS为 15
注意:热点参数限流对默认的 SpringMVC
资源无效
三、隔离和降级
虽然限流可以尽量避免因高并发而引起的
服务故障,但服务还会因为其它原因而故
障,而要将这些故障控制在一定范围,避
免雪崩,就要靠线程隔离 (舱壁模式) 和熔
断降级手段了,不管是线程隔离还是熔断
降级,都是对客户端 (调用方) 的保护
1. FeignClient 整合 Sentinel
(1) 修改 OrderService 的 application.yml 文件,
开启 Feign 的 Sentinel 功能
feign:sentinel:enabled: true # 开启Feign的Sentinel功能
(2) 给 FeignClient 编写失败后的降级逻辑
方式一:FallbackClass,无法对远程调用
的异常做处理
方式二:FallbackFactory,可以对远程调
用的异常做处理,我们选择这种
给 FeignClient 编写 FallbackFactory 并注册
为 Bean
@Slf4j
@Bean
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {@Overridepublic UserClient create(Throwable throwable) {// 创建UserClient接口实现类,实现其中的方法,编写失败降级的处理逻辑return new UserClient() {@Overridepublic User findById(Long id) {// 记录异常信息log.error("查询用户失败", throwable);// 根据业务需求返回默认的数据,这里是空用户return new User();}};}
}
(3) 在 feing-api 项目中的 UserClient 接口中
使用 UserClientFallbackFactory
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {@GetMapping("/user/{id}")User findById(@PathVariable("id") Long id);
}
2. 线程隔离(舱壁模式)
线程隔离有两种方式实现:
① 线程池隔离
② 信号量隔离 (Sentinel 默认采用)
(1) 信号量隔离
优点:轻量级,无额外开销
缺点:不支持主动超时
不支持异步调用
场景:高频调用,高扇出
(2) 线程池隔离
优点:支持主动超时
支持异步调用
缺点:线程的额外开销比较大
场景:低扇出
低扇出:就是说让一个类里少量或适中地
使用其他的类
高扇出 (超过约7个):说明一个类使用了大
量其他的类,因为可能变得过于复杂
在添加限流规则时,可以选择两种阈值类型:
QPS:就是每秒的请求数
线程数:是该资源能使用用的 tomcat 线程
数的最大值,也就是通过限制线程
数量,实现舱壁模式
3. 熔断降级
熔断降级是解决雪崩问题的重要手段,其
思路是由断路器统计服务调用的异常比例、
慢请求比例,如果超出阈值则会熔断该服
务。
即拦截访问该服务的一切请求;而当服务
恢复时,断路器会放行访问该服务的请求
断路器熔断策略有三种:
① 慢调用
② 异常比例
③ 异常数
(1) 慢调用
业务的响应时长 (RT) 大于指定时长的请
求,统计单位时长内慢调用的比例,超
过阈值则熔断
RT 超过 500ms 的调用是慢调用,统计最近
10000ms 内的请求,如果请求量超过 10 次,
并且慢调用比例不低于0.5,则触发熔断,熔
断时长为5秒,然后进入 half-open 状态,放
行一次请求做测试
(2) 异常比例
统计单位时长内异常调用的比例,超过
阈值则熔
统计最近 1000ms 内的请求,如果请求量
超过 10 次,并且异常比例不低于 0.5,则
触发熔断,熔断时长为 5 秒,然后进入
half-open 状态,放行一次请求做测试
(3) 异常数
统计单位时长内异常调用的次数,超过
阈值则熔断
四、授权规则
(1) 授权规则
授权规则可以对调用方的来源做控制,
有两种方式:
白名单:来源 (origin) 在白名单内的调用者
允许访问
黑名单:来源 (origin) 在黑名单内的调用者
不允许访问
例如,我们限定只允许从网关来的请求访问
orde-service,那么流控应用中就填写网关
的名称
① Sentinel 是通过 RequestOriginParser
这个接口的 parseOrigin 来获取请求的
来源的
public interface RequestOriginParser {/*** 从请求request对象中获取origin,获取方式自定义*/String parseOrigin(HttpServletRequest request);
}
在 Sentinel 中,该接口的 parseOrigin 方法
返回的永远是 default,也就是说,无论请求
开源是来自网关还是浏览器,Sentinel 根本
无法区分这两个请求
需要实现这个接口,编写它的业务逻辑,让
从网关来的请求和浏览器来的请求返回不同
的结果
② 从 request 中获取一个名为 origin 的请求
头,作为 origin 的值
@Component
public class HeaderOriginParser implements RequestOriginParser {@Overridepublic String parseOrigin(HttpServletRequest httpServletRequest) {// 尝试获取请求头String origin = httpServletRequest.getHeader("origin");// 非空判断if (StringUtils.isEmpty(origin)){origin = "blank";}return origin;}
}
③ 在 gateway 服务中,利用网关的全局过
滤器添加名为 gateway 的 origin 头
spring:cloud:gateway:default-filters:- AddRequestHeader=origin,gateway # 添加名为origin的请求头,值为gateway
④ 给 /order/{orderId} 配置授权规则:
(2) 自定义异常结果
默认情况下,发生限流、降级、授权拦截
时,都会抛出异常到调用方
如果要自定义异常时的返回结果,需要实现
BlockExceptionHandler 接口:
public interface BlockExceptionHandler {// 处理请求被限流、降级、授权拦截时抛出的异常void handle(HttpServletRequest var1, HttpServletResponse var2, BlockException var3) throws Exception;
}
BlockException 包含很多个子类,以应对不同
的场景:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
在 order-service 中定义类,实现 BlockExceptionHandler 接口:
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {@Overridepublic 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().write("{\"msg\": " + msg + ", \"status\":" + status + "}");}
}
五、规则持久化
1. 规则管理模式
Sentinel 的控制台规则管理有三种模式:
① 原始模式:控制台配置的规则直接推送到
Sentinel 客户端,也就是我们的应用,然后
保存在内存中,服务重启则丢失
② pull 模式:控制台将配置的规则推送到
Sentinel 客户端,而客户端会将配置规则保
存在本地文件或数据库中,以后会定时去本
地文件或数据库中查询,更新本地规则
③ push 模式:控制台将配置规则推送到远程
配置中心,例如 Nacos 或 Zookeeper,
Sentinel 客户端监听 Nocas,获取配置变更
的推送消息,完成本地配置更新 (推荐)
2. 实现push模式
push 模式的实现最为复杂,因为其依赖于
nacos,并且需要改 Sentinel 控制台的源码,
整体步骤如下:
(1) 修改 order-service 服务,使其监听
Nacos 配置中心
① 引入依赖
<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
② 配置 nacos 地址
spring:cloud:sentinel:datasource:flow:nacos:server-addr: localhost:8848 # nacos地址dataId: orderservice-flow-rulesgroupId: SENTINEL_GROUPrule-type: flow # 还可以是degrade、authority、param-flowdegrade:nacos:server-addr: localhost:8848 # nacos地址dataId: orderservice-degrade-rulesgroupId: SENTINEL_GROUPrule-type: degrade # 还可以是degrade、authority、param-flow
(2) 修改 Sentinel-dashboard 源码,配置
nacos 数据源
修改源码的 pom 文件,将 sentinel-
datasource-nacos 依赖的 scope 去掉
<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId><!--<scope>test</scope>-->
</dependency>
(3) 修改 Sentinel-dashboard 源码, 修改前端
页面
① 拷贝 test 目录下的 nacos 代码到 main下的
com.alibaba.csp.sentinel.dashboard.rule 包
② 修改刚刚拷贝的 nacos 包下的 NacosConfig
类,修改其中的 nacos 地址
@Bean
public ConfigService nacosConfigService() throws Exception {return ConfigFactory.createConfigService("localhost:8848");
}
④ 修改 com.alibaba.csp.sentinel.dashboard.
controller.v2 包下的 FlowControllerV2 类
(4) 重新编译、打包 Sentinel-dashboard 源码
① 修改 src/main/webapp/resources/app/scripts/
directives/sidebar/ 目录下的 sidebar.html 文件,
将其中的这部分注释打开
② 修改其中的文本
<li ui-sref-active="active" ng-if="entry.appType==0"><a ui-sref="dashboard.flow({app: entry.app})"><i class="glyphicon glyphicon-filter"></i> 流控规则-NACOS</a>
</li>