Sentinel使用示例、源码解析、滑动时间窗口算法等详解

目录

一、基本使用

(1)限流方式一:捕获异常的方式定义资源

1. pom文件引入

2. 代码示例准备

3. 运行代码

 (2)限流方式二注解方式:

1. 需要引入支持注解的jar包:

2. 注解方式代码:

3. 运行代码

(3)熔断降级

二、源码解析

1. 整体看源码链路

SentinelResourceAspect

(1). CtSph.lookProcessChain() 核心方法!

(2).  chain.entry 整个链条的调用

2. StatisticSlot.entry

3. FlowSlot.entry

4. DegradeSlot.entry

三、限流算法滑动时间窗口

1. StatisticSlot.entry

1)rollingCounterInSecond

2)StatisticNode.addPass()

2. ArrayMetric.addPass()

3. 自己实现滑动时间窗口算法

四、漏桶算法

五、令牌桶算法

 六、限流算法对比

1. 计数器 VS 滑动窗口

2. 漏桶算法 VS 令牌桶算法


一、基本使用

Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

下面示例是SpringBoot结合Sentinel,使用上比较简单。

(1)限流方式一:捕获异常的方式定义资源

1. pom文件引入

    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.6.RELEASE
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-sentinel
            2.2.5.RELEASE
        

        
            org.projectlombok
            lombok
            1.18.6
        
    

2. 代码示例准备

// 项目启动类
@SpringBootApplication
public class SentinelApplication {
    public static void main(String[] args) {
        SpringApplication.run(SentinelApplication.class, args);
    }
}
// service层
@Service
public class OrderQueryService {
    public String queryOrderInfo(String orderId) {
        System.out.println("获取订单信息:" + orderId);
        return "return OrderInfo :" + orderId;
    }
}
// controller层
@Slf4j
@RestController
public class OrderCreateController {

    @Autowired
    private OrderQueryService orderQueryService;

    /**
     * 最原始方法--需要捕获异常
     * @param orderId
     * @return
     */
    @RequestMapping("/getOrder")
    @ResponseBody
    public String queryOrder1(@RequestParam("orderId") String orderId) {
        Entry entry = null;
        try {
            // 传入资源
            entry = SphU.entry(SentinelConfigs.KEY);
            return orderQueryService.queryOrderInfo(orderId);
        } catch (BlockException e) {
            // 接口被限流的时候, 会进入到这里
            log.warn("---queryOrder1接口被限流了---, exception: ", e);
            return "接口限流, 返回空";
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
    }
}

sentinel规则设置:

@Component
public class SentinelConfigs {
    public static final String KEY = "flowSentinelKey";

    @Bean
    public void initFlowQpsRule() {
        List rules = new ArrayList();
        FlowRule rule1 = new FlowRule();
        rule1.setResource(KEY);
        // QPS控制在2以内
        rule1.setCount(2);
        // QPS限流
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
    }
}

3. 运行代码

当qps>2时,就会触发限流,如下:

Sentinel使用示例、源码解析、滑动时间窗口算法等详解_第1张图片

 (2)限流方式二:注解方式:

上面是最原始的方式实现限流,但是对代码侵入性太高,我们推荐使用注解方式!

1. 需要引入支持注解的jar包:

        
            com.alibaba.csp
            sentinel-annotation-aspectj
            1.8.0
        
/**
 * Sentinel切面类配置
 */
@Configuration
public class SentinelAspectConfiguration {

    @Bean
    public SentinelResourceAspect getSentinelResource() {
        return new SentinelResourceAspect();
    }
}

2. 注解方式代码:

    @SentinelResource(value = "getOrderInfo",
            blockHandler = "flowQpsException",
            fallback = "queryOrder2FallBack")
    public String queryOrder2(String orderId) {
        // 模拟接口运行时抛出代码异常
        if ("0".equals(orderId)) {
            throw new RuntimeException();
        }
        System.out.println("获取订单信息:" + orderId);
        return "return OrderInfo :" + orderId;
    }

    /**
     * qps异常,被限流
     * 注意: 方法参数、返回值要与原函数保持一致
     *
     * @param orderId
     * @param e
     * @return
     */
    public String flowQpsException(String orderId, BlockException e) {
        e.printStackTrace();
        return "flowQpsException for queryOrder22: " + orderId;
    }

    /**
     * 运行时异常的fallback方式
     * 注意: 方法参数、返回值要与原函数保持一致
     *
     * @param orderId
     * @param a
     * @return
     */
    public String queryOrder2FallBack(String orderId, Throwable a) {
        return "fallback queryOrder2: " + orderId;
    }

controller方法测试:

    /**
     * 限流方式二:注解方式定义
     *
     * @param orderId
     * @return
     */
    @RequestMapping("/query/order2")
    @ResponseBody
    public String queryOrder3(@RequestParam("orderId") String orderId) {
        return orderQueryService.queryOrder2(orderId);
    }

3. 运行代码

模拟运行时异常,执行fallback方法

请求:​​​​​​http://localhost:8080//query/order2?orderId=0

Sentinel使用示例、源码解析、滑动时间窗口算法等详解_第2张图片

 模拟限流:

请求:http://localhost:8080//query/order2?orderId=1223

Sentinel使用示例、源码解析、滑动时间窗口算法等详解_第3张图片

正常执行了限流操作。 

(3)熔断降级

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

直接上代码:

@Slf4j
@RestController
public class DegradeController {
    @Autowired
    private DegradeService degradeService;

    @RequestMapping("/query/info/v1")
    public String queryInfo(@RequestParam("spuId") String spuId) {
        String res = degradeService.queryInfo(spuId);
        return res;
    }
}

 降级这里有三种设置方式,这里只写一个例子:设置异常比例。

@Configuration
public class DegradeConfigs {

    /**
     * Sentinel切面类配置
     * @return
     */
    @Bean
    public SentinelResourceAspect getSentinelResource() {
        return new SentinelResourceAspect();
    }

    /**
     * DEGRADE_GRADE_EXCEPTION_RATIO
     * 将比例设置成0.1将全部通过, exception_ratio = 异常/通过量
     * 当资源的每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态
     */
    @PostConstruct
    public void initDegradeRule() {
        List rules = new ArrayList<>();
        DegradeRule rule = new DegradeRule();
        rule.setResource("queryInfo");
        
        rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
        // 错误比例设置为0.1
        rule.setCount(0.1);

        // 设置时间窗口:10s
        rule.setTimeWindow(10);
        rules.add(rule);
        DegradeRuleManager.loadRules(rules);
    }
}
@Slf4j
@Service
public class DegradeService {
    private static final String KEY = "queryInfo";

    @SentinelResource(value = KEY,
            blockHandler = "blockHandlerMethod",
            fallback = "queryInfoFallback")
    public String queryInfo(String spuId) {

        // 模拟调用服务出现异常
        if ("0".equals(spuId)) {
            throw new RuntimeException();
        }
        return "query goodsinfo success, " + spuId;
    }

    public String blockHandlerMethod(String spuId, BlockException e) {
        log.warn("queryGoodsInfo222 blockHandler", e.toString());
        return "已触发限流, blockHandlerMethod res: " + spuId;

    }

    public String queryInfoFallback(String spuId, Throwable e) {
        log.warn("queryGoodsInfo222 fallback", e.toString());
        return "业务异常, return fallback res: " + spuId;
    }
}

测试的时候,可以使用jmeter压一下,我设置的比较简单,使用postman或者网页直接请求都行,多点几次,就能看到降级的执行:

Sentinel使用示例、源码解析、滑动时间窗口算法等详解_第4张图片

测试请求:http://localhost:8080/query/info/v1?spuId=0 

Sentinel使用示例、源码解析、滑动时间窗口算法等详解_第5张图片

官方也有降级例子,可以看下:
熔断降级 · alibaba/Sentinel Wiki · GitHub

二、源码解析

说明:

1. 先概括性的看源码的整个调用链路,下面三部分介绍几个常用的重要slot的源码

2. StatisticSlot 用于存储资源的统计信息,例如资源的RT、QPS、thread count等等,这些信息将用作多维度限流,降级的依据(fireEntry用来触发下一个规则的调用)

3. FlowSlot 校验资源流控规则

4. DegradeSlot 校验资源流降级规则

1. 整体看源码链路

SentinelResourceAspect

// 请关注这行代码
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());

一直点进去,看里面的方法

CtSph.entryWithType
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized, Object[] args) throws BlockException {
        // 将资源名称包装成StringResourceWrapper
        StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
        return this.entryWithPriority(resource, count, prioritized, args);
    }

CtSph.asyncEntryWithPriorityInternal()

    // 前面的if else边缘逻辑,先忽略
    private AsyncEntry asyncEntryWithPriorityInternal(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException {
        Context context = ContextUtil.getContext();
        if (context instanceof NullContext) {
            return this.asyncEntryWithNoChain(resourceWrapper, context);
        } else {
            if (context == null) {
                context = CtSph.InternalContextUtil.internalEnter("sentinel_default_context");
            }

            if (!Constants.ON) {
                return this.asyncEntryWithNoChain(resourceWrapper, context);
            } else {
                // 核心方法!!!
                ProcessorSlot chain = this.lookProcessChain(resourceWrapper);
                if (chain == null) {
                    return this.asyncEntryWithNoChain(resourceWrapper, context);
                } else {
                    AsyncEntry asyncEntry = new AsyncEntry(resourceWrapper, chain, context);

                    try {
                        // 对整个链条的调用,下面详细讲解
                        chain.entry(context, resourceWrapper, (Object)null, count, prioritized, args);
                        asyncEntry.initAsyncContext();
                        asyncEntry.cleanCurrentEntryInLocal();
                    } catch (BlockException var9) {
                        asyncEntry.exitForContext(context, count, args);
                        throw var9;
                    } catch (Throwable var10) {
                        RecordLog.warn("Sentinel unexpected exception in asyncEntryInternal", var10);
                        asyncEntry.cleanCurrentEntryInLocal();
                    }

                    return asyncEntry;
                }
            }
        }
    }
 
  

(1). CtSph.lookProcessChain() 核心方法!

    private static volatile Map chainMap = new HashMap();
        
    ProcessorSlot lookProcessChain(ResourceWrapper resourceWrapper) {
        ProcessorSlotChain chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
        // double check 提升加锁性能【想想单例模式】
        if (chain == null) {
            synchronized(LOCK) {
                chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
                if (chain == null) {
                    if (chainMap.size() >= 6000) {
                        return null;
                    }

                    // pipeline  联想下责任链模式,后面会对这个方法进行详细讲解
                    chain = SlotChainProvider.newSlotChain();
                    Map newMap = new HashMap(chainMap.size() + 1);
                    newMap.putAll(chainMap);
                    newMap.put(resourceWrapper, chain);
                    chainMap = newMap;
                }
            }
        }

        return chain;
    }
 
  
SlotChainProvider.newSlotChain()
    public static ProcessorSlotChain newSlotChain() {
        if (slotChainBuilder != null) {
            return slotChainBuilder.build();
        } else {
            slotChainBuilder = (SlotChainBuilder)SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);
            if (slotChainBuilder == null) {
                RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default", new Object[0]);
                // 初始化DefaultSlotChainBuilder
                slotChainBuilder = new DefaultSlotChainBuilder();
            } else {
                RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: " + slotChainBuilder.getClass().getCanonicalName(), new Object[0]);
            }

            // 构建一个链条
            return slotChainBuilder.build();
        }
    }
DefaultSlotChainBuilder.build() 
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        List sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
        Iterator var3 = sortedSlotList.iterator();

        while(var3.hasNext()) {
            ProcessorSlot slot = (ProcessorSlot)var3.next();
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain", new Object[0]);
            } else {
                // 就是在链表上增加一个节点
                chain.addLast((AbstractLinkedProcessorSlot)slot);
            }
        }

        return chain;
    }

(2).  chain.entry 整个链条的调用

DefaultProcessorSlotChain.entry()
    public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable {
        // 链条的第一个
        this.first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
    }
AbstractLinkedProcessorSlot.transformEntry
    void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args) throws Throwable {
        this.entry(context, resourceWrapper, o, count, prioritized, args);
    }

first是什么:

AbstractLinkedProcessorSlot first = new AbstractLinkedProcessorSlot() {
        public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable {
            super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
        }

        public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
            super.fireExit(context, resourceWrapper, count, args);
        }
    }; 
  
fireEntry()方法
    public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable {
        if (this.next != null) {
            // next的transformEntry方法
            this.next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
        }

    }

2. StatisticSlot.entry

    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
        Iterator var8;
        ProcessorSlotEntryCallback handler;
        try {
            // do some checking
            this.fireEntry(context, resourceWrapper, node, count, prioritized, args);
            // 增加线程数(LongAdder加一)
            node.increaseThreadNum();
            // 统计通过的请求
            node.addPassRequest(count);
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseThreadNum();
                context.getCurEntry().getOriginNode().addPassRequest(count);
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseThreadNum();
                Constants.ENTRY_NODE.addPassRequest(count);
            }

            Iterator var13 = StatisticSlotCallbackRegistry.getEntryCallbacks().iterator();

            while(var13.hasNext()) {
                ProcessorSlotEntryCallback handler = (ProcessorSlotEntryCallback)var13.next();
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        } catch (PriorityWaitException var10) {
            node.increaseThreadNum();
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseThreadNum();
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseThreadNum();
            }

            var8 = StatisticSlotCallbackRegistry.getEntryCallbacks().iterator();

            while(var8.hasNext()) {
                handler = (ProcessorSlotEntryCallback)var8.next();
                handler.onPass(context, resourceWrapper, node, count, args);
            }
            // 要注意看这个异常捕获
        } catch (BlockException var11) {
            BlockException e = var11;
            context.getCurEntry().setBlockError(var11);
            // 增加block的统计
            node.increaseBlockQps(count);
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseBlockQps(count);
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseBlockQps(count);
            }

            var8 = StatisticSlotCallbackRegistry.getEntryCallbacks().iterator();

            while(var8.hasNext()) {
                handler = (ProcessorSlotEntryCallback)var8.next();
                handler.onBlocked(e, context, resourceWrapper, node, count, args);
            }

            throw e;
        // 抛出业务异常
        } catch (Throwable var12) {
            context.getCurEntry().setError(var12);
            throw var12;
        }

    }

3. FlowSlot.entry

    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
        this.checkFlow(resourceWrapper, context, node, count, prioritized);
        this.fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
FlowRuleChecker.checkFlow
    public void checkFlow(Function> ruleProvider, ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
        if (ruleProvider != null && resource != null) {
            // 从内存中拿流控规则
            Collection rules = (Collection)ruleProvider.apply(resource.getName());
            if (rules != null) {
                Iterator var8 = rules.iterator();

                // 遍历规则,看能否通过校验
                while(var8.hasNext()) {
                    FlowRule rule = (FlowRule)var8.next();
                    // 对资源逐条校验每个规则
                    if (!this.canPassCheck(rule, context, node, count, prioritized)) {
                        throw new FlowException(rule.getLimitApp(), rule);
                    }
                }
            }

        }
    }
    private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) {
        Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
        return selectedNode == null ? true : rule.getRater().canPass(selectedNode, acquireCount, prioritized);
    }
DefaultController.canPass
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        int curCount = this.avgUsedTokens(node);
        if ((double)(curCount + acquireCount) > this.count) {
            // 先不用关注
            if (prioritized && this.grade == 1) {
                long currentTime = TimeUtil.currentTimeMillis();
                long waitInMs = node.tryOccupyNext(currentTime, acquireCount, this.count);
                if (waitInMs < (long)OccupyTimeoutProperty.getOccupyTimeout()) {
                    node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                    node.addOccupiedPass(acquireCount);
                    this.sleep(waitInMs);
                    throw new PriorityWaitException(waitInMs);
                }
            }

            return false;
        } else {
            return true;
        }
    }

4. DegradeSlot.entry

    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
        this.performChecking(context, resourceWrapper);
        this.fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

三、限流算法滑动时间窗口

滑动时间窗口计数器算法思想:它将时间窗口划分为更小的时间片断,每过一个时间片断,时间窗口就会往右滑动一格,每一个时间片断都有独立的计数器。在计算整个时间窗口内的请求总数会累加全部的时间片断内的计数器。时间窗口划分的越细,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

Sentinel使用示例、源码解析、滑动时间窗口算法等详解_第6张图片

ps: 图片来源于网络 

1. StatisticSlot.entry

node.addPassRequest(count);
    public void addPassRequest(int count) {
        super.addPassRequest(count);
        this.clusterNode.addPassRequest(count);
    }
StatisticNode.addPassRequest
    public void addPassRequest(int count) {
        // 滑动时间窗口实现
        this.rollingCounterInSecond.addPass(count);
        this.rollingCounterInMinute.addPass(count);
    }
StatisticNode类:

1)rollingCounterInSecond

private transient volatile Metric rollingCounterInSecond;
    private transient Metric rollingCounterInMinute;
    private LongAdder curThreadNum;
    private long lastFetchTime;

    public StatisticNode() {

        // 看下传入的这两个参数  2  1000ms
        this.rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);
        this.rollingCounterInMinute = new ArrayMetric(60, 60000, false);
        this.curThreadNum = new LongAdder();
        this.lastFetchTime = -1L;
    }

看下ArrayMetric类:

    public ArrayMetric(int sampleCount, int intervalInMs) {
        this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
    }
OccupiableBucketLeapArray的构造方法:
    public OccupiableBucketLeapArray(int sampleCount, int intervalInMs) {
        // 看下super
        super(sampleCount, intervalInMs);
        this.borrowArray = new FutureBucketLeapArray(sampleCount, intervalInMs);
    }
    public LeapArray(int sampleCount, int intervalInMs) {
        AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
        AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
        AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");

        // 一些初始化
        // 500:时间窗口的长度
        // windowLengthInMs = 1000 / 2 = 500
        this.windowLengthInMs = intervalInMs / sampleCount;
        this.intervalInMs = intervalInMs;
        this.sampleCount = sampleCount;
        // new一个大小为2的数组 ==》sampleCount就是桶的个数
        this.array = new AtomicReferenceArray(sampleCount);
    }

2)StatisticNode.addPass()

    public void addPassRequest(int count) {
        // 滑动时间窗口实现
        this.rollingCounterInSecond.addPass(count);
        this.rollingCounterInMinute.addPass(count);
    }

2. ArrayMetric.addPass()

    @Override 
    public void addPass(int count) {
        // 下面来看下currentWindow()方法
        // 获取当前时间对应的时间窗口
        WindowWrap wrap = this.data.currentWindow();
        ((MetricBucket)wrap.value()).addPass(count);
    }
LeapArray.currentWindow()

其实注释写的已经很清楚了

    public WindowWrap currentWindow() {
        return this.currentWindow(TimeUtil.currentTimeMillis());
    }



    /**
     * Get bucket item at provided timestamp.
     *
     * @param timeMillis a valid timestamp in milliseconds
     * @return current bucket item at provided timestamp if the time is valid; null if time is invalid
     */
    public WindowWrap currentWindow(long timeMillis) {
        if (timeMillis < 0) {
            return null;
        }

        // 计算落在哪个时间窗口里
        int idx = calculateTimeIdx(timeMillis);
        // Calculate current bucket start time.
        // 落在这个时间窗口的具体坐标
        long windowStart = calculateWindowStart(timeMillis);

        /*
         * Get bucket item at given time from the array.
         *
         * (1) Bucket is absent, then just create a new bucket and CAS update to circular array.
         * (2) Bucket is up-to-date, then just return the bucket.
         * (3) Bucket is deprecated, then reset current bucket and clean all deprecated buckets.
         */
        while (true) {
            WindowWrap old = array.get(idx);
            if (old == null) {
                /*
                 *     B0       B1      B2    NULL      B4
                 * ||_______|_______|_______|_______|_______||___
                 * 200     400     600     800     1000    1200  timestamp
                 *                             ^
                 *                          time=888
                 *            bucket is empty, so create new and update
                 *
                 * If the old bucket is absent, then we create a new bucket at {@code windowStart},
                 * then try to update circular array via a CAS operation. Only one thread can
                 * succeed to update, while other threads yield its time slice.
                 */
                WindowWrap window = new WindowWrap(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                if (array.compareAndSet(idx, null, window)) {
                    // Successfully updated, return the created bucket.
                    return window;
                } else {
                    // Contention failed, the thread will yield its time slice to wait for bucket available.
                    Thread.yield();
                }
            } else if (windowStart == old.windowStart()) {
                /*
                 *     B0       B1      B2     B3      B4
                 * ||_______|_______|_______|_______|_______||___
                 * 200     400     600     800     1000    1200  timestamp
                 *                             ^
                 *                          time=888
                 *            startTime of Bucket 3: 800, so it's up-to-date
                 *
                 * If current {@code windowStart} is equal to the start timestamp of old bucket,
                 * that means the time is within the bucket, so directly return the bucket.
                 */
                return old;
            } else if (windowStart > old.windowStart()) {
                /*
                 *   (old)
                 *             B0       B1      B2    NULL      B4
                 * |_______||_______|_______|_______|_______|_______||___
                 * ...    1200     1400    1600    1800    2000    2200  timestamp
                 *                              ^
                 *                           time=1676
                 *          startTime of Bucket 2: 400, deprecated, should be reset
                 *
                 * If the start timestamp of old bucket is behind provided time, that means
                 * the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
                 * Note that the reset and clean-up operations are hard to be atomic,
                 * so we need a update lock to guarantee the correctness of bucket update.
                 *
                 * The update lock is conditional (tiny scope) and will take effect only when
                 * bucket is deprecated, so in most cases it won't lead to performance loss.
                 */
                if (updateLock.tryLock()) {
                    try {
                        // Successfully get the update lock, now we reset the bucket.
                        // 重置时间窗口
                        return resetWindowTo(old, windowStart);
                    } finally {
                        updateLock.unlock();
                    }
                } else {
                    // Contention failed, the thread will yield its time slice to wait for bucket available.
                    Thread.yield();
                }
            } else if (windowStart < old.windowStart()) {
                // Should not go through here, as the provided time is already behind.
                return new WindowWrap(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            }
        }
    }

再回到ArrayMetric.addPass()方法来看:

    @Override 
    public void addPass(int count) {
        WindowWrap wrap = this.data.currentWindow();
        // 计算时间窗口里面的统计数据
        ((MetricBucket)wrap.value()).addPass(count);
    }
MetricBucket.addPass()

下面分析一个统计pass的指标,其他的类型,比如抛异常时的block指标,可以类比来看

    public void addPass(int n) {
        // 增加一个pass的统计指标
        add(MetricEvent.PASS, n);
    }

MetricEvent枚举类:

public enum MetricEvent {

    /**
     * Normal pass.
     */
    // 代表通过的所有校验规则
    PASS,
    /**
     * Normal block.
     */
    // 没有通过校验规则,抛出BlockException的调用
    BLOCK,
    // 发生了正常业务异常的调用
    EXCEPTION,
    // 调用完成的情况,不管是否抛异常了
    SUCCESS,
    // 所有的success调用耗费的总时间
    RT,

    /**
     * Passed in future quota (pre-occupied, since 1.5.0).
     */
    OCCUPIED_PASS
}

MetricBucket.add(MetricEvent event, long n)

    private final LongAdder[] counters;

    private volatile long minRt;

    public MetricBucket() {
        MetricEvent[] events = MetricEvent.values();
        this.counters = new LongAdder[events.length];
        for (MetricEvent event : events) {
            counters[event.ordinal()] = new LongAdder();
        }
        initMinRt();
    }    


    // 增加时间窗口里的通过请求数
    public MetricBucket add(MetricEvent event, long n) {
        counters[event.ordinal()].add(n);
        return this;
    }

3. 自己实现滑动时间窗口算法

代码逻辑:新建一个本地缓存,每5s为一个时间窗口,每1s为一个时间片断,时间片断作为缓存的key,原子类计数器做为缓存的value。每秒发送随机数量的请求,计算每一个时间片断的前5秒内的累加请求数量,超出阈值则限流。

先引入guava依赖

        
            com.google.guava
            guava
            18.0
        
@Slf4j
public class WindowLimitTest {
    private LoadingCache counter =
            CacheBuilder.newBuilder()
                    .expireAfterWrite(10, TimeUnit.SECONDS)
                    .build(new CacheLoader() {
                        @Override
                        public AtomicLong load(Long seconds) throws Exception {
                            return new AtomicLong(0);
                        }
                    });

    // 线程池建议使用手动创建,线程池不是这篇博客的重点,为了简便,就简单来写了
    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);

    // 限流阈值
    private long limit = 15;

    /**
     * 每隔1s累加前5s内每1s的请求数量,判断是否超出限流阈值
     */
    public void slideWindow() {
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            try {
                long time = System.currentTimeMillis() / 1000;
                //每秒发送随机数量的请求
                int reqs = (int) (Math.random() * 5) + 1;
                counter.get(time).addAndGet(reqs);
                long nums = 0;
                // time windows 5 s
                for (int i = 0; i < 5; i++) {
                    nums += counter.get(time - i).get();
                }
                log.info("time=" + time + ",nums=" + nums);
                if (nums > limit) {
                    log.info("限流了,nums=" + nums);
                }
            } catch (Exception e) {
                log.error("slideWindow() error", e);
            } finally {

            }
        }, 5000, 1000, TimeUnit.MILLISECONDS);
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        WindowLimitTest limit = new WindowLimitTest();
        limit.slideWindow();
    }
}

输出结果:

Sentinel使用示例、源码解析、滑动时间窗口算法等详解_第7张图片

四、漏桶算法

RateLimiterController

类比消息队列;

简单写下思路,感兴趣的可以自己完善

public class LeakyBucket {
    // 当前时间
    public long timeStamp = System.currentTimeMillis();

    // 桶的容量
    public long capatity;
    
    // 水漏出的速度(每秒系统能处理的请求数)
    public long rate;

    // 当前水量(当前累积请求数)
    public long water;
    
    public boolean limit() {
        long now = System.currentTimeMillis();
        // 先执行漏水,计算剩余水量
        water = Math.max(0, water - (now - timeStamp) / 1000 * rate);
        timeStamp = now;
        if ((water + 1) < capatity) {
            // 尝试加水,并且还未满
            water += 1;
            return true;
        } else {
            // 水满,拒绝加水
            return false;
        }
    }
}

五、令牌桶算法

WarmUpController.canPass

warm up--预热

简单写下思路,感兴趣的可以自己完善

public class TokenBucket {
    // 当前时间
    public long timeStamp = System.currentTimeMillis();

    // 桶的容量
    public long capatity;

    // 令牌进入速度
    public long rate;

    // 当前令牌数量
    public long tokens;
    
    public boolean grant() {
        long now = System.currentTimeMillis();
        // 先执行漏水,计算剩余水量
        tokens = Math.min(capatity, tokens + (now - timeStamp) * rate);
        timeStamp = now;
        if (tokens < 1) {
            // 若不到1个令牌,则拒绝
            return false;
        } else {
            // 还有令牌,领取令牌
            tokens -= 1;
            return true;
        }
    }
}

 六、限流算法对比

1. 计数器 VS 滑动窗口

1)计数器算法是最简单的算法,可以看成是滑动窗口的低精度实现。

2)滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。

3)也就是说,如果滑动窗口的精度越高,需要的存储空间越大。

2. 漏桶算法 VS 令牌桶算法

1)漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。

2)因为默认的令牌桶算法,取走token是不需要耗费时间的,也就是说,假设桶内有100个token时,那么可以瞬间允许100个请求通过。

3)当然我们需要具体情况具体分析,只有最合适的算法,没有最优的算法。

你可能感兴趣的:(java技术架构学习-new,eureka,java,postman)