Sentinel限流框架

前言

2020年,由于疫情大爆发。A所在的图书馆接到了上面的任务:1天内不允许超过50人同进在馆内看书,外省人禁入内。如果你是此图书管理员,你会怎么做?

  1. 数据收集(访客的身份证,来自哪)
  2. 数据统计(对一天内的访客总数统计,儿童馆统计)
  3. 规则限制(根据当前统计数据,比对规则)

简单的例子

String resource = "进入图书馆";
Rule rule = new Rule();
rule.setResource(resource);
rule.setLimitApp("外省"); //限制外省人
rule.setCount(50);//一天只能50人
//载入规则
AuthorityRuleManager.loadRules(Collections.singletonList(rule);


String name = "访客身份证";
String origin = "来自省份";
ContextUtil.enter(name, origin);
Entry entry = null;
try {
      //请求进入图书馆
      entry = SphU.entry(resource);
      //我是XX来自XX省,已拿到图书馆的入场证明     
} catch (BlockException ex) {
      //我是XX来自XX省,被图书馆拒绝入内   
} finally {
      if (entry != null) {
           //我出图书馆了
           entry.exit();
      }
      //我回家了
      ContextUtil.exit();
}

Sentinel的调用链

ContextUtil.enter(name, origin);

在这里插入图片描述
创建一Context对象,它表示一次访问有档案(上下文)。保存在ThreadLocal中的,每次执行的时候会优先到ThreadLocal中获取。

  • name: 名字
  • origin:调用源,哪个App请求过来的
  • curEntry:调用链的当前entry
  • entranceNode:存入当前Entry的一些数据
  SphU.entry(1);
  SphU.entry(2);
  SphU.entry(3);

在这里插入图片描述
entry表示是否成功申请资源的一个凭证。

  • parent和child 表示上一个资源和下一个资源
  • createTime:当前Entry的创建时间,主要用来后期计算rt
  • resourceWrapper:当前Entry所关联的资源
  • node:当前Entry所关联的node,该node主要是记录了当前context下该资源的统计信息
 entry.exit();

在这里插入图片描述

Sentinel中的Slot链

//SphU.entry最终会调用这方法
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
        throws BlockException {
        Context context = ContextUtil.getContext();
       
        //找对资源对应配置的Slot
        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

        Entry e = new CtEntry(resourceWrapper, chain, context);
         
        //根据资源配置的slot链一个一个调用 
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
       
        return e;
    }

在这里插入图片描述

  • NodeSelectorSlot 资源和调用者双维度的存储对象。
  • ClusterBuilderSlot 资源维度的存储对象,用于存储资源的统计信息以及调用者信息。
  • StatisticsSlot 则用于记录,统计不同维度的 runtime 信息;
  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
  • AuthoritySlot 则根据黑白名单,来做黑白名单控制;
  • FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;
  • DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级;

原来Sentinel通过实现不同的Slot及调用顺序实现了数据收集,统计,及规则的较检。注意,所有的Slot是资源维度的,不同资源Slot对象是不同的

Slot中的存储Node

node是保存资源的实时统计数据的,例如:passQps,blockQps,rt等实时数据.

NodeSelectorSlot
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable {
    // 根据「上下文」的名称获取DefaultNode
    // 多线程环境下,每个线程都会创建一个context,
    // 只要资源名相同,则context的名称也相同,那么获取到的节点就相同
    DefaultNode node = map.get(context.getName());
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            if (node == null) {
                // 如果当前「上下文」中没有该节点,则创建一个DefaultNode节点
                node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
                // 省略部分代码
            }
            // 将当前node作为「上下文」的最后一个节点的子节点添加进去
            // 如果context的curEntry.parent.curNode为null,则添加到entranceNode中去
            // 否则添加到context的curEntry.parent.curNode中去
            ((DefaultNode)context.getLastNode()).addChild(node);
        }
    }
    // 将该节点设置为「上下文」中的当前节点
    // 实际是将当前节点赋值给context中curEntry的curNode
    // 在Context的getLastNode中会用到在此处设置的curNode
    context.setCurNode(node);
    fireEntry(context, resourceWrapper, node, count, args);
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
DefaultNode是基于资源和context双维度的一个存储对象。

ClusterBuilderSlot
//注意这边是static
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
    if (clusterNode == null) {
        synchronized (lock) {
            if (clusterNode == null) {
                // Create the cluster node.
                clusterNode = Env.nodeBuilder.buildClusterNode();
                // 将clusterNode保存到全局的map中去
                HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(16);
                newMap.putAll(clusterNodeMap);
                newMap.put(node.getId(), clusterNode);

                clusterNodeMap = newMap;
            }
        }
    }
    // 将clusterNode塞到DefaultNode中去
    node.setClusterNode(clusterNode);

    // 省略部分代码

    fireEntry(context, resourceWrapper, node, count, args);
}

ClusterNode是基于资源为维度的一个存储对象。

资源角度:Node关系图

在这里插入图片描述
其中entranceNode是每个上下文的入口,该节点是直接挂在root下的,是全局唯一的,每一个context都会对应一个entranceNode。另外defaultNode是记录当前调用的实时数据的,每个defaultNode都关联着一个资源和clusterNode,有着相同资源的defaultNode,他们关联着同一个clusterNode。

Slot中的数据收集及统计

StatistcSlot
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
        // 触发下一个Slot的entry方法
        fireEntry(context, resourceWrapper, node, count, args);
        // 如果能通过SlotChain中后面的Slot的entry方法,说明没有被限流或降级
        // 统计信息
        node.increaseThreadNum();
        node.addPassRequest();
        // 省略部分代码
}

@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    DefaultNode node = (DefaultNode)context.getCurNode();
    if (context.getCurEntry().getError() == null) {
        long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
        if (rt > Constants.TIME_DROP_VALVE) {
            rt = Constants.TIME_DROP_VALVE;
        }
        node.rt(rt);
        node.decreaseThreadNum();
        // 省略部分代码
    } 
    fireExit(context, resourceWrapper, count);
}

收集请求资源线程的的数量,请求数量及响应时间。

利用时间窗口Metric做统计
//node.addPassRequest()
//最终会触发clusterNode,defaultNode的addPassRequest
public void addPassRequest(int count) {
        super.addPassRequest(count);
        this.clusterNode.addPassRequest(count);
}

//分别以秒维度和分钟维度计数
public void addPassRequest(int count) {
        rollingCounterInSecond.addPass(count);
        rollingCounterInMinute.addPass(count);
}

//获取当前的时间窗口,然后把数加上去
public void addPass(int count) {
    WindowWrap<MetricBucket> wrap = data.currentWindow();
    wrap.value().addPass(count);
}

//以一秒为统计间隔,分2个时间窗口
//rollingCounterInSecond = new ArrayMetric(2,1000);
//以一分钟为统计间隔,分60个时间窗口
//rollingCounterInMinute = new ArrayMetric(60, 60*1000);
public abstract class LeapArray<T> {
    /**
     * LeapArray对象
     * @param sampleCount 时间窗口的个数,单位:个
     * @param intervalInMs 统计的间隔,单位:毫秒
     */
    public LeapArray(int sampleCount, int intervalInMs) {
        //第个时间窗口长度 
        this.windowLengthInMs = intervalInMs / sampleCount;
        //统计的间隔
        this.intervalInMs = intervalInMs;
        //时间窗口的个数
        this.sampleCount = sampleCount;
        //时间窗口对象数组
        this.array = new AtomicReferenceArray<>(sampleCount);
    }

}

public class WindowWrap<T> {

    //时间窗口长度,多少秒
    private final long windowLengthInMs;

    //时间窗口的起始长度
    private long windowStart;

    //用来计数用的
    private MetricBucket value;
}

拿rollingCounterInSecond=new ArrayMetric(2,1000)举列

  1. 把时间以500ms切割成块,设当前时间x%500=200得在这里插入图片描述
  2. 将时间往后推移150,300ms,得到的时间窗口都在timeId1。因此这个时间段发生的请求都将统计到timeId1时间窗口对象的value值中。
    在这里插入图片描述
  3. 时间继续向前推200,得到的时间窗口在timeId2,因此这个时间段发生的请求都将统计到timeId2时间窗口对象的value值中.
    在这里插入图片描述
  4. 时间继续向前推600,得到的时间窗口在timeId3,因此这个时间段发生的请求都将统计到timeId3时间窗口对象的value值中。因为只允许2个窗口,timeId1失效

在这里插入图片描述

Slot中的规则效验

FlowSlot
FlowRule rule1 = new FlowRule();
rule1.setResource(KEY);
//设置允许通过的最大请求数20; 限流阈值
rule1.setCount(20);
//设置限流阈值类型, QPS 或线程数模式, 默认是QPS. 
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");

//通过FlowSlot最终会调用DefaultController
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        //最终会调用node.passQps()
        int curCount = avgUsedTokens(node);
        if (curCount + acquireCount > count) {
            //省略代码
            return false;
        }
        return true;
    }

//各时间窗口的通过数,除以统计时间
 public double passQps() {
        return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
    }

FlowSlot 很好的利用的前面收集到的数据。SystemSlot,AuthoritySlot,DegradeSlot过程相似。

Sentinel中的项目结构介绍

在这里插入图片描述

  1. sentinel-core 核心模块,限流、降级、系统保护等都在这里实现(我们上面讲的)
  2. sentinel-transport 传输模块,提供了基本的监控服务端和客户端的API接口,以及一些基于不同库的实现(与外界交互)
  3. sentinel-dashboard 控制台模块,可以对连接上的sentinel客户端实现可视化的管理(客户端装上传输模块把数据传到这)
  4. sentinel-adapter 适配器模块,主要实现了对一些常见框架的适配(dubbo,spring-mvc等等)
  5. sentinel-extension 扩展模块,主要对DataSource进行了部分扩展实现(数据持久化)
  6. sentinel-benchmark 基准测试模块,对核心代码的精确性提供基准测试
  7. sentinel-demo 样例模块,可参考怎么使用sentinel进行限流、降级等
  8. sentinel-cluster 集群模式,提供统一tokenService实现(客户端装上传输模块把数据传到这统一处理)

Sentinel集群模式

在这里插入图片描述

假设经过压测,机器配置为4C8G最高能承受的TPS为 1500,而机器配置为8C16G能承受的TPS为3000,那如果采取单机限流,其阔值只能设置为1500,因为如果超过1500,会将4C8G的机器压垮。

为了充分利用硬件的资源,诸如 Dubbo 都提供了基于权重的负载均衡机制,例如可以将8C16G的机器设置的权重是4C8G的两倍,这样充分利用硬件资源.

如果是单机模式,我们要让程序根据不同的机型,分别设立阔值。
如果是集群模式,只需要对整个集群设置一个阔值。

原理

集群限流的原理很简单,和单机限流一样,都需要对 qps 等数据进行统计,区别就在於单机版是在每个实例中进行统计,而集群版是有一个专门的实例进行统计。
在这里插入图片描述

  • token client:集群流控客户端,用于向所属 token server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • token server:即集群流控服务端,处理来自 token client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。
//FlowRule策略中
 public boolean canPassCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                                    boolean prioritized) {
        String limitApp = rule.getLimitApp();
        if (limitApp == null) {
            return true;
        }
        //如果是集群模式,则调用Cluster
        if (rule.isClusterMode()) {
            return passClusterCheck(rule, context, node, acquireCount, prioritized);
        }
        //否则调用本地模式
        return passLocalCheck(rule, context, node, acquireCount, prioritized);
    }

部署方式

  1. 独立部署,就是单独启动一个 token server 服务来处理 token client 的请求
  2. 嵌入部署,就是在多个 sentinel-core 中选择一个实例设置为 token server,随着应用一起启动,其他的 sentinel-core 都是集群中 token client

若在生产环境使用集群限流,管控端还需要关注以下的问题:

  1. Token Server 自动管理(分配/选举 Token Server)
  2. Token Server 高可用,在某个 server 不可用时自动 failover 到其它机器

参考文章

Sentinel 原理-调用链
Sentinel 集群限流设计原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章