前言

当服务流量突然激增,服务间调用依赖方不可用等等的情况出现时,如何保障自身服务的可用性与稳定性(防止服务被压垮、防止雪崩),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界面:
image-1671101502286

如果没有发现启动的应用,可以访问下应用的接口,Sentinel 客户端在 首次访问资源时 会初始化并给控制台发送心跳。

其它说明

dashboard相关日志:
控制台访问操作日志:user.home/logs/csp/sentineldashboard.log接入端接收规则日志:{user.home}/logs/csp/sentinel-dashboard.log 接入端接收规则日志:{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 则返回系统保护规则。


演示

配置流控规则

image-1671101587576

频繁刷新接口

image-1671101624856

限流时默认返回:Blocked by Sentinel (flow limiting)
此处异常已经被捕获处理了,BlockException

jmeter压测

jmeter压测时,配置Constant Throughput Timer,限制每分钟QPS。


原理

架构图

image-1671101710628

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关系

image-1671101736997

核心概念:
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的关系:
image-1671101882011

从api看下节点Tree关系:
http://localhost:8719/tree?type=root
8719为dashboard transport默认端口

从源码看类继承关系:
image-1671101902927

SPI扩展

image-1671101919094

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还可以使用硬编码的方式自定义资源进行限流


看到最后,给大家推荐个小程序吧
变有钱记账本,让我变有钱)
image-1671115699919