前言
Sentinel 组件生效于每个微服务节点,每个服务节点都可以对节点中某些资源进行防护配置。
Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。
也就是说,Sentinel 配置有两种方式:一种是基于核心库进行代码的配置,一种是基于管控台的配置。基于管控台的配置都会通过网络通信同步到服务节点,服务节点在通过核心库修改节点在内存中对应资源的防护配置。也就是说,管控台将配置的操作进行了可视配置化
。代码配置的方式可以查看官方文档,下面会着重以管控台的方式进行配置。
Dashboard 安装
官方下载地址,根据项目使用的 SpringCloudAlibaba 版本下载对应的版本,我使用的是 v1.8.6 版本。
下载成功后,启动 Sentinel:java -Dserver.port=8400 -Dcsp.sentinel.dashboard.server=127.0.0.1:8400 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar
启动命令中有两个端口分别解释一下:
- -Dserver.port 即管控台的端口,本质上管控台是一个 SpringBoot 开发的项目
- -Dcsp.sentinel.dashboard.server 用于推送管控台最新更新的后台服务端口,业务节点根据此端口进行通信。
可见,管控台启动时,实则会监听两个端口。
启动成功后访问:http://127.0.0.1:8400/
,默认账号与密码:sentinel
集成 Sentinel
引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
bootstrap.yaml
spring:
main:
allow-bean-definition-overriding: true
cloud:
nacos:
...
# 服务监控
sentinel:
transport:
# 控制台的地址(ip:port)
dashboard: 127.0.0.1:8400
# 与控制台通讯的端口,默认是8719,不可用会一直+1
# 用于客户端暴露 sentinel 原生 api 的访问端口,用于管控台获取客户端簇点链路、监控数据、更新规则等
port: 6601
# 和控制台保持心跳的ip地址
client-ip: 127.0.0.1
# 发送心跳的周期,默认是10s
heartbeat-interval-ms: 3000
# 禁止收敛URL的入口 context
web-context-unify: false
# 饿加载: 服务启动后立即注册 Sentinel server 中,而非必须请求一次后才进行首次注册
eager: true
服务节点与管控台的通信流程如下:
- 节点启动时根据管控台地址进行注册
- 节点另启动一个端口,用于接收用户修改的最新防护规则
- 管控台会根据服务节点另启动的端口,当修改防护规则时,将最新的内容推送给服务节点(具体是 http 还是基于 netty 的 RPC 通信取决于配置 )
配置资源
现在 Sentinel 的管控台已经有了,那么我们要对什么进行监控与防护呢?资源。
资源可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。
新建服务节点 sentinel-service
,端口为 6600。我们新增一个接口,并配置其为 Sentinel 中的资源:
编程式
@RestController
public class SentinelController {
public static final String RESOURCE1_NAME = "resource1";
public static final String RESOURCE2_NAME = "resource2";
@GetMapping(value = "/sentinel/" + RESOURCE1_NAME)
public String resource1() {
Entry entry = null;
try {
// 1. 编码式的指定获取资源
entry = SphU.entry(RESOURCE1_NAME);
return "success";
} catch (BlockException e) {
return "sentinel exception";
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
AOP 注解配置式
// 1. 注入处理 @SentinelResource 注解的切面
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
// 2. 通过注解的形式进行资源配置
@SentinelResource(value = RESOURCE2_NAME)
@GetMapping(value = "/sentinel/" + RESOURCE2_NAME + "/{string}")
public String resource2(@PathVariable String string) {
int i = 1 / 0;
return "success ->" + string;
}
实际应用中,很少以编程式的方式配置资源,基本都是基于注解的形式进行配置,这样可以避免模板代码。
配置规则
有了需要防护的资源之后,就需要配置防护规则。防护规则的配置有两种方式:
- 编码配置
// 栗子:配置防护规则为流控规则,限制资源访问的 QPS 最多为1
private void init() {
List<FlowRule> flowRuleList = Arrays.asList(
new FlowRule(RESOURCE1_NAME).setGrade(RuleConstant.FLOW_GRADE_QPS).setCount(1),
new FlowRule(RESOURCE2_NAME).setGrade(RuleConstant.FLOW_GRADE_QPS).setCount(1)
);
flowRuleList.addAll(FlowRuleManager.getRules());
FlowRuleManager.loadRules(flowRuleList);
}
- 管控台配置:通过页面的形式配置防护规则的各个细节,后面在介绍防护模式时会详细说明
配置防护处理逻辑
资源与规则都配置好了,那么假设当某个资源的访问量激增 QPS 过高,触发了防护规则防止服务节点挂掉,该如何去编写具体的防护处理逻辑?
独立处理
在 @SentinelResource 中配置处理异常的回调方法:
@SentinelResource(value = RESOURCE2_NAME,
blockHandler = "handleSentinelException", blockHandlerClass = SentinelController.class,
fallback = "handleBusinessException", fallbackClass = SentinelController.class
)
@GetMapping(value = "/sentinel/" + RESOURCE2_NAME + "/{string}")
public String resource2(@PathVariable String string) {
int i = 1 / 0;
return "success ->" + string;
}
public static String handleSentinelException(String string, BlockException blockException) {
return "sentinel exception -> " + string;
}
public static String handleBusinessException(String string, Throwable throwable) {
return "business exception -> " + string;
}
分为两种回调:blockHandler 与 fallback。前者为处理触发了防护规则时抛出的异常,后者为处理业务方法内部发生了异常。
回调方法的声明格式是有要求的(强制):
- blockHandler 回调的编写规范:
public static
SourceMethodReturnType method(
SourceMethodParam1 p1, SourceMethodParam... p..., BlockException exception);
- fallback 回调的编写规范:
public static
SourceMethodReturnType method(
SourceMethodParam1 p1, SourceMethodParam... p..., Throwable throwable);
二者同时存在时,仅执行 blockHandler 的回调
接口资源全局处理
新增测试接口:
@GetMapping(value = "/sentinel/globalHandle")
public String globalHandle() {
return "global handle";
}
定义全局 Sentinel 异常处理器:
...
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
...
@Component
public class GlobalBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException blockException) throws Exception {
JSONObject result = new JSONObject();
if (blockException instanceof FlowException) {
result.put("code", 100);
result.put("message", "已限流");
} else if (blockException instanceof ParamFlowException) {
result.put("code", 101);
result.put("message", "热点参数已限流");
} else if (blockException instanceof DegradeException) {
result.put("code", 100);
result.put("message", "已降级");
} else if (blockException instanceof SystemBlockException) {
result.put("code", 100);
result.put("message", "已触发系统保护规则");
} else if (blockException instanceof AuthorityException) {
result.put("code", 100);
result.put("message", "授权未通过");
}
response.setStatus(500);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
result.writeJSONString(response.getWriter());
}
}
可以发现,我们并没有通过注解的形式:@SentinelResource(value = "globalHandleResource")
进行资源设置,这是因为基于注解的形式不会走全局异常处理器,而是会根据注解的配置执行特定的回调方法,如果没有配置方法名则抛出异常。
那么不配置资源名称,又该如何针对性的配置规则呢?默认的,对于没有显示配置资源名称的接口资源
,会根据请求路径生成资源名称,从而可以在管控台中进行配置。在思考一下,也就是说 Sentinel 的全局异常处理器只能处理接口资源。因为显示声明资源的同时必须也指定特定的处理方法。
防护模式一:流量控制
注意:如果想要应用管控台中配置的规则,就不能在通过编码式在项目中配置规则,否则会导致资源无法展示在管控台中
根据阈值类型,当资源访问请求量超出阈值限制时,根据流控模式限流特定资源上的请求,再根据限流效果进行限流处理。一般配置于服务提供者。
阈值类型
流控的决策条件。
- QPS:Queries per-second 每秒查询率,指服务在规定时间内能够处理多少流量的衡量指标。限制申请资源请求的 QPS,超出后进行限流。
- 线程数:限制每秒申请资源请求的线程数,超出后进行限流。
区别不是很容易理解,举个用户访问 WEB 服务器(Tomcat)接口的栗子:
- QPS 限流:着重于每秒能够处理用户接口请求的数量(注重结果)。
- 线程数限流:着重于每秒能够处理用户接口请求的 HTTP 线程数,也可以说每秒内用户并发请求接口量(注重执行者)。
流控模式
流控哪些访问资源的请求。
1. 直接:直接限流当前资源。
2. 关联:当关联的资源触发当前限流阈值时,并非限流关联资源,而是当前资源。
编写两个接口:
@GetMapping(value = "/sentinel/relate/createOrder")
public String relateCreateOrder() {
return "Relate create order";
}
@GetMapping(value = "/sentinel/relate/getOrder")
public String relateGetOrder() {
return "Relate get order";
}
配置关联流控模式:createOrder 接口 QPS 达到阈值时,限流 getOrder 接口
通过 Apifox 接口工具对 createOrder 接口进行压测:
立即访问获取订单接口(异常处理使用前面编写的 GlobalBlockExceptionHandler):
3. 链路:对调用链路中的请求来源进行限流。对比关联模式,一是限流的是配置的入口资源而非自身。二是限流的入口资源必须与当前资源在一条调用链路上。
@Resource
private UserService userService;
@GetMapping(value = "/sentinel/chain/createOrder")
public String chainCreateOrder() {
return "Chain create order -> " + userService.getUser();
}
@Service
public class UserService {
@SentinelResource(value = "getUser", blockHandler = "handleGetUserBlock")
public String getUser() {
return "张三";
}
public String handleGetUserBlock(BlockException blockException) {
return "查询用户限流";
}
}
增加配置(选配,版本问题,如果链路流控不生效则添加下面的配置):
sentinel:
...
# 禁止收敛URL的入口 context
web-context-unify: false
配置链路模式:
浏览器快速刷新访问 http://localhost:6600/sentinel/chain/createOrder
接口
输出:
Chain create order -> 张三
Chain create order -> 查询用户限流
流控效果
流控的具体方式是什么样的。
1. 快速失败:当请求达到限流阈值时,直接抛出限流异常 FlowException:Blocked by Sentinel(flow limiting)
2. warm up:预热。在指定时间段内,逐步提高限流阈值。默认冷加载因子为 3,即初始开放三分之一的阈值量。该方式是为了防止长期处于低流量访问的冷系统,无法承受瞬间的激增流量导致系统不可用
。
接口代码:
@GetMapping(value = "/sentinel/behavior/warmup")
public String warmup() {
return "warm up";
}
配置预热流控规则,含义:阈值初始为 2 (6/3),并在 5 秒内逐步提升到 6。
进行压测(线程数多的时候,Apifox 的压测功能实在难用,效果也不好,故通过 jmeter 进行压测),启动 100 个线程,10 秒内完成请求,平均每秒十次请求:
查看 sentinel 管控台的实时监控菜单:
注意最左侧的统计结果,初始为 QPS 最多为 2,最高峰为 6。
3. 排队等待:将限流的请求入等待队列,若指定等待时间内仍未被处理即等待超时则限流。排队等待是针对于脉冲流量的一种优化方式,可以将高峰限流的请求阻塞到中低峰时进行执行,提升中低峰时的峰值,而不是直接对限流的请求响应失败。合理利用了系统资源,提高了系统的吞吐量。
脉冲流量是一种规律性的不断触及限流阈值顶峰的流量波。将在处于阈值边界而被限流的请求延迟到中、低峰时间段进行执行,既不会拒绝用户请求,也使得系统可以处理更多的请求。最优的情况为系统 QPS 一直处于顶峰但又没有拒绝用户的请求。
下面进行排队等待的配置:对于超过 QPS 5 的请求进入等待队列,最多等待 5 秒被处理。
防护模式二:熔断降级
官方解释:
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
可以在熔断规则菜单或者簇点链路中对资源进行熔断配置:
慢调用比例策略
当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断(不会在经历一次熔断检测,而是直接进行熔断)。
异常比例策略
当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常请求的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
异常数策略
当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常请求的数目大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
降级处理
当发生熔断降级时,该如何处理,在 @SentinelResource 中指定异常回调方法中编写即可。
熔断与流控的区别
那么熔断对比流控,该如区分二者之间的区别呢?
应用方不同:熔断应用于服务调用方,流控应用于服务提供方。
- 对一段调用三方接口的代码段进行熔断配置,此时我们就是服务调用者。
- 对一个接口进行流控配置,此时我们就是服务提供者。
无论熔断还是流控在配置上的步骤都是一样的,都是基于特定资源配置不同的规则。但,应该配置熔断规则还是流控规则,应该看资源代码是什么类型的。
判定点不同:
- 熔断判定在于是否处于熔断窗口期:是则不会去访问资源,而直接进行降级处理。
- 流控判定在于是否达到流控的阈值:若当下已达到了流控阈值,要么进行限流异常回调处理或直接响应限流异常。若下一秒请求量变少,就可以立即正常访问了,没有熔断的窗口期。
sentinel 整合 openfeign
分别引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
bootstrap.yaml
中配置 sentinel 集成到 openfeign 上:
# sentinel 整合 openfeign
feign:
sentinel:
enabled: true
现在,正常配置 @FeignClient 的 fallback 降级即可,在之前的博客: SpringCloud - RPC (3) 集成 OpenFeign 的降级配置小节详细介绍过。当触发资源的 sentinel 熔断规则进行降级时,就会执行配置的降级逻辑。
@RestController
public class OpenfeignController {
@FeignClient(name = "nacos-sentinel-service", fallbackFactory = FeignService.FeignFallbackFactory.class)
public interface FeignService {
@GetMapping(value = "/sentinel/feign/echo/{string}")
String echo(@PathVariable String string);
@Slf4j
@RestController
class FeignController implements FeignService {
@SentinelResource(value = "echo", blockHandler = "handleEchoException")
@Override
public String echo(String string) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return string;
}
public static String handleEchoException(String string, BlockException blockException) {
log.error(blockException.getMessage());
return "handleEchoException";
}
}
@Slf4j
@Component
class FeignFallbackHandler implements FeignService {
@Override
public String echo(String string) {
log.error("sentinel 熔断降级");
return "fake value";
}
}
@Slf4j
@Component
class FeignFallbackFactory implements FallbackFactory<FeignFallbackHandler> {
@Resource
private FeignFallbackHandler feignFallbackHandler;
@Override
public FeignFallbackHandler create(Throwable cause) {
log.error(cause.getMessage());
return feignFallbackHandler;
}
}
}
@Resource
private FeignService feignService;
@GetMapping(value = "/sentinel/openfeign/echo/{string}")
public String echo(@PathVariable String string) {
return feignService.echo(string);
}
}
注意:如果资源同时配置了注解级别的 bloackHandler 则不会再去执行 @FeignClient 上配置的 fallback 降级处理。即 @SentinelResource 优先级高于 @FeignClient。
防护模式三:热点参数
对热点数据(访问量极高的数据)的访问进行限制。比如营销活动中的砍价、秒杀商品,根据活动商品 id 查询该商品详情,会使得接口的 QPS 极高。那么就可以对访问量极高的商品(商品 id)进行流控,而对其他的商品访问则进行放宽。注意:热点参数仅支持 QPS 流控模式
分为两部分:
-
基本规则:监控资源方法的参数,0 表示第一位参数。在统计时间窗口内,监控资源方法参数的 QPS,当超过流控阈值后对资源进行限流。
-
参数额外项规则:当传入的指定类型的参数值为特定配置值时,流控阈值不在取基本规则中的阈值,而是单独配置的阈值。当超过单独配置的流控阈值后对资源进行限流。
简单的说:如果参数值不为例外参数值,则应用基本规则。否则应用参数额外项规则。注意:如果资源方法的参数值为空,则不会被任何热点参数规则匹配进行流控。
那么与普通的流控规则有什么不同呢?热点参数规则关注的是资源方法的不为空的参数值,而流控规则关注的是资源。
测试:
@Slf4j
@RestController
public class HotpointController {
// ============================================= 热点规则:热点参数 =============================================
@GetMapping(value = "/sentinel/hotpoint")
@SentinelResource(value = "hp", blockHandler = "handleHotpointParamException")
public Integer hotpointParam(@RequestParam(required = false) Integer param) {
return param;
}
public static Integer handleHotpointParamException(Integer integer, BlockException blockException) {
log.error(blockException.getMessage());
return 999;
}
}
3 秒内访问 > 1次:http://localhost:6600/sentinel/hotpoint?param=1
会响应 999
3 秒内访问 > 3次:http://localhost:6600/sentinel/hotpoint?param=2
会响应 999
注意,热点参数规则只会在通过 @SentinelResource 标识的资源上生效
防护模式四:系统保护
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。
入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的阈值类型:
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
- CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
(配置的持久化
与集群流控
,配置太繁琐,资源也很少,以后有时间在记录吧 😃