目录
雪崩问题

在微服务中,当服务D宕机,服务A去调用服务D时,由于服务A等待服务D的返回结果而阻塞,在阻塞期间不会释放tomcat的连接资源,当持续的调用服务D请求发送,tomcat的资源总会被耗尽。这时调用服务B与C的请求也会阻塞在服务A中,导致服务A也宕机。

解决雪崩问题有如下四种解决方案
超时处理
设置超时时间,请求超过一定时间没有响应就返回错误信息,不会阻塞。
但这只是缓解了雪峰问题,当每秒请求量大于每秒释放量时,还是会占用很多tomcat资源。

线程隔离
限定每个业务能使用的线程个数,避免耗尽整个tomcat的资源。
虽然解决了雪崩问题,但还是存在资源浪费,如果服务C真的宕机,那么请求C的资源就被浪费了。

熔断降级
由断路器统计业务执行的异常比例如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
是一种比较好的解决雪崩问题的一种方式

流量控制
限制业务访问的QPS(每秒钟处理的请求数量),避免业务因流量的突增而故障
属于预防雪崩问题的发生,通过Sentinel控制向服务发起请求的频率

服务对比
| Sentinel | Hystrix | |
| 隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
| 熔断降级策略 | 基于慢调用比例或异常比例 | 基于失败比率 |
| 实时指标实现 | 滑动窗口 | 滑动窗口(基于RxJava) |
| 规则配置 | 支持多种数据源 | 支持多种数据源 |
| 扩展性 | 多个扩展点 | 插件的形式 |
| 基于注解的支持 | 支持 | 支持 |
| 限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 |
| 流量整形 | 支持慢启动、匀速排队模式 | 不支持 |
| 系统自适应保护 | 支持 | 不支持 |
| 控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
| 常见框架的适配 | Servlet、Spring cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
- 线程池隔离:对每个服务创建一个独立线程池,会存在线程数量大于CPU核数的情况,而线程切换又占用CPU资源,性能存在损失
- 信号量隔离:只有一个线程池,对业务已经使用的线程进行统计,当到达一定数量后,不会对该业务分配更多的线程。
- 慢调用比例:某个业务的请求耗时量久的数量与该业务总请求数量的比例
安装Sentinel控制台
下载地址:https://github.com/alibaba/Sentinel/releases
下载好后,放在无中文目录下启动
Java -jar sentinel-dashboard-你的版本.jar
访问8080端口,默认账号密码均为sentinel。
如果需要修改默认端口或是账号密码,需要添加启动参数
| 配置项 | 默认值 | 说明 |
| server.port | 8080 | 服务端口 |
| sentinel.dashboard.auth.username | sentinel | 默认用户名 |
| sentinel.dashboard.auth.password | sentinel | 默认密码 |
java -Dserver.port=8088 -jar sentinel-dashboard-你的版本.jar
更多配置请参考Sentinel文档
案例
demo下载地址:day01微服务保护
下载完demo之后,启动nacos服务。
引入sentinel依赖
<!--引入sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
添加配置内容
spring:
cloud:
sentinel:
transport:
dashboard: localhost:你的sentinel启动端口
启动服务并访问地址:localhost:8080/order/101
观察sentinal的控制台出现如下界面

簇点链路
就是项目中的调用链路,链路中被监控的每个接口就是一个资源。默认情况下sentinel会监控SpringMVC的每一个端点(可以理解为Controller的每个方法),因此SpringMVC的每一个端点就是调用链路中的一个资源。

限流规则
流控模式
流控的三种模式
- 默认是直接模式。也就是说,当前资源到达阈值时,对当前资源限流。
- 关联模式:统计与当前资源相关联的另一个资源,当触发阈值时,对关联的资源进行限流
- 链路模式:统计从指定链路访问到本地资源的请求,触发阈值时,对指定链路限流。

直接模式

超出阈值的请求会被拦截并报错。
这里我们将阈值设置为5。同时使用jemter模拟并发问题


启动查看运行结果

成功5次。失败5次。查看Sentinel的控制台

关联模式
使用场景:当用户支付时需要修改订单状态,同时也要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是支付和更新订单信息。支付的优先级要高于更新订单,因此当修改订单业务触发阈值时,需要对查询订单业务限流。

当update方法到达阈值后,会对query方法限流。
update与query方法的代码如下
@GetMapping("/query")
public String queryOrder(){
return "查询订单成功";
}
@GetMapping("/update")
public String updateOrder(){
return "修改订单成功";
}
接下来,对update进行压测,同时访问query方法。



可以看到,updata请求全都可以进行访问。但query方法被限流
使用条件:
- 一个优先级高,一个优先级低
- 两个有竞争关系的资源
链路模式
使用场景:查询订单和创建订单业务,两者都需要查询商品,当查询订单并发较大时,也会影响创建订单的业务。因此可以针对从查询订单进入到查询商品的请求统计,并设置限流。
Controller代码如下
@GetMapping("/query")
public String queryOrder(){
orderService.queryGoods();
return "查询订单成功";
}
@GetMapping("/save")
public String saveOrder(){
orderService.queryGoods();
return "创建订单";
}
Service代码如下
@SentinelResource("goods")
public String queryGoods() {
return "查询商品";
}
访问两个接口查看Sentinel控制台

发现goods只在save链路上,而query中不存在goods。这是因为Sentinel对Controller进行了context整合,Sentinel将/query与/save整合到sentinel_spting_web_context同一链路下,goods无法在同一链路下出现两次。因此导致链路模式的限流失效。
解决方案,添加配置内容
spring:
cloud:
sentinel:
web-context-unify: false
重启项目,再次访问两个接口。

添加流控规则

使用jmeert进行测试




前者为save链路的请求结果,后者为query的请求结果。
说明:
- Sentinel默认只会监控Controller中的资源。如果需要对Service中的资源进行监控需要添加注解@SentinelResource
- 在Sentinel的1.6.3版本开始,Sentinel Web filter默认收敛所有的URL入口context,导致链路限流不生效。但在1.7.0本版开始,在CommonFilter引入了WEB_CONTEXT_UNIFY参数,用于控制是否收敛context。
流控效果
流控效果是指,当请求达到流量阈值时的采取措施。包括三种:
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowExeception异常。是默认的处理方式。
- warm up:预热模式。对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大值。
- 排队等待:让所有请求按照先后次序排队执行,两个请求的间隔不能小于指定时长。
Warm up
是应对服务冷启动的一种方案,请求阈值初始值是【最大阈值/冷启动因子】,持续指定时长后,主键提高到最大阈值,默认冷启动因子为3。
例如最大阈值为10,预热时间为5s。那么初始阈值为10/3。然后再5内后逐渐增长到10

案例实现:
添加流控效果

启动jmeter测试

观察Sentinal控制台

可以看到随着时间增加不断增加通过QPS数量。
排队等待
让所有请求进入一个队列,按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
例如QPS等于5,那么每200ms处理队列中的一个请求,timeout=2000,意味着预期等待超过2秒的请求会被拒绝并抛出异常

案例实现:
编辑流控规则如下

启动jmeter进行测试

观察Sentinel控制台



可以看到。随着时间的增加,响应时间也逐渐增加,同时也出现超过最大等待时间的请求被拒绝。
热点参数限流
统计参数值相同的请求,判断其QPS是否超过了阈值

上图的配置含义为:对hot资源的第一个参数做统计,每秒相同参数值的请求数量不能超过5。
需要注意的是,热点参数对默认的SpringMVC资源无效。(Sentinel的热点参数功能是针对特定的接口和方法的,它通过统计接口的调用次数、流量控制、熔断降级等功能来保护服务的稳定性和可靠性。然而,默认的 SpringMVC 资源(例如 @RequestMapping 注解)并不是针对特定接口和方法的,而是针对整个 URL 的)
需要添加@SentimentResource注解
@SentinelResource("hot")
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId, @RequestHeader(value = "gateway", required = false) String str) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}

热点规则的高级设置需要自己新建。

启动jmeter测试




观察Sentinel控制台

隔离与降级
Feign整合Sentinel
之所以使用Feign来整合Sentinel是因为无论是隔离还是降级都是保护调用方的服务能够正常运行。因此可以在调用方添加保护。
案例实现
引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
配置文件
feign:
sentinel:
enabled: true
编写FeignClient编写失败后的降级逻辑
- FallbackClass:无法对远程调用的异常做处理
- FallbackFactory:可以对远程调用的异常做处理。通常使用这种。
编写FallbackFactory代码
@Slf4j
public class UserClientFallback implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
@Override
public User queryByUserId(Long id) {
//编写降级逻辑
log.info("查询用户失败:",throwable);
//返回默认值或空值
return new User();
}
};
}
}
对Client指定对应的降级类
@FeignClient(value = "user-server",fallbackFactory = UserClientFallback.class)
public interface UserClient{
@GetMapping("/user/{id}")
User queryByUserId(@PathVariable("id")Long id);
}
将UserClientFallback加载为
启动并访问地址:localhost:8080/order/101。
观察Sentinel控制台。

线程隔离
- 线程池隔离
- 信号量隔离(Sentinel的默认实现)

两种隔离方式对比
- 线程池隔离支持主动超时(通过线程的超时时间控制),支持异步调用。信号量隔离不支持主动超时,不支持异步调用
- 线程池隔离线程的额外开销较大。而信号量隔离则比较轻量,无额外开销。
- 线程池隔离适用于低扇出(该服务能够调用的其他服务较少)。信号量隔离适用于高频调用,高扇出
规则设置

使用jmeter进行测试


虽然测试全部通过,但是有些响应是通过降级逻辑返回。
熔断降级
熔断降级的思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。当服务恢复正常则会放行该服务的请求。
断路器的三种状态:
- Closed:关闭状态,统计异常比例
- Open:当达到失败阈值后由closed状态转为open状态。
- Half-Open:当熔断时间结束,进入半开状态,放行一次请求,如果失败则回到Open状态,成功则进入Closed状态。

慢调用
响应时长大于指定时长的请求被认为慢调用。在指定时间内,如果请求数量超过设定的最小数量,且慢调用比例到达阈值就会触发断路器。
规则设置

修改代码,让线程休眠60ms
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
if (id == 1) {
Thread.sleep(60);
}
return userService.queryById(id);
}
对id为1的请求快速访问5次,接着访问id为2的请求。

可以看到,id为2的用户也查询不到数据了。等待五秒后再次访问id为2的请求

异常比例、异常数
统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
规则设置

修改代码如下
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
if (id == 1) {
Thread.sleep(60);
}
if (id ==2){
throw new RuntimeException("抛出错误");
}
return userService.queryById(id);
}
快速访问id为2的请求5次,然后访问id为3的请求发现,也无法查询到数据

等待5秒后再次访问可以正常访问。
授权规则
对请求者身份进行判断。在白名单的调用者可以访问,在黑名单的调用者不允许访问
与Gateway的区别:Gateway是对网关端口访问的身份校验。不对服务器访问者的身份进行校验。Sentinel是对当外部通过直接访问服务器地址的身份校验。通常来讲,我们只会在白名单中设置gateway的地址。
案例实现
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源。由于Gateway与浏览器发起的请求来源默认都是default,因此,Sentinel无法识别哪些请求来自网关,哪些请求来自浏览器。对此,我们要实现该接口,对网关请求与浏览器请求进行区分。
在Gateway中,可以通过添加过滤器,对每个经过网关的请求添加请求头
spring:
cloud:
gateway:
routes: # 路由地址
- id: order-service # 路由唯一标识
uri: lb://order-service #路由目标地址
predicates: # 路由断言。判断请求是否符合规则
- Path=/order/**
filters:
- AddRequestHeader=origin,gateway
添加请求来源处理器
@Component
public class HandlerOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getHeader("origin");
if (StringUtils.isEmpty(origin)){
origin = "blank";
}
return origin;
}
}
规则设置

访问8080服务器端口,发现无法正常访问。

访问10010网关端口。可以正常访问

自定义异常结果
无论是流控还是熔断又或是热点与授权管理,得到的都是限流异常(flow limiting)。这对用户来说不很友好,我们需要针对不同异常返回不同的异常结果。具体实现思路是实现BlockExceptionHandler接口。
常见的BlockException有如下五类
| 异常 | 说明 |
| FlowException | 限流异常 |
| ParamFlowException | 热点参数限流的异常 |
| DegradeException | 降级异常 |
| AuthorityException | 授权规则异常 |
| SystemBlockException· | 系统规则异常 |
实现代码如下
@Component
public class SentinelExceptionHandler 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+"}");
}
}
规则持久化
Sentinel默认将规则保存在内存当中。因此每次重启服务后都需要重新设置规则。Sentinel控制台规则管理有如下三种模式
原始模式
Sentinel默认的模式,将规则保存在内存当中,重启后服务丢失
Pull模式
控制台将配置的规则推送到Sentinel的客户端,客户端会将规则保存在本地文件或数据库中。微服务会定时去本地文件或数据库中查询规则是否改变。来更新本地规则。
缺点:时效性比较差。在集群中,存在一个服务器更改规则而其他服务器还没有进行更改的情况。

Push模式
控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听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-rules
groupId: SENTINEL_GROUP
rule-type: flow # 还可以是:degrade、authority、param-flow
接下来修改Sentinel的源码。下载Sentinel的源代码文件
下载地址:https://github.com/alibaba/Sentinel/tags
下载好后使用IDEA打开(时间较久),修改模块sentinel-dashboard中的pom文件
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!--<spoce>test</spoce> 注释掉-->
</dependency>
添加nacos的支持。sentinel-dashboard模块下的test文件夹下已经编写好对nacos的支持,我们需要拷贝到main下


修改nacos地址


修改nacos数据源

修改前端页面

注释打开并修改其中内容


打包

运行启动

访问Sentinel端口并进行清除缓存刷新。
点击F12后,右键刷新键


测试
在新出现的选项中添加流控规则。

点击新增,后观察nacos的控制台。出现如下配置文件

现在即使重启服务器。Sentinel的信息也不会丢失。
575

被折叠的 条评论
为什么被折叠?



