雪崩问题
微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
雪崩问题产生的原因:
- 微服务相互调用,服务提供者出现故障或阻塞。
- 服务调用者没有做好异常处理,导致自身故障。
- 调用链中的所有服务级联失败,导致整个集群故障。
解决问题的思路:
- 尽量避免服务出现故障或阻塞。
- 保证代码的健壮性;
- 保证网络的畅通性;
- 保障应对较高的并发请求;
- 服务调用者做好远程调用的异常处理后备方案,避免故障的扩散。
服务保护方案
请求限流
限制访问接口的QPS请求并发量,避免服务因流量激增出现故障。
线程隔离
俗称仓壁模式,模拟船舱隔板的防水原理,通常限定每个业务能使用的线程数量而将故障业务隔离,避免故障扩散。
服务熔断
由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,则拦截该接口的请求。熔断期间,所有请求快速失败,全部都走fallback逻辑。
超时处理
设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。
服务保护技术
Sentinel是阿里巴巴推出的,而Hystrix出现的比较早期,由Netflix推出,兼容性和功能性都比较落后,Spring也有推出自己的技术方案Spring Cloud Circuit Breaker
,实现依赖于Resilience4J
和Spring Retry
这两个组件,目前常见应用于生产环境的还是Sentinel和Hystrix,但是需要注意的是Hystrix支持持SpringCloud 2020之前的版本,企业内部多数遵循能用老的就用老的,所以还是非常常见到Hystrix的身影。
Sentinel
Sentinel官方网站:https://sentinelguard.io/zh-cn/index.html
Sentinel 控制台
1、获取 Sentinel 控制台
方式一:可以从 release 页面 下载最新版本的控制台 jar 包。
方式二:可以从最新版本的源码自行构建 Sentinel 控制台:
- 下载 控制台 工程
- 使用以下命令将代码打包成一个 fat jar:
mvn clean package
2、启动 Sentinel 控制台
注意:启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
使用如下命令启动控制台:
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=0.0.0.0:8090 -Dproject.name=sentinel-dashboard -Dsentinel.dashboard.auth.username=admin -Dsentinel.dashboard.auth.password=123456 -Dserver.servlet.session.timeout=7200 -jar sentinel-dashboard-1.8.8.jar
参数选项解释:
-Dserver.port=8090
:用于指定 Sentinel 控制台端口为8090,不指定时默认为8090;
-Dcsp.sentinel.dashboard.server=0.0.0.0:8090
:用于指定 Sentinel 控制台监听地址为0.0.0.0:8090,不指定时默认为0.0.0.0:8090;
-Dproject.name
:用于指定 Sentinel 控制台显示名称,不指定时默认为sentinel-dashboard;
-Dsentinel.dashboard.auth.username
:用于指定 Sentinel 控制台访问的认证用户,不指定时默认为sentinel;
-Dsentinel.dashboard.auth.password
:用于指定 Sentinel 控制台访问的认证密码,不指定时默认为sentinel;
-Dserver.servlet.session.timeout
:用于指定 Sentinel 控制台会话超时时间,不指定时默认为7200;
3、访问 Sentinel 控制台
微服务整合
引入依赖
xml
<!-- SpringCloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
服务配置Sentinel
yaml
spring:
cloud:
sentinel:
# 取消控制台懒加载
eager: false
transport:
# 控制台地址
dashboard: 127.0.0.1:8090
# 开启请求方式前缀
http-method-specify: true
请求限流配置
找到我们要进行流控管理的微服务应用,然后按照路径"簇点链路 -> 流控 -> 对应的请求路径 -> 单机阈值",阈值类型以QPS每秒请求为单位,我们这里为了能够体现出效果,所以设置为每秒请求不能超过两次
设置好上述配置后,我们进行测试,发现每秒超出2次请求后,服务端会返回状态码为429
的Blocked by Sentinel (flow limiting)
结果,说明我们的配置生效了~
线程隔离配置
找到我们要进行流控管理的微服务应用,然后按照路径"簇点链路 -> 流控 -> 对应的Fegin客户请求 -> 单机阈值",阈值类型以并发线程数为单位,我们这里为了能够体现出效果,提前在对应的位置增加了sleep的操作,并设置并发线程不能超过两个
设置好上述配置后,我们进行测试,发现我们同时发送超出2次请求后,服务端会返回状态码为429
的Blocked by Sentinel (flow limiting)
结果,说明我们的配置生效了~
服务熔断配置
熔断降级是解决雪崩问题非常有效且重要手段,思路是由断路器
统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务,即拦截访问该服务的一切请求,而当服务恢复时,断路器会放行该服务的请求。
断路器熔断策略有三种:
- 慢调用比例:超过指定时长的调用为慢调用,统计单位时长内慢调用的比例,超过阈值则熔断
- 异常比例:统计单位时长内异常调用的比例,超过阈值则熔断
- 异常数:统计单位时长内异常调用的次数,超过阈值则熔断
上面配置的意思就是,当请求超过200毫秒的调用是慢调用,统计最近1000毫秒内的请求,如果请求量超过2次,并且慢调用比例不低于0.5,也就是两个请求里面有一半的请求命中,则触发熔断,熔断时长为20秒。然后进入half-open状态,放行一次请求做测试。
以下是熔断后返回的结果:
java
{
"code": 500,
"msg": "获取用户失败:null",
"data": null,
"time": "2024-10-30T15:36:40.456031"
}
授权规则配置
Sentinel是通过RequestOriginParser
这个接口的parseOrigin
来获取请求的来源的,授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问
想要实现授权规则的配置,那么我们就要在业务服务实现parseOrigin
接口,例如,我们尝试从request中获取一个名为origin
的请求头,作为origin
的值
java
package com.if010.common.nacos.config;
import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
/**
* Sentinel 授权规则实现类
* Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的
* 授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式
* @author Kim同学
*/
@Component
public class HeaderOriginParserConfig implements RequestOriginParser {
@Override
public String parseOrigin(javax.servlet.http.HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getHeader("origin");
if (StrUtil.isEmpty(origin)) {
return "blank";
}
return origin;
}
}
实现完接口后,我们还需要在gateway服务中,利用网关的过滤器添加名为gateway的origin头
yaml
spring:
cloud:
gateway:
default-filters:
# 添加名为origin的请求头,值为gateway
- AddRequestHeader=origin,gateway
最后我们配置上规则
配置好之后,我们测试可以发现除了头部请求携带了origin=if010-test
外,请求都会被拒绝掉,返回了fallback处理的结果
json
{
"code": 500,
"msg": "获取用户失败:[429 ] during [GET] to [http://if010-system/sys/user/8888888888888888888] [SysUserClient#getSystemUserInfoById(Long)]: [Blocked by Sentinel (flow limiting)]",
"data": null,
"time": "2024-10-30T21:32:35.326395"
}
FeignClient的两种Fallback方式
- 方式一: FallbackClass,无法对远程调用的异常做出处理
- 方式二: FallbackFactory,可以对远程调用的异常做处理,通常都会选择这种
Fallback降级处理实现
步骤一: 自定义类,实现FallbackFactory,编写对某个FeignClient的fallback逻辑
java
package com.if010.system.api.factory;
import com.if010.common.core.entity.R;
import com.if010.system.api.SysUserClient;
import com.if010.system.entity.bo.SysUserBo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
/**
* 【降级处理】系统用户远程调用出现异常时,熔断或超时处理
* @author Kim同学
*/
@Slf4j
public class SysUserClientFallbackFactory implements FallbackFactory<SysUserClient> {
@Override
public SysUserClient create(Throwable throwable) {
// 记录异常信息,可以返回空,或者直接抛出异常
log.error("系统用户服务远程调用异常:{}", throwable.getMessage());
// 创建 SysUserClient 接口的实现类,实现其中的方法,编写失败降级的处理逻辑
return new SysUserClient() {
@Override
public R<SysUserBo> getSystemUserInfoById(Long id) {
return R.fail("获取用户失败:" + throwable.getMessage());
}
};
}
}
步骤二: 将刚刚定义的FallbackFactory注册成一个Bean
注册成Bean的方式有很多种,具体根据自己的业务和系统框架进行调整,这里是抽取出来成为了一个模块,所以我们将注册bean是以imports文件方式定义
yml
/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.if010.system.api.factory.SysUserClientFallbackFactory
步骤三: 在服务接口中使用FallbackFactory
java
package com.if010.system.api;
import com.if010.common.core.entity.R;
import com.if010.system.api.factory.SysUserClientFallbackFactory;
import com.if010.system.entity.bo.SysUserBo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 【API】系统用户管理模块
* @author Kim同学
*/
@FeignClient(value = "if010-system", fallbackFactory = SysUserClientFallbackFactory.class)
public interface SysUserClient {
/**
* 【GET】获取系统用户列表
*/
@GetMapping("/sys/user/{id}")
R<SysUserBo> getSystemUserInfoById(@PathVariable("id") Long id);
}
步骤四: 测试
根据测试结果,我们可以看到返回了我们自己自定义的Fallback内容
json
{
"code": 500,
"msg": "获取用户失败:timeout executing GET http://if010-system/sys/user/8888888888888888888",
"data": null,
"time": "2024-10-30T14:40:33.814556"
}
持久化配置
当应用重启后,Sentinel 规则就消失了,生产环境需要将配置的规则进行持久化。
在官方文档中介绍的是API动态规则扩展
和DataSource动态规则扩展
,API动态规则扩展指的是通过Sentinel提供的API接口实现直接修改loadRules
,而DataSource动态规则扩展指的是通过拉模式或者推模式进行获取配置文件或者数据
DataSource扩展常见的实现方式
- 拉模式: 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
- 推模式: 规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
Sentinel支持的数据源扩展
接下来我们以 推模式,使用Nacos配置规则 为例:
添加依赖
xml
<!-- Sentinel Nacos 规则动态扩展持久化依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>x.y.z</version>
</dependency>
Nacos规则配置
请求限流规则配置
json
[
{
// 资源名
"resource": "GET:/sys/user/{id}",
// 针对来源,若为 default 则不区分调用来源
"limitApp": "default",
// 限流阈值类型(1:QPS | 0:并发线程数)
"grade": 1,
// 阈值
"count": 1,
// 是否是集群模式
"clusterMode": false,
// 流控模式(0:直接 | 1:关联 | 2:链路)
"strategy": 0,
// 流控效果(0:快速失败 | 1:Warm Up(预热模式) | 2:排队等待)
"controlBehavior": 0,
// 预热时间(秒,预热模式需要此参数)
"warmUpPeriodSec": 10,
// 超时时间(毫秒,排队等待模式需要此参数)
"maxQueueingTimeMs": 500,
// 关联资源、入口资源(关联、链路模式)
"refResource": "rrr"
}
]
服务熔断规则配置
json
[
{
// 资源名
"resource": "GET:/sys/user/{id}",
// 针对来源,若为 default 则不区分调用来源
"limitApp": "default",
// 熔断策略(0:慢调用比例 | 1:异常比率 | 2:异常计数)
"grade": 0,
// 最大RT(毫秒)、比例阈值[0.0,1.0]、异常数
"count": 200,
// 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)
"slowRatioThreshold": 0.5,
// 最小请求数
"minRequestAmount": 5,
// 单位统计时长(类中默认1000毫秒)
"statIntervalMs": 1000,
// 熔断时长(单位为秒)
"timeWindow": 10
}
]
授权访问规则配置
json
[
{
// 资源名
"resource": "sentinel_spring_web_context",
// 流控应用
"limitApp": "test,test2",
// 授权类型(0代表白名单|1代表黑名单)
"strategy": 0
}
]
网关 - 请求限流规则
json
[
{
// API名称、API分组名称
"resource": "if010-service",
// API类型 (0: RouteID | 1: API分组)
"resourceMode": 1,
// QPS阈值
"count": 5,
// 阈值类型
"grade": 1
}
]
网关 - API分组规则
json
[
{
// API分组名称
"apiName": "if010-service",
// 定义规则组,可以配置多条规则
"predicateItems": [
{
// 匹配串, 这里表示匹配所有
"pattern": ".*",
// 匹配模式 (0: 精确 | 1: 前缀 | 2:正则)
"matchStrategy": 2
}
]
}
]
业务服务配置文件
yaml
spring:
cloud:
sentinel:
# 取消控制台懒加载
eager: false
# 开启请求方式前缀
http-method-specify: true
transport:
# 控制台地址
dashboard: 127.0.0.1:8849
datasource:
# [自定义] 配置数据源名称(流控规则)
flow:
nacos:
# Nacos服务地址
server-addr: 127.0.0.1:8848
# Nacos认证用户
username: nacos
# Nacos认证密码
password: 123456
# Nacos配置所属命名空间 (默认为空,为空则表示使用的是public)
namespace: ""
data-id: ${spring.application.name}-flow-rules
# Nacos配置所属Group
group-id: SENTINEL_GROUP
# Nacos配置格式
data-type: json
# flow(请求限流规则)、degrade(服务熔断规则)、authority(授权访问规则)、param-flow(热点规则)
# 网关特殊的规则: gw-api-group(API分组规则)、gw-flow(gw-flow为网关流控,flow为普通流控)
rule-type: flow
到此就算是配置完成啦,重新启动服务,我们可以看到规则还是存在的,但是有个小缺陷就是Sentinel控制台的新增、修改、删除等操作是无法保存到Nacos中的,解决办法有很多,可以Clone官方的代码下来进行修改,详情可以参考 星空流年-Sentinel规则持久化到Nacos及规则数据双向同步 这篇文章,根据这篇文章流控规则可以完美的实现效果,但是其他的授权规则、系统规则小编试了好像业务并不生效,主要是格式的问题,有兴趣的可以自行研究一下,另外一种办法就是自己写业务逻辑实现Nacos规则拉取回显然后修改推送,方法千千万,合适自己的业务场景最重要~