Sentinel技术介绍及源码浅析

余梦

Sentinel介绍

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

  • Sentinel历史
  • 2012 年,Sentinel 诞生
  • 2013-2017 年,作为阿里基础技术模块, Sentinel 承接了阿里巴巴近 几年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等
  • 2018 年,Sentinel 开源

Sentinel基本概念

a1

如上图所示,Sentinel中一个资源访问条目被抽象成一个Entry,这个Entry包含几个重要的信息,一个是当前节点(Node)、源节点,一个是资源信息(比如资源名称等)。规则则是对资源访问控制的一种策略。 Node是Sentinel中对应一个资源实时访问指标的抽象,其代表请求进入后的一种状态,每个请求进来,对于关联的资源,采取前面设置的访问策略进行指标统计。


Sentinel基本功能

a2


如何使用Sentinel

第一步当然是引入sentinel依赖了,对于不同框架的集成 采用不同的适配模块,详情参考官方文档。

定义资源

抛出异常的方式定义资源 用这种方式,当资源发生了限流之后会抛出 BlockException。这个时候可以捕捉异常,进行限流之后的逻辑处理。示例代码如下:

Entry entry = null;  
// 务必保证finally会被执行
try {  
  // 资源名可使用任意有业务语义的字符串
  entry = SphU.entry("自定义资源名");
  /**
   * 被保护的业务逻辑
   */
} catch (BlockException e1) {
  // 资源访问阻止,被限流或被降级
  // 进行相应的处理操作
} finally {
  if (entry != null) {
    entry.exit();
  }
}

返回布尔值方式定义资源 用这种方式,当资源发生了限流之后会返回 false,这个时候可以根据返回值,进行限流之后的逻辑处理。示例代码如下:

  // 资源名可使用任意有业务语义的字符串
  if (SphO.entry("自定义资源名")) {
    // 务必保证finally会被执行
    try {
      /**
      * 被保护的业务逻辑
      */
    } finally {
      SphO.exit();
    }
  } else {
    // 资源访问阻止,被限流或被降级
    // 进行相应的处理操作
  }

注解方式定义资源

<dependency>  
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>x.y.z</version>
</dependency>

@SentinelResource 注解 @SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:

value: 资源名称,必需项(不能为空)
entryType: 入口类型,可选项(默认为 EntryType.OUT)
blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。若未配置,则将 BlockException 直接抛出。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallback: fallback 函数名称,可选项,仅针对降级功能生效(DegradeException)。fallback 函数的访问范围需要是 public,参数类型和返回类型都需要与原方法相匹配,并且需要和原方法在同一个类中。
若 blockHandler 和 fallback 都进行了配置,则遇到降级的时候首先选择 fallback 函数进行处理。

注意 blockHandler 是处理被 block 的情况(所有类型的 BlockException),而 fallback 仅处理被降级的情况(DegradeException)。其它异常会原样抛出,Sentinel 不会进行处理。

public class TestService {

    // 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,并且必须为 static 函数.
    @SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
    public void test() {
        System.out.println("Test");
    }

    // 原函数
    @SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
    public String hello(long s) {
        return String.format("Hello at %d", s);
    }

    // Fallback 函数,函数签名与原函数一致.
    public String helloFallback(long s) {
        return String.format("Halooooo %d", s);
    }

    // Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
    public String exceptionHandler(long s, BlockException ex) {
        // Do some log here.
        ex.printStackTrace();
        return "Oops, error occurred at " + s;
    }
}

异步调用支持 Sentinel 从 0.2.0 版本开始支持异步调用资源的定义。在异步调用中,需要通过 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).
}

关于异步调用的详细用法,请参考官方文档。


主流框架的适配:

a4 因篇幅有限,其它各种框架的适配,请参考官方文档。


定义规则

Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。

规则的定义 Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则 以及 授权规则。因篇幅问题,对应功能的规则配置请参考官方文档,如下只是一个示例:

private static void initFlowQpsRule() {  
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule1 = new FlowRule();
        rule1.setResource(KEY);
        // set limit qps to 20
        rule1.setCount(20);
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
    }

Sentinel工作原理

a3

Sentinel的工作原理,简单理解可以看上面的脑图

对于需要保护的资源,系统先定义其rule,创建资源、然后创建默认的SoltChain。这个SoltChain,我们可以自行装配,也可以直接默认使用Sentinel提供的DefaultProcessorSlotChain。Sentinel提供了基于SPI的拓展口子,让我们可以自己拓展每一个ProcessorSlot。Sentinel中ProcessorSlot的设计思路和Netty中Handler处理链的设计非常像。一个请求进入某个资源,其接下来的调用链路都会由前面装配好的路劲进行,每个插槽(Solt)的作用参考上图。这其中,无论多少个request进入资源,其全部共享同一个上下文Context,Sentinel内部是按资源名划分的。每个Entry都关联一个Context和一个Chain。这块的设计思路,如果看过Netty的源码,你会感觉非常的熟悉。后面我会谈到Sentinel的核心组件和源码。


Sentinel和Hystrix 对比

Hystrix 的关注点在于以 隔离 和 熔断 为主的容错机制,超时或被熔断的调用将会快速失败,并可以提供 fallback 机制。
Sentinel 的侧重点在于:多样化的流量控制、熔断降级、系统负载保护、实时监控和控制台
Hystrix中每个 Command 创建时都要指定 commandKey 和 groupKey(用于区分资源)以及对应的隔离策略(线程池隔离 or 信号量隔离)
Sentinel 的资源定义与规则配置的耦合度更低
Sentinel 中资源定义和规则配置是分离的。用户先通过 Sentinel API 给对应的业务逻辑定义资源(埋点),然后可以在需要的时候配置规则。
Hystrix在采用线程池隔离时,有个最大的缺点就是,Hystrix 的线程池隔离针对不同的资源分别创建不同的线程池,对于一些线程或者资源敏感的应用,可能会创建太多的线程,造成线程上下文切换的 overhead 比较大。比如使用Tomcat的应用,Tomcat作为web服务器,本身就消耗不少线程资源,如果再使用Hystrix,在采用线程池隔离时上下文切换会有非常大的损耗。

总结如下 a5

Sentinel源码浅析

Sentinel核心组件UML图

a6 这个图有点大,超出限制,我截图后压缩后才能正常上传,有同学如果感兴趣可以私下找我要。 Sentinel中比较重要的几个比较重要的抽象
Entry,Context,Node,Sph,ProcessorSlot,Rule,RuleManager,SentinelProperty,PropertyListener,Metric,MetricBucket,MetricWriter,MetricsReader,MetricTimerListener,WindowWrap,LeapArray、transport。具体抽象的其实就是(对应上面)资源、节点、获取资源条目的抽象、资源上下文、处理插槽、规则、规则管理器、配置、配置监听器、度量、度量槽、度量写、度量读、度量信息监听器(定时写入)、时间片、滑动窗口、发送统计数据到指定的sentinel控制台。对于每一个请求,进入某个资源,都会通过资源名称获取一个资源以及其对应的上下文,所有的请求都会经过一个处理链(SoltChain),其中StatisticSlot负责统计度量指标,具体的操作是调用StatisticNode这个统计节点进行操作的,统计节点StatisticNode维持两个滑动窗口,一个窗口周期是1秒,一个是60秒。滑动窗口实现没啥特别之处,每个时间片内部的桶信息计数是通过大名鼎鼎的Doug Lea写的实现的并发累计器Striped64实现的LongAdder。具体思路和算法是将AtomicInteger的内部核心数据value分离成一个数组,每个线程访问时,通过哈希等算法映射到其中一个数字进行计数,而最终的计数结果,则为这个数组的求和累加,其中,热点数据value被分离成多个单元cell,每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成,这样,热点就进行了有效的分离,提高了并行度,LongAdder正是使用了这种思想,类似ConcurrentHashMap,热点分离。因为传统的CAS实现,在并发量较大的时候,修改失败的概率就很高,在大量修改失败时,这些原子操作就会进行多次循环尝试,因此性能就会受到影响。LongAdder的热点分离分段累加操作,在并发较高时性能相对会好很多。
下面发下请求进入资源后的整个时序图

sentinel时序图

最后放下个人学习sentinel的一个思维导图,起个抛砖引玉的作用。 sentinel学习思维导图


总的来说sentinel的模块和代码实现还是非常清晰的,大家有兴趣可以再深入研究下,以上均为个人理解观点,因为个人能力和研究深度有限,观点难免有失偏颇,欢迎各位同学私下斧正交流。