第05章-Sentinel集群容错
# 5.1 雪崩效应
- 如图所示,假设这是一个高并发的微服务系统,一开始所有服务都是正常的
- A服务挂了,导致B服务调用A服务失败,从B发往A的请求就会强制等待直至超时
- Java中一次请求往往对应一个线程,如果请求被强制等待了,线程就会被强制阻塞,一直到请求超时的时候这个线程才会被释放,由于这是一个高并发的系统,B服务上阻塞的线程会越来越多,而线程对应着计算机的资源,如内存、CPU
- 如果不做处理的话,B服务终将资源耗尽无法再创建新的线程,这样B服务也挂了
- 同样道理,C、D服务最后也会因为B服务而挂掉,这种基础服务故障导致上层服务故障就是服务的雪崩效应,也叫级联故障
# 5.2 常见的容错方案
- 超时
- 限流
- 舱壁模式
- 断路器模式
舱壁模式可参考:https://blog.csdn.net/dot_life/article/details/80823272
断路器三态转换:
# 5.3 整合Sentinel
# 5.3.1 整合工作
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
引入sentinel依赖后就整合好了。这里整合actuator是为了通过actuator/sentinel
这个endpoint检验时候已经整合sentinel,配合actuator暴露端点:
# actuator端点控制
management:
endpoints:
web:
exposure:
include: '*'
2
3
4
5
6
此时访问actuator/sentinel
,就能看到相关信息。
# 5.3.2 整合sentinel控制台
github下载对应版本的sentinel-dashboard的jar包
本地使用
java -jar sentinel-dashboard-1.8.0.jar
启动控制台为应用指定控制台的地址
spring: cloud: sentinel: transport: # 指定sentinel控制台的地址 dashboard: localhost:8080
1
2
3
4
5
6
此时访问控制台还是看不到应用信息,是因为sentinel是懒加载的,访问应用任意controller接口后即可在控制台观察到。
# 5.4 流控规则和效果
# 5.4.1 流控规则
通过控制台可以看到有3种流控模式
1、直接
表示当前资源达到阈值后触发流控
2、关联
当关联的资源达到阈值,就限流本资源
如图所示,表示当/actuator/sentinel
的QPS达到1后,就限流/shares/{id}
3、链路
针对指定入口资源做流控
如 /endpoint_1 和 /endpoint_2 都调用资源 common,可以针对入口资源 /endpoint_1 做流控。相当于细粒度的来源控制。
参考:sentinel流量控制 (opens new window)
# 5.4.2 流控效果
- 快速失败:直接失败,抛异常
- Warm Up:根据codeFactor(默认3)的值,从阈值/codeFactor,经过预热时长,才到达设置的QPS阈值
- 排队等待:匀速排队,让请求以均匀的速度通过,阈值类型必须设成QPS,香则无效
# 5.5 降级规则
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 - 异常比例 (
ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。 - 异常数 (
ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
参考:sentinel熔断降级 (opens new window)
# 5.6 热点参数规则
热点参数规则可对指定资源的参数做流控,以下例子
@RestController
@RequestMapping("/test")
public class TestController {
/**
* 测试sentinel热点参数限流的资源
*/
@GetMapping("/sentinel/param")
@SentinelResource("hot")
public String sentinelParamControl(@RequestParam(required = false) String a,
@RequestParam(required = false) String b) {
return a + " " + b;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
设置热点规则:
上图表示参数索引为0(第一个参数)的参数,在1s内QPS达到1,则触发限流
即访问
/test/sentinel/param?a=1&b=2
,达到阈值会触发限流
/test/sentinel/param?b=2
,不传热点参数,不会触发限流
# 5.7 授权规则
基于服务名配置黑名单白名单
# 5.8 系统自适应限流
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。可从控制台-系统规则处添加。
流控规则:
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。 - CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
# 5.9 代码配置流控规则
参考:Alibaba Sentinel 规则参数总结 (opens new window)
# 5.10 Sentinel控制台
# 5.10.1 Sentinel客户端和控制台通信
- 微服务需要继承transport模块,继承该模块之后,就把自己注册到控制台上,并定时发送心跳
- 此时控制台就知道各微服务的地址和通信端口等
如图是服务的机器列表,其中端口不是服务的server.port,是微服务和sentinel通信的端口,通过注册的IP和端口就可以实现通信,包括推送规则等。
# 5.10.2 控制台相关配置项
应用端连接控制台配置项:
spring.cloud.sentinel.transport:
#指定控制台的地址
dashboard: localhost:8080
#指定和控制台通信的IP#如不配置,会自动选择一个Ip注册
client-ip: 127.0.0.1
#指定和控制台通信的端口,默认值8719
#如不设置,会自动从8719开始扫描,依次+1,直到找到未被占用的端口
port: 8719
#心跳发送周期,默认值nul1
#但在simpleHttpHeartbeatSender会用默认值10秒
heartbeat-interval-ms: 10000
2
3
4
5
6
7
8
9
10
11
控制台配置项参考:sentinel控制台 (opens new window)
# 5.11 Sentinel API介绍
之前都是使用Sentinel自动将Spring MVC的端点作为资源的,其实Sentinel可以将任意代码作为资源保护起来。可以通过配置 spring.cloud.sentinel.filter.enable=false
关闭对Spring MVC端点的保护,即关闭自动将端点作为资源。
Sentinel核心API的SphU类,用于定义资源。
参考:Sentinel定义资源方式 (opens new window)
其他API:
业务异常统计 Tracer
Sentinel默认只会统计BlockException,其他异常不会统计异常数和异常比例,可以使用Tracer的API记录其他业务异常
上下文工具类 ContextUtil
用于标识调用链路入口,用于区分不同的调用链路
参考:Sentinel其他API (opens new window)
# 5.12 SentinelResource注解支持
Sentinel 提供了 @SentinelResource 注解用于定义资源
注意:注解方式埋点不支持 private 方法。
@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource
注解包含以下属性:
value
:资源名称,必需项(不能为空)entryType
:entry 类型,可选项(默认为EntryType.OUT
)blockHandler
/blockHandlerClass
: 降级处理的函数名称和类名,可选项fallback
/fallbackClass
:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。defaultFallback(since 1.6.0)
:默认的 fallback 函数名称,可选项exceptionsToIgnore(since 1.6.0)
:用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
具体配置参考:Sentinel注解支持 (opens new window)
# 5.13 Http客户端整合Sentinel
# 5.13.1 RestTemplate整合Sentinel
在RestTemplate的Bean定义方法上加@SentinelRestTemplate注解就可以将RestTemplate请求的标记为资源
@Bean
@SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class) //整合注解
public RestTemplate restTemplate() {
return new RestTemplate();
}
2
3
4
5
测试代码:
@Autowired
private RestTemplate restTemplate;
@GetMapping("/sentinel/rest_template/{userId}")
public UserDTO sentinelRestTemplate(@PathVariable Integer userId) {
return restTemplate.getForObject("http://user-center/users/{userId}", UserDTO.class, userId);
}
2
3
4
5
6
7
整合RestTemplate开关,resttemplate.sentinel.enable=true
,默认为true
参考:RestTemplate支持 (opens new window)
# 5.13.2 Feign整合Sentinel
#Feign整合Sentinel开关,默认为false
feign.sentinel.enable=true
2
// FeignClient接口
@FeignClient(name = "service-provider", fallback = EchoServiceFallback.class, configuration = FeignConfiguration.class)
public interface EchoService {
@RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
String echo(@PathVariable("str") String str);
}
// FeignClient配置
class FeignConfiguration {
@Bean
public EchoServiceFallback echoServiceFallback() {
return new EchoServiceFallback();
}
}
// FeignClient错误处理实现
class EchoServiceFallback implements EchoService {
@Override
public String echo(@PathVariable("str") String str) {
return "echo fallback";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 5.14 规则持久化
Sentinel提供两种规则持久化手段
- 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
- 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
参考:
- 动态规则扩展 (opens new window)
- 在生产环境中使用 Sentinel (opens new window)
- 拉模式示例 (opens new window)
- 推模式Nacos示例 (opens new window)
- 推模式Nacos详解 (opens new window)
# 5.15 集群流控
Sentinel 集群限流服务端有两种启动方式:
独立模式(Alone),即作为独立的 token server 进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。
嵌入模式(Embedded),即作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。
独立模式实现单个节点的流控,嵌入模式实现集群整体流控,控制整体集群的QPS。
部署token server切换到嵌入模式后即可在sentinel dashboard上配置集群流控。
# 5.16 Sentinel拓展
# 5.16.1 Sentinel错误页优化
Sentinel默认降级限流等都统一写出一句话响应,如何自定义错误响应:
/**
* BlockExceptionHandler接口用于处理限限流后处理
* 来版本接口名叫UrlBlockHandler
*/
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
// 根据不同的异常设置不同返回值
BlockErrorMsg errorMsg = null;
if (e instanceof FlowException) {
errorMsg = new BlockErrorMsg(100, "限流了");
} else if (e instanceof DegradeException) {
errorMsg = new BlockErrorMsg(101, "降级了");
} else if (e instanceof ParamFlowException) {
errorMsg = new BlockErrorMsg(102, "热点参数限流");
} else if (e instanceof AuthorityException) {
errorMsg = new BlockErrorMsg(103, "授权规则不通过");
} else if (e instanceof SystemBlockException) {
errorMsg = new BlockErrorMsg(104, "系统规则限流");
}
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.setContentType("application/json;charset=utf-8");
// 写出json响应
response.getWriter().write(new ObjectMapper().writeValueAsString(errorMsg));
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class BlockErrorMsg {
private Integer status;
private String msg;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
经测试上述规则对 @SentinelResource 注解标识的资源不起作用,注解资源需要用注解中的属性 blockHandler/blockHandlerClass,详见 [5.12 SentinelResource注解支持](#5.12 SentinelResource注解支持)
# 5.16.2 Sentinel区分来源
Sentinel提供了RequestOriginParser来源解析器接口,用来解析请求来源。只需要编写该接口实现类,就可按照指定规则解析请求来源。
/**
* 实现Sentinel的 RequestOriginParser 来源解析器接口,
* 解析来自HTTP请求的请求来源(例如IP,用户,appName)
*/
@Component
public class MyRequestOriginParser implements RequestOriginParser {
/**
* 解析给定HTTP请求的来源
* @param request http请求
* @return 解析出的来源,返回什么就表示来源是什么
*/
@Override
public String parseOrigin(HttpServletRequest request) {
// 此处将origin参数解析为请求来源,也可以规定使用header等其他信息
// String origin = request.getHeader("origin");
String origin = request.getParameter("origin");
if (StringUtils.isBlank(origin)) {
throw new IllegalArgumentException("error!");
}
return origin;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
其实就是根据规定将来源信息设置上,用以之后各种针对来源的流控规则校验。配置好以上来源解析后,就可以在控制台配置根据来源的流控规则了,会根据上述解析的来源判断是否限流。
# 5.16.3 Sentinel资源RESTful支持
对于RESTful请求,如 /shares/{id},对于不同的参数,Sentinel会将其视为不同的资源。为解决这种问题,可以使用 UrlCleaner 接口,使用方式如下
/**
* UrlCleaner:Sentinel提供用于重置用于标识资源Url的接口
*/
@Component
public class MyUrlCleaner implements UrlCleaner {
/**
* 重置Url
* @param originUrl 原始Url
* @return 返回用于标识资源的Url
*/
@Override
public String clean(String originUrl) {
// 让 /shares/1 和 /shares/2 都记作 /shares/{number},视为相同资源
String[] split = originUrl.split("/");
return Arrays.stream(split)
.map(e -> NumberUtils.isNumber(e) ? "{number}" : e) //数字转换
.reduce((a, b) -> a + "/" + b) //累加操作
.orElse("");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这里自定义的UrlCleaner实现类重新处理的请求的Url,将其视为相同的资源。
参考:Sentinel RESTful 接口流控处理优化 (opens new window)
# 5.16.4 Sentinel拓展的本质
上述三小节讲述了如下3个拓展接口
- BlockExceptionHandler:提供Sentinel异常处理
- RequestOriginParser:来源支持
- UrlCleaner:重新定义资源名称
这些拓展点的本质是由 SentinelWebInterceptor 调用的,利用Spring MVC的拦截器实现的。老版本使用的是过滤器CommonFilter,1.7.1版本之后被SentinelWebInterceptor代替(更新说明 (opens new window))。
所以可以修改SentinelWebInterceptor源码融入自己的逻辑。