文章目录
一、Sentinel与雪崩问题
1.1 雪崩问题与解决方案
- 服务A依赖服务B
- 服务B故障,导致A阻塞
- 导致A不会释放tomcat连接
- 长期则导致A资源耗尽,A故障
- 依赖A的服务同理
于是类似滚雪球,故障的范围越来越大。
处理思路:
- 超时处理:设定超时时间,请求超过一定时间没有响应则返回错误信息,不会无休止等待。该方式为缓解作用。
- 舱壁模式:限定每个业务能使用的线程数目,避免耗尽整个tomcat的资源,因此也就线程隔离。浪费了一些资源。
- 熔断降级:有断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
- 流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。QPS:每秒钟请求的数量。预防服务故障。
用Sentinel来控制QPS。
1.2 Sentinel 与 Hystrix
- 线程池隔离:给每一个独立的业务都会有个独立的线程池,通过单独的线程池来限制。
- 信号量隔离:统计当前业务使用了几个线程,并限制业务只能使用几个线程,一但超过就被限制。
- 基于慢调用比例:依据业务大多数情况下处理得是否很慢,来判断是否熔断。如果该业务大多数情况下都很忙,则认为会拖垮整个服务,因此在请求很多时熔断它。
- 流量整形:指让突发流量变为匀速流量,让服务处理起来较轻松。
Sentinel | Hystrix | |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于慢调用比例或异常比例 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 |
流量整形 | 支持慢启动、匀速排队模式 | 不支持 |
系统自适应保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 支持查看,不支持动态配置,不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC等 | Servlet、Spring Cloud Netflix |
1.3 安装部署
alibaba/Sentinel
下载如下文件即可。
下载完成解压后,就可以直接用运行即可
账号密码都是sentinel
java -jar sentinel-dashboard-1.8.1.jar
1.4 整合
在SpringBoot的消费中,导入如下依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
设置yml文件
spring:
sentinel:
transport:
dashboard: localhost:8080
如果启动不了或者显示循环依赖,请参照下面版本进行调整
版本说明
如果还是不行,添加如下语句,允许循环依赖
spring:
main:
allow-circular-references: true
配置完成后,我们使用消费者多次访问提供者后,再查看Sentinel,可以看到如下结果。
1.5 限流规则
- 簇点链路:项目内的调用链路。比如controller->service->mapper,就是一个链路。链路中被检控的每个接口就是一个资源,比如默认Sentinel会监控SpringMVC中的每一个端点——Controller中的方法。
流量控制、熔断等都是针对簇点链路中的资源拉设置的。
用设置流控,单机阈值为5,然后用Jemeter测试
结果:
1.5.1 流控模式
- 直接:统计当前资源的请求,出发阈值时,对当前资源直接限流,默认模式
- 关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
- 链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流
1.5.2 关联
一般用于竞争关系,一个优先级较高,一优先级低。但优先级高的触发阈值时,优先级低的被限制。
我们为消费者控制层新增两个方法
@GetMapping("/query")
public String queryOrder(){
return "查询";
}
@GetMapping("/update")
public String updateOrder(){
return "更新";
}
}
访问一下,后
我们想让update达到一定次数后,query被限流。
我们想要给谁限流,就给谁加规则。
现在进行测试
1.5.3 链路
- 当达到阈值时,限制某一条链路。可以理解为,某一个方法被很多模块调用,可以通过限制被哪一个模块调用的数量,来保证他模块顺利调用。
- 有查询订单和创建订单业务,两者都要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。
在Service层添加一个查询的方法
/**
* 手动标记该方法为资源
* */
@SentinelResource("goods")
@Override
public void queryGoods() {
System.out.println("查询商品");
}
为控制层添加
@GetMapping("/query")
public String queryOrder(){
orderService.queryGoods();
return "查询";
}
@GetMapping("/save")
public String saveOrder(){
orderService.queryGoods();
return "新增完成";
}
修改
sentinel:
transport:
dashboard: localhost:8080
web-context-unify: false #关闭context整合,Sentinel默认会把Controller的方法做context。导致链路模式的流量失效
接下来访问一下,
分别请求
可以看到,query被限制,但是update没有被限制
query
update
1.5.4 控制效果
- 快速失败:达到阈值后,请求被立即拒绝,并抛出FlowException异常,为默认处理方式。
- 预热模式 warm up:对超出阈值的请求,同样是拒绝跑出异常。但它的阈值会动态变化,从一个较小值逐渐增加到最大阈值。
- 请求初始阈值 = threshold(最大阈值)/coldFacotor(冷启动因子),持续指定时长后,逐渐提高到threshold值。coldFacotor默认是3,即刚开始阈值为1/3*threshold,在设定的预热时间过后,会变为threshold。
- 排队等待:让所有的请求进入一个队列中,然后按照阈值允许的时间间隔依次执行,后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
- 比如:QPS = 5(每秒最多处理5个),意味着每200ms处理队列中的一个请求。当timeout=2000,即最大等待时间为2000ms,超过求拒绝并抛出异常。
- 当前面有10个请求,且第一个刚开始处理时,第11个会直接被拒绝。
- 流量整形,让到达的流量稳定。
1.5.5 热点参数限流
分别统计参数值相同的请求,判断是否超过阈值。
- 参数索引:统计第几个参数
- 单机阈值:就是最多多少个(集群的话就是均摊阈值,分摊到每个多少)
- 统计窗口时长:每秒内单机阈值不能超过
参数例外项,也即是会有一些参数需要单独配置,可能它们需要高于或者低于其他参数。
如果没有高级选项,从左侧的热点规则一栏进去新增。
- 注意:热点参数限流对默认的SpringMVC资源无效,我们需要手动标记。方式同链路一样,打一个注解即可。
1.6 隔离与降级
1.6.1 Feign整合Sentinel
首先在配置中打开feign的sentinel功能。
feign:
client:
config:
user-service:
logger-level: BASIC
sentinel:
enabled: true
随后编辑要降级的逻辑,有两种方式
- 方式一:FallbackClass,无法对远程调用做异常处理
- 方式一:FallbackFactory,可以对远程调用的异常做处理
package com.config;
import com.model.User;
import com.service.UserClient;
import org.springframework.cloud.openfeign.FallbackFactory;
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public User getUserById(Long id) {
System.err.println("查询失败:" + cause);
return new User();
}
};
}
}
package com.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Config {
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
}
重启后,运行,访问,就可以看到
接下来就可以进行配置了
1.6.2 线程隔离
如下图设置即可,就完成了舱壁模式。
1.6.3 熔断降级
1.6.3.1 熔断策略
有三种:
-
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数目超过设定的最小数量,慢调用比例大于设定阈值,则出发熔断。
- RT:最大响应时长,超过就算慢调用
- 比例阈值:慢调用比例超过了0.5,就算熔断
- 熔断时长:每次熔断持续5s
- 最小请求数:每次统计最小要有5个
- 统计时长:统计每1000ms内的请求
- 即在统计时长内且超过最小请求次数的所有请求,会被用来计算比例阈值。
-
异常比例:统计指定时间内的调用,如果调用次数超过指定请求数目,并且出现异常比例达到比例阈值,则触发熔断。
-
异常数:统计指定时间内的调用,如果调用次数超过指定请求数目,并且出现异常数目超过指定异常数目,则触发熔断。
二、Sentinel 授权规则
为什么需要?避免服务地址暴露,被访问者绕过网关直接访问。
白名单:来源(origin)在白名单的调用者允许访问
黑名单:来源(origin)在黑名单的调用者不允许访问
2.1 授权规则
Setinel 通过 RequestOriginParser 这个接口的parseOrigin来获取请求来源,但无论是走网关还是直接访问默认都是default,无法区分。
因此我们需要自己去实现它。
public interface RequestOriginParser{
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
为消费者实现该方法。
package com.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import com.alibaba.nacos.common.utils.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
/**
* 如果浏览器或者网关获取的origin头不一样,那么就可以区分
* */
String origin = httpServletRequest.getHeader("origin");
if(StringUtils.isEmpty(origin)){
return "blank";
}
return origin;
}
}
为网关添加过滤器,将我们上面的来源加上
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes: # 网关路由配置
- id: user-service #路由id,自定义,但需要唯一
uri: lb://user-service #路由目标地址,lb即负载均衡,后面跟着服务名称
# uri: http://127.0.0.1:8001 # 也可以这样写,但是地址固定,不推荐
predicates: # 路由断言,即判断是否符合要求
- Path=/user/** # 这个规则是,只要以/user/开头,就算符合要求
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=origin,gateway # , 是等于的意思
重启后
2.2 自定义异常
所有异常默认的都是限流异常,我们接下来自定义异常,避免抛异常时不知道是哪里出错
public interface BlockExceptionhanlder{
/**
* 处理请求被限流、降级、授权拦截时抛出的异常
*/
void handle(HttpServletRequest request,HttpServletResponse response,BlockException e) throws Exception;
}
BlockException包含很多子类
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowExcetion | 热点参数限流异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
package com.config;
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 SentinelBolckhandler 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 DegradeException){
msg = "请求被降级了!";
}
else if(e instanceof ParamFlowException){
msg = "热点参数被限流了!";
}
else if(e instanceof AuthorityException){
msg = "请求没有权限!";
status = 401;
}
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(status);
httpServletResponse.getWriter().println("{\"message\":\""+msg+"\",\"status\":"+status+"}");
}
}
2.3 规则持久化
有三种模式:
- 原始模式: 默认模式,规则保存在内存之中,重启服务会丢失
- pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或者数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。缺点就是不是实时的,导致数据不一致。
- push模式:控制台将配置规则推送到远程配置中心(nacos)。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。该方式实时更新,推荐该模式。
但是该模式最为复杂,因为需要修改Sentinel控制台源代码。
我们为消费者引入如下依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
spring:
application:
name: order-service
profiles: # 环境
active: dev
cloud:
nacos:
config:
enabled: true
server-addr: localhost:8848 #nacos地址
file-extension: yaml #文件后缀名
sentinel:
transport:
dashboard: localhost:8080
web-context-unify: false #关闭context整合,Sentinel默认会把Controller的方法做context。导致链路模式的流量失效
datasource:
flow:
nacos:
server-addr: localhost:8848
data-id: orderservice-flow-rules
group-id: SENTINEL_GROUP
rule-type: flow #此处我们选择限流,也可以选:degrade、authority、param-flow
随后重启服务
下载源码
alibaba/Sentinel
接下来我们修改源码:
在源码中找到dashboard
在其中如下图进行注释
将该类的test中的nacos,复制到main中对应的位置
在main中,修改nacosConfig类
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import java.util.List;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Eric Zhao
* @since 1.4.0
*/
@Configuration
@ConfigurationProperties(prefix = "nacos")
public class NacosConfig {
private String 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);
}
@Bean
public ConfigService nacosConfigService() throws Exception {
return ConfigFactory.createConfigService(addr);
}
public String getAddr(){
return addr;
}
public void setAddr(String addr){
this.addr = addr;
}
}
随后在Properties文件中,添加Nacos地址
nacos.addr=localhost:8848
接着修改数据源
修改如下
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
接着修改前端页面
取消该部分注释
<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>
随后打包
启动后如下:
点击流控规则Nacos,并添加一个规则
可以看到多出来了一个规则
同理,如果要改其他的,也是需要把页面进行修改。
参考文献
[1]黑马程序员Java微服务