Spring Cloud Alibaba Sentinel 简介与入门
简介
Sentinel直译过来是哨兵。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
高并发性能指标
在学习Sentinel之前最好是了解一下各个性能指标的意思,可移步我们的 《高并发性能指标》一文
Sentinel的主要功能
流量控制
任何单个应用的处理能力都是有限的,比如某个应用最大只能在单位时间内处理1000QPS,那么再大一点的流量,可能导致应用整体不可用,导致系统瘫痪等等一系列的问题。
Sentinel 从以下几个角度进行流量控制:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
熔断降级
Spring Cloud生态中,Hystrix(netflix 开源的熔断降级处理依赖) 和 Spring Cloud Circuit breaker (Spring 官方提供的熔断降级处理依赖)都提供了熔断降级功能。
什么是熔断降级:由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。例如下图的Service D
Service D出现不稳定,会导致整个调用路径请求堆积,直接影响到Service G 和 Service A的返回。
Sentinel 基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
Sentinel 客户端和控制台
- Sentinel 客户端:不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。也就是说不只是Spring Cloud中,只要是java代码我们就能使用Sentinel。
- 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。
入门示例
引入Sentinel依赖
Sentinel 是可以单独使用的,所以我们的入门程序就只依赖 Sentinel 即可
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
测试代码
package com.yyoo.sentinel.demo;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import java.util.ArrayList;
import java.util.List;
public class Test1 {
public static void main(String[] args) {
// 配置规则.
initFlowRules();
while (true) {
// try-with-resources 特性(可以不用在finally中关闭)
try (Entry entry = SphU.entry("HelloWorld")) {
// 被保护的逻辑
System.out.println("hello world");
} catch (BlockException ex) {
// 处理被流控的逻辑
System.out.println("blocked!");
}
}
}
/**
* 定义规则(rule)列表
*/
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
// 设置规则匹配的资源名称
rule.setResource("HelloWorld");
// 规则作用类型(此为QPS类型,还有一种为FLOW_GRADE_THREAD,线程数类型)
// 默认为QPS类型
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 按Grade设置,此处为限制 QPS 为 20.
rule.setCount(20);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
Demo 运行之后,我们可以在日志 ~/logs/csp/${appName}-metrics.log.xxx 里看到下面的输出:(~ 即为用户目录)
|--timestamp-|------date time----|--resource-|p |block|s |e|rt
1675402839000|2023-02-03 13:40:39|HelloWorld|20|34171|20|0|0|0|0|0
1675402840000|2023-02-03 13:40:40|HelloWorld|20|99964|20|0|0|0|0|0
1675402841000|2023-02-03 13:40:41|HelloWorld|20|117545|20|0|0|0|0|0
1675402842000|2023-02-03 13:40:42|HelloWorld|20|137136|20|0|0|0|0|0
1675402843000|2023-02-03 13:40:43|HelloWorld|20|164920|20|0|0|0|0|0
1675402844000|2023-02-03 13:40:44|HelloWorld|20|152597|20|0|0|0|0|0
1675402845000|2023-02-03 13:40:45|HelloWorld|20|151261|20|0|0|0|0|0
1675402846000|2023-02-03 13:40:46|HelloWorld|20|155132|20|0|0|0|0|0
1675402847000|2023-02-03 13:40:47|HelloWorld|20|152466|20|0|0|0|0|0
1675402848000|2023-02-03 13:40:48|HelloWorld|20|143706|20|0|0|0|0|0
1675402849000|2023-02-03 13:40:49|HelloWorld|20|167305|20|0|0|0|0|0
1675402850000|2023-02-03 13:40:50|HelloWorld|20|163450|20|0|0|0|0|0
1675402851000|2023-02-03 13:40:51|HelloWorld|20|161299|20|0|0|0|0|0
1675402852000|2023-02-03 13:40:52|HelloWorld|20|160861|20|0|0|0|0|0
1675402853000|2023-02-03 13:40:53|HelloWorld|20|160179|20|0|0|0|0|0
1675402854000|2023-02-03 13:40:54|HelloWorld|20|153261|20|0|0|0|0|0
- p 代表通过的请求
- block 代表被阻止的请求
- s 代表成功执行完成的请求个数
- e 代表用户自定义的异常
- rt 代表平均响应时长
资源与规则
用 Sentinel 来进行资源保护,主要分为几个步骤:
- 定义资源
- 定义规则
- 检验规则是否生效
定义资源的方式
方式一:主流框架的默认适配
为了减少开发的复杂程度,我们对大部分的主流框架,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor 等都做了适配。您只需要引入对应的依赖即可方便地整合 Sentinel。具体示例我们会在后续文章中会提到。
方式二:抛出异常的方式定义资源
我们的入门示例即是此方式
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
// 被保护的业务逻辑
// do something here...
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 在此处进行相应的处理操作
}
方式三:返回布尔值方式定义资源
SphO 提供 if-else 风格的 API。用这种方式,当资源发生了限流之后会返回 false,这个时候可以根据返回值,进行限流之后的逻辑处理。示例代码如下:
// 资源名可使用任意有业务语义的字符串
if (SphO.entry("自定义资源名")) {
// 务必保证finally会被执行
try {
/**
* 被保护的业务逻辑
*/
} finally {
SphO.exit();
}
} else {
// 资源访问阻止,被限流或被降级
// 进行相应的处理操作
}
方式四:注解方式定义资源
Sentinel 支持通过 @SentinelResource 注解定义资源并配置 blockHandler 和 fallback 函数来进行限流之后的处理。示例:
// 原本的业务方法.
@SentinelResource(blockHandler = "blockHandlerForGetUser")
public User getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
// blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用
public User blockHandlerForGetUser(String id, BlockException ex) {
return new User("admin");
}
注意 blockHandler 函数会在原方法被限流/降级/系统保护的时候调用,而 fallback 函数会针对所有类型的异常。
方式五:异步调用支持
Sentinel 支持异步调用链路的统计。在异步调用中,需要通过 SphU.asyncEntry(xxx) 方法定义资源,并通常需要在异步的回调函数中调用 exit 方法。以下是一个简单的示例:
try {
AsyncEntry entry = SphU.asyncEntry(resourceName);
// 异步调用.
doAsync(userId, result -> {
try {
// 在此处处理异步调用的结果.
} finally {
// 在回调结束后 exit.
entry.exit();
}
});
} catch (BlockException ex) {
// Request blocked.
// Handle the exception (e.g. retry or fallback).
}
SphU.asyncEntry(xxx) 不会影响当前(调用线程)的 Context,因此以下两个 entry 在调用链上是平级关系(处于同一层),而不是嵌套关系:
// 调用链类似于:
// -parent
// ---asyncResource
// ---syncResource
asyncEntry = SphU.asyncEntry(asyncResource);
entry = SphU.entry(normalResource);
若在异步回调中需要嵌套其它的资源调用(无论是 entry 还是 asyncEntry),只需要借助 Sentinel 提供的上下文切换功能,在对应的地方通过 ContextUtil.runOnContext(context, f) 进行 Context 变换,将对应资源调用处的 Context 切换为生成的异步 Context,即可维持正确的调用链路关系。示例如下:
public void handleResult(String result) {
Entry entry = null;
try {
entry = SphU.entry("handleResultForAsync");
// Handle your result here.
} catch (BlockException ex) {
// Blocked for the result handler.
} finally {
if (entry != null) {
entry.exit();
}
}
}
public void someAsync() {
try {
AsyncEntry entry = SphU.asyncEntry(resourceName);
// Asynchronous invocation.
doAsync(userId, result -> {
// 在异步回调中进行上下文变换,通过 AsyncEntry 的 getAsyncContext 方法获取异步 Context
ContextUtil.runOnContext(entry.getAsyncContext(), () -> {
try {
// 此处嵌套正常的资源调用.
handleResult(result);
} finally {
entry.exit();
}
});
});
} catch (BlockException ex) {
// Request blocked.
// Handle the exception (e.g. retry or fallback).
}
}
此时的调用链就类似于:
-parent
---asyncInvocation
-----handleResultForAsync
规则的种类
- 流量控制规则 (FlowRule)
- 熔断降级规则 (DegradeRule)
- 系统保护规则(SystemRule)
- 来源访问控制规则 (AuthorityRule)
- 热点参数规则(ParamFlowRule)