sentinel使用及Slot构建源码分析
前言
当服务流量突然激增,服务间调用依赖方不可用等等的情况出现时,如何保障自身服务的可用性与稳定性(防止服务被压垮、防止雪崩),sentinel是一种以流量为切入点的解决方案,来保障服务的高可用。
介绍
Sentinel是什么?
Sentinel是阿里 spring-cloud-alibaba的开源项目之一,其它几个包括有Nacos Config、Nacos Discovery、RocketMq。
Sentinel是一个流控组件。以流量切入,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
提供的特性有哪些?
1、应用场景多:秒杀(突发流量控制在可承受范围内)、实时熔断;
2、监控平台:提供了sentinel dashboard,实时监控功能;
3、其它框架整合能力:开箱即用的整合能力,例如Spring Cloud、Dubbo、gRPC,只需简单配置;
4、完善的SPI扩展:可以通过实现扩展点,快速定制逻辑。
具体如何使用?dashboard长什么样?
环境搭建
新建项目
构建spring boot项目,引用spring-cloud-starter-alibaba-sentinel的包
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.1</version>
</dependency>
增加一个测试接口
@RestController
@RequestMapping("/hello")
public class HelloController {
/**
* http://127.0.0.1:8103/hello/world
* @return
*/
@GetMapping("/world")
public Result hello () {
return Result.success("hello");
}
}
新建配置文件 application.yaml
server:
port: 8103
spring:
application:
name: sentinel-demo
main:
allow-circular-references: true
cloud:
# 配置连接sentinel dashboard
sentinel:
transport:
# 配置启动的 sentinel dashboard地址
dashboard: 127.0.0.1:8100
# 端口配置,会在应用对应的机器上启动一个 Http Server,
# 该 Server 会与 Sentinel 控制台做交互。
# 比如 Sentinel 控制台添加了1个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中
port: 8730
配置连接sentinel dashboard,用于监控
至此,完成启动项目。
启动sentinel dashboard
下载jar包(目前1.8.6最新版):https://github.com/alibaba/Sentinel/releases
也可下载工程,自行构建:https://github.com/alibaba/Sentinel/tree/master/sentinel-dashboard
命令行启动:
java -Dserver.port=8100 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar
默认登录名密码:sentinel
-Dserver.port=8100:dashboard的默认端口
-Dcsp.sentinel.api.port:默认8719,sentienl dashboard transport 模块的端口
dashboard界面:
如果没有发现启动的应用,可以访问下应用的接口,Sentinel 客户端在 首次访问资源时 会初始化并给控制台发送心跳。
其它说明
dashboard相关日志:
控制台访问操作日志:{user.home}/logs/csp/sentinel-record.log.xxx
接入端 transport server 日志:${user.home}/logs/csp/command-center.log.xxx
api查询规则:
引用transport后,可以通过api的方式进行获取规则
http://localhost:PORT/getRules?type=XXXX
PORT表示引用transport模块的控制台或客户端;
type=flow 以 JSON 格式返回现有的限流规则,degrade 返回现有生效的降级规则列表,system 则返回系统保护规则。
演示
配置流控规则
频繁刷新接口
限流时默认返回:Blocked by Sentinel (flow limiting)
此处异常已经被捕获处理了,BlockException
jmeter压测
jmeter压测时,配置Constant Throughput Timer,限制每分钟QPS。
原理
架构图
Sentinel的核心骨架是ProcessorSlotChain,将所有的插槽slot按顺序串成链chain(责任链模式),从而将不同的功能组合在一起。默认的Slot如下:
1、统计数据部分
1.1、NodeSelectorSlot:负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
1.2、ClusterBuilderSlot:构建用于存储资源的统计信息以及调用者信息,统计信息例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;简单说就是用于构建ClusterNode;
1.3、StatisticSlot:则用于记录、统计不同纬度的实时指标监控信息;
2、规则校验部分
2.1、FlowSlot:则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
2.2、AuthoritySlot:则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
2.3、DegradeSlot:则通过统计信息以及预设的规则,来做熔断降级;
2.4、SystemSlot:则通过系统的状态,例如 cpu负载 等,来控制总的入口流量;
多种Node关系
核心概念:
Resource:Sentinel 通过资源来保护具体的业务代码。我们只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了,剩下的交给 Sentinel 就可以了。在 Sentinel 中具体表示资源的类是:ResourceWrapper。
Context:代表调用链路上下文,插槽间数据的传递,贯穿整个调用链路中的多个资源。(根据名称创建,默认名称sentinel_default_context,保存在ThreadLocal中)。
Entry:每次调用SphU#entry()的方法时创建,返回Entry对象,可以理解为获取限流凭证,触发调用SlotChain,其与Context的对应关系为 Context:Entry=1:n。
Node相关概念:
Root:一个应用一个Root节点;
Node:用于完成数据统计的接口;
StatisticNode:统计节点,用于完成数据统计;
EntranceNode:入口节点,一个Context会有一个入口节点,统计当前Context的流量数据。(根据Context名称创建,保存在内存Map集合中);
DefaultNode:默认节点,用于统计一个资源在当前Context中的流量数据;
ClusterNode:集群节点,用于统计一个资源在所有Context中的流量数据;
总结Node的关系:
从api看下节点Tree关系:
http://localhost:8719/tree?type=root
8719为dashboard transport默认端口
从源码看类继承关系:
SPI扩展
Sentinel将ProcessorSlot作为SPI接口进行扩展,使得SlotChain具有扩展能力,同时可以指定顺序。
其它还有初始化逻辑扩展、transport模块(如心跳)扩展等
Slot构建源码分析
配置sentinel资源访问时的拦截器:
// com.alibaba.csp.sentinel.adapter.spring.webmvc.config.InterceptorConfig
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//Config
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
config.setBlockExceptionHandler(new BlockExceptionHandler() {
//Do something ......
});
//Custom configuration if necessary
config.setHttpMethodSpecify(false);
config.setWebContextUnify(true);
config.setOriginParser(new RequestOriginParser() {
@Override
public String parseOrigin(HttpServletRequest request) {
return request.getHeader("S-user");
}
});
//Add sentinel interceptor,拦截所有路径
registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
}
}
启动第一次访问进行加载,进入对应拦截器 InterceptorConfig:
// com.alibaba.csp.sentinel.adapter.spring.webmvc.AbstractSentinelInterceptor
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
try {
String resourceName = getResourceName(request);
// 省略部分校验
// Parse the request origin using registered origin parser.
String origin = parseOrigin(request);
String contextName = getContextName(request);
// 创建Context
ContextUtil.enter(contextName, origin);
// 生成Entry,资源初次访问的话,会生成链ProcessorSlotChain 及 对应的槽点ProcessorSlot
Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
return true;
} catch (BlockException e) {
try {
handleBlockException(request, response, e);
} finally {
ContextUtil.exit();
}
return false;
}
}
在调用SphU.entry时会判断当前请求的资源是否是第一次访问,如果是第一次:
// com.alibaba.csp.sentinel.CtSph
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// ResourceWrapper重写了hashCode方法,根据资源名获取ProcessorSlotChain
// 所以其是按资源创建ProcessorSlotChain及ProcessorSlot
// DCL,即double-checked locking
ProcessorSlotChain chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
if (chain == null) {
synchronized(LOCK) {
chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
if (chain == null) {
if (chainMap.size() >= 6000) {
return null;
}
// chainMap中没有,调用创建
chain = SlotChainProvider.newSlotChain();
// 添加map中
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap(chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
同一个资源会全局共享一个SlotChain
继续调用SlotChainProvider� 进行创建 ProcessorSlotChain:
// com.alibaba.csp.sentinel.slotchain.SlotChainProvider
public static ProcessorSlotChain newSlotChain() {
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
// 通过spi的方式进行创建slotChainBuilder,spi默认配置类为:com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder
slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();
if (slotChainBuilder == null) {
// Should not go through here.
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}",
slotChainBuilder.getClass().getCanonicalName());
}
// 进入com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder的build方法
return slotChainBuilder.build();
}
最终进入DefaultSlotChainBuilder,完成ProcessorSlot的创建,并addLast到ProcessorSlotChain,组成链:
@Override
public ProcessorSlotChain build() {
// 默认的DefaultProcessorSlotChain
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// 通过Spi完成ProcessorSlot的创建
List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
}
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
至此,整个slotChain完成初始化。
总结
1、SlotChain的创建是和资源绑定的,相同资源共用SlotChain
2、sentinel还可以使用硬编码的方式自定义资源进行限流
看到最后,给大家推荐个小程序吧
(变有钱记账本,让我变有钱)