SpringCloud(11)— 微服务保护(Sentinel)
一 认识Sentinel
1.雪崩问题及其解决方案
微服务调用链路中的某个服务出现问题,引起整个链路中所有的微服务都不可用,这就是我们常说的雪崩问题。
如何解决雪崩问题?
常见的解决方案有如下几种:
- 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,防止无休止等待。
- 舱壁模式:限定每个业务使用的线程数,避免耗尽整个web服务器资源,因此也叫线程隔离。
- 熔断降级:由断路器统计业务失败的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求
- 流量控制:限制访问业务的QPS(单位时间内的业务数量),避免服务因流量的突增而故障
2.微服务保护技术对比
sentinel目前是SpringBoot Alibaba的一个组件。
3.认识Sentinel
Sentinel是阿里巴巴的一款微服务流量控制组件。
官方网址:home | Sentinel (sentinelguard.io)
Github地址:GitHub - alibaba/Sentinel: A powerful flow control component enabling reliability, resilience and monitoring for microservices. (面向云原生微服务的高可用流控防护组件)
Sentinel的特征:
- 丰富的应用场景
- 完备的实时监控
- 广泛的开源生态
- 完善的SPI扩展点
4.安装和运行Sentinel
Sentinel控制台 的下载地址:Releases · alibaba/Sentinel (github.com)
下载下来之后是一个 jar 包,使用命令运行即可,笔者这里下载的是最新版1.8.6
# 直接运行,默认端口为8080
java -jar sentinel-dashboard-1.8.6.jar
# 指定部分参数运行,通过 -Dserver.port=xxxx 指定运行端口
java -Dserver.port=8080 -jar sentinel-dashboard-1.8.6.jar
# 其余更多配置参数参考官网
运行之后,在浏览器访问即可。默认的用户名和密码均为 sentinel
常用的参数配置项:
使用举例:运行时在参数向前面加上“ -D ”,例如 -Dserver.port=xxxx 即可。
5.项目整合Sentinel
1.引入Sentinel 的依赖
<!--引入Sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2.在配置文件中配置Sentinel相关参数
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
3.访问微服务的任意端口,触发 sentinel 监控
二 流量控制
1.限流基本用法
簇点链路:指的是项目内部的调用链路,链路中被监控的每个接口就是一个资源。
默认情况下,sentinel 会监控微服务中的每一个端点(endpoint),因此微服务中的每一个端点就是调用链路中的一个资源
流控,熔断
等都是 针对簇点链路中的资源
来设置的,可以直接点击资源后边的按钮来设置规则
1.流控设置
点击资源后面的 流控
按钮,便可以对资源进行流控设置
- 资源名称:表示要流控的资源
- 针对来源:表示从什么地方进来的请求需要进行流控,
default
表示一切对该资源的请求都要进行流控 - 阈值类型:
QPS
表示单位时间内的并发量,后边的单机阈值
表示所选阈值类型的上限。如果超出则会被拦截并且报错
利用 jmeter
进行测试,会发现超出设定的QPS
单机阈值的请求将返回被sentinel拦截的信息
2.流控模式
现在来看看流控规则中的额高级设置
在流控规则界面打开高级选线
按钮,可以看到有很多流控模式
和流控效果
供我们选择
其中流控模式
中的三种模式代表的含义分别如下:
- 直接:统计当前资源的请求,出发阈值时对当前资源直接限流,也是默认的模式
- 关联:统计与当前资源关联的另一个资源,触发阈值时,对当前资源限流
- 链路:统计从指定链路访问到本资源的请求,触发阈值时,对
指定链路
限流
Sentinel中的流控设置,默认情况下使用的是直接模式
1.关联模式
统计与当前资源关联的另一个资源,触发阈值时,对当前资源限流
使用场景:比如用户支付订单的同时,要查询订单。查询和修改操作会争抢数据库资源,产生竞争。因此当支付订单状态出发阈值时,需要对查询业务限流
以上示例,当 /user/{id}
的 QPS
达到阈值时,将会对 /order/{id}
进行限流,避免影响到 /user/{id}
满足一下条件时可以考虑使用关联模式:
- 两个有竞争关系的资源
- 一个优先级高,一个优先级低
2.链路模式
统计从指定链路访问到本资源的请求,触发阈值时,对指定链路
限流
例如有两条请求链路,
/test1
->/common
/test2
->/common
如果只希望统计从 /test1
进入 /common
的请求,则可以这样配置:
这样一来,当从 /test1
进入 /common
的QPS
达到阈值时,将对 /test1
进行限流
Sentinel
默认值标记 Controller
中的方法为资源,如果要标记其他方法,需要利用 @SentinelResource
注解
@SentinelResource("data")
public void queryData(){
System.out.println("查询数据");
}
Sentinel
默认会将Controller
方法做context
整合,导致链路模式的流控失效,需要修改项目配置文件,添加配置。
spring:
cloud:
sentinel:
# 关闭context整合
web-context-unify: false
transport:
dashboard: localhost:8080
3.流控效果
流控效果是指请求达到流控阈值时应采取的措施
Sentinel
提供了三种流控效果:
- 快速失败:达到阈值后,新的请求会被直接拒绝并抛出
FlowException
异常,这是默认的处理方式 - Warm Up:预热模式,对于超出阈值的请求同样是直接拒绝。但是这种阈值会动态变化,从一个较小阈值逐渐增加到最大阈值
- 排队等待:让所有请求按照先后顺序排队执行,两个请求的间隔不能小于指定时长
1.Warm Up
warm up
也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值为 threshold / coldFactor
,持续指定时长后,逐渐提高到 threshold
值,而 coldFactor
的默认值为3。
预热模式主要是防止冷启动时过高并发从而导致故障
2.排队等待
当请求超过QPS
值时,快速失败和预热模式都会抛出异常。而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面的请求执行完成,如果请求预期的等待时间超出最大时长,则请求会被拒绝。
4.热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS
阈值,而热点参数限流是分别统计参数值相同
的请求,判断是否超过QPS
阈值。
例如以下配置:
热点资源 hot 的参数值为1的限流阈值为4
热点资源 hot 的参数值为2的限流阈值为2
由此可见,热点参数限流是一种更为细腻的限流方式,将限流规则降低到了参数级别上。
三 隔离和降级
限流是对微服务故障的一种预防措施,但是一旦服务已经出现了故障,则我们需要其他手段来防止出现级联失败甚至雪崩的问题。
而要将这些故障控制在一定范围内,避免雪崩,就需要依赖于线程隔离(舱壁模式)
和熔断降级
手段了。
不管是线程隔离
还是熔断降级
,都是对客户端(调用者 / 消费者)的保护
1.FeignClient整合Sentinel
SpringCloud
中,微服务调用是通过Feign来实现的,因此做客户端保护必须整合Feign
和Sentinel
步骤1:修改消费者微服务的项目配置文件,开启Feign的Sentinel功能
feign:
sentinel:
# 开启 Feign 的 Sentinel 功能
enabled: true
开启以后,Feign请求
将会作为一个资源存在于Sentinel
中,这样我们就可以对它进行流控
等操作了
步骤2:给FeignClient
编写失败后的降级逻辑
- 方式1:FallbackClass,无法对远程调用的异常做处理
- 方式2:FallbackFactory,可以对远程调用的异常做处理,一般使用这种
步骤3:编写具体的降级逻辑
在feign-api
项目中编写 UserClientFallbackFactory
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<IUserClientFeign> {
@Override
public IUserClientFeign create(Throwable throwable) {
return new IUserClientFeign() {
@Override
public User findById(Integer id) {
log.error("查询异常");
//未查询到用户时指定返回内容,此处返回空对象
return new User();
}
};
}
}
将其定义为 bean
@Configuration
public class FeignConfig {
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
}
在@FeignClient
上指定fallbackFactory
@FeignClient(value = "alibaba-user",fallbackFactory = UserClientFallbackFactory.class)
@RequestMapping("/user")
public interface IUserClientFeign {
/**
* 定义请求接口
* @param id userId
* @return 返回对象
*/
@GetMapping("/{id}")
User findById(@PathVariable("id") Integer id);
}
2.线程隔离
线程隔离有两种实现方式:
- 线程池隔离
- 信号量隔离(sentinel默认)
线程池隔离原理:程序启动时默认创建指定数量的线程池用于被其他微服务使用。当线程池被全部占据时将不再接受访问。业务处理完时线程解除占用。
信号量隔离原理:采用信号量计数器的办法,当计数器达到指定的数量则不再接受访问。即有新业务时信号量+1,业务处理完成时信号量-1。
在添加限流规则时,可以选择两种阈值类型:
- QPS:每秒请求数
- 线程数:是该资源使用的
tomcat
线程数的最大值,也就是通过限制线程数量,实现舱壁模式
3.熔断降级
熔断降级是解决雪崩问题的重要手段,其思路是由断路器统计服务调用的异常比例,慢请求比例。如果超出阈值则会熔断该服务。即拦截该服务的一切请求;而当服务恢复时,断路器也会放行该服务的请求。
以下是断路器的工作流程
断路器熔断策略有三种:慢调用,异常比例,异常数。
1.慢调用
慢调用:业务的响应时间(RT)大于指定时长的请求认定为慢调用请求。在指定时间内请求数量超过设定的最小数量,慢调用比例大于设定的阈值时触发熔断。
以上示例的含义:对于 /order/{id}
资源,响应时间大于2000ms
时认为时慢调用。统计1000ms
内的请求,如果请求量超过5次
且慢调用比例超过0.5
,则触发熔断,熔断时长为5s
,然后进入Half-Open
状态,放行一次请求做测试。
2.异常比例或异常数
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且异常的比例
或异常数
达到设定的阈值,则触发熔断。
异常比例或异常数的熔断规则与调用时长无关,只关心是否抛出异常和抛出异常的比例
以上示例的含义:对于 /user/{id}
资源,统计1000ms
内的请求,如果请求量超过5次
且异常比例超过0.5
,则触发熔断,熔断时长为5s
,然后进入Half-Open
状态,放行一次请求做测试。
四 授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问
1.请求来源
Sentinel是通过RequestOriginParse这个接口的parseOrigin来获取请求来源的
2.授权实现
在微服务中实现 RequestOriginParser
接口,这里与统一网关约定使用origin
作为请求头参数名。
@Component
public class HeaderOriginParse implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
// 1.获取请求头
String origin=httpServletRequest.getHeader("origin");
if(StringUtils.isEmpty(origin)){
origin="blank";
}
return origin;
}
}
在统一网关服务的配置文件中添加origin
请求头参数,值为alibaba-gateway
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=origin,alibaba-gateway
通过以上设置,可以保证只要是来源于统一网关服务
的请求,请求头参数 origin
的值均为alibaba-gateway
最后,在授权规则中填入来源名称,设置白名单即可
添加了授权规则之后,对于 /order/{id}
的访问就必须通过统一网管来访问。
注意:请求来源参数(origin)的参数名和参数值切勿对外暴露,防止别人伪造请求头
3.自定义异常拦截请求
默认情况下,发生限流,降级,熔断时,都会抛出异常到调用方。如果要实现自定义异常时的返回结果,需要实现BlockExceptionHandler
接口。
BlockException
包含很多子类,分别对应不同的场景
以下是微服务调用时被Sentinel
拦截的简单示例:
@Component
public class SentinelBlockHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
String msg = "未知异常";
if (e instanceof FlowException) {
msg = "请求被限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级";
} else if (e instanceof ParamFlowException) {
msg = "热点参数被限流";
}else if (e instanceof AuthorityException) {
msg = "授权规则异常";
}
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.getWriter().println("{\n" +
" \"status\": 429,\n" +
" \"msg\":\""+msg+"\"\n" +
"}");
}
}
以下是返回示例:
返回的Json格式自定义,此处只做演示,不深究
{
"status": 429,
"msg": "授权规则异常"
}
五 规则持久化
在配置了Sentinel
的各种规则后,重启服务会导致规则丢失。
这是因为Sentinel
将规则保存在内存中,重启微服务规则自然会丢失。
1.规则管理模式
Sentinel
的控制台规则管理有三种模式:
- 原始模式:
Sentinel
的默认模式,将规则保存在内存,重启服务则丢失 - pull模式:保存在本地或数据库,定时去读取
- push模式:保存在
Nacos
,监听变更实时更新
1.Pull模式
Pull模式:控制台将配置的规则推送至Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。
缺点:存在时效性问题,容易导致数据不一致
2.Push模式
Push模式:控制台将配置规则推送到远程配置中心,例如Nacos
。Sentinel
客户端监听Nacos
,获取配置变更的推送消息,完成本地配置更新。
2.实现push模式
1.引入依赖
在微服务中引入 sentinel-datasource-nacos
,监听 Nacos
的依赖。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2.配置Nacos地址
在微服务的配置信息中配置Nacos
地址以及监听的配置信息
spring:
cloud:
sentinel:
datasource:
#流控
flow:
nacos:
server-addr: localhost:8848
data-id: orderserver-flow-rules #最终的配置文件名称
group-id: SENTINEL_GROUP # 组ID
rule-type: flow # 还可以是其他,具体请查看 RuleType 的实例,需要配置多个时复制多份即可
#降级
degrade:
nacos:
server-addr: localhost:8848
data-id: orderserver-degrade-rules #最终的配置文件名称
group-id: SENTINEL_GROUP # 组ID
rule-type: degrade
3.修改sentinel源码
Sentinel-Dashboard
默认不支持nacos持久化,需要修改源码
在 Github
上下载sentinel
的源码,使用 IDE工具打开
1.在sentinel-dashboard源码的pom文件中,nacos的依赖默认的scope是test,只能在测试时使用,这里要去除:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2.在sentinel-dashboard的test包下,已经编写了对nacos的支持,我们需要将其拷贝到main下。
3.修改 nacos
的地址,修改测试代码中的 NacosConfig
类,让其读取application.properties中的值
@Configuration
@ConfigurationProperties(prefix = "nacos")
public class NacosConfig {
/**
* nacos地址
*/
private String addr;
@Bean
public ConfigService nacosConfigService() throws Exception {
return ConfigFactory.createConfigService(addr);
}
public void setAddr(String addr){
this.addr=addr;
}
public String getAddr(){
return this.addr;
}
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
}
4.在sentinel-dashboard
的application.properties
中添加nacos地址配置:
nacos.addr=localhost:8848
5.需要修改 com.alibaba.csp.sentinel.dashboard.controller.v2
包下的 FlowControllerV2
类,让添加的Nacos数据源生效
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
6.修改前端页面,添加一个支持nacos的菜单,
修改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> 流控规则 V1</a>
</li>
7.去掉测试单元,重新打包项目,然后运行
java -jar sentinel-dashboard-1.8.6.jar
3.添加规则
在 开放出来的模块中添加流控规则,将会被同步到 Nacos,从而实现持久化
更多内容,参考Setinel官网