阿里大佬带你解密Sentinel

2,063 阅读6分钟

概述

在接连写了两篇关于限流的文章(《面试补习》- 你来说说什么是限流?, 限流神器Sentinel,不了解一下吗?)后,总感觉还差最后一点内容来闭环整个限流相关的内容,这两天在翻查相关文章后,找到了一篇通俗易懂的 《Sentinel 解密》 文章,感谢 寒舟大佬提供的帮助~ 将这部分内容补全。

1、Sentinel 概念

1.1、代码结构

image.png

1.2、核心概念

Resource

资源是 Sentinel 对所保护的内容的抽象,任何想保护的代码函数等都可以通过 SphU.entry 接口将其定义为一个资源,SphU.entry 接口的第一个参数描述了该资源的名称

Node

Node是sentinel中用来存储统计数据的内存结构,以树形结构和hash存储:

image.png

一个resource按照不同入口设置不同统计结点,存储context级别的统计信息;一个resource下设置统一的cluster node,存储resource粒度的统计信息;每个cluster node根据来源下挂不同origin node,存储各个来源的统计信息。

配置中不同限流模式其实最终对应的就是选择不同的node进行计算:

  • 直接模式: 选择cluster node
  • 关联模式: 选择关联resource的cluster node
  • 应用涞源: 选择origin node
  • 链路模式: 选择default node

Node 类型:

  • StatisticNode:最为基础的统计节点,是后续三种节点的父类,包含秒级和分钟级两个滑动窗口结构。
  • DefaultNode:链路节点,用于统计调用链路上某个资源的数据,维持树状结构。
  • ClusterNode:簇点,用于统计每个资源全局的数据(不区分调用链路),以及存放该资源的按来源区分的调用数据
  • EntranceNode:入口节点,特殊的链路节点,对应某个 Context 入口的所有调用数据。

image.png

Context

Context是对资源操作时的上下文环境, 同一个线程中通过thread local 进行传里, context中维护了currentEntry, 同一个线程中可以多次entry/exit, 所以entry同样维护了一个树状结构:

image.png

Entry

调用点,这里面保存着和某一特定资源的某一次调用相关的信息,比如这次调用的起始执行时间,结束时间,是否抛出异常,调用链路中的父子关系,该调用点对应资源的处理链(ProcessorSlotChain),还保存了该调用点所对应的资源统计节点

Slot

slot是规则处理器,通过责任链模式进行串联处理。

image.png

1.3、核心类图

image.png

sentinel主要通过Node实现结点维护统计信息、通过LeapArray实现滑动窗口算法。

2、Sentinel 流程

2.1、规则配置

image.png

应用通过订阅监听模式实现各种监听器,监听然后对应不同配置的进行更新,图中列出了几个主要监听器,如: 规则配置、基本配置、集群配置等。

2.2、规则验证

image.png

应用调用entry入口即启动了规则验证流程,主要步骤为获取context, 构建并执行slot chain,如果违反规则纪录失败计数,清理contxt;反之,则纪录成功计数,返回验证成功。

3、Sentinel 核心

3.1、限流算法

我们所熟知的限流算法包括: 1. 计数器 2. 固定窗口 3. 滑动窗口 4.漏桶 5.令牌桶等。

  • 1、计数器,维护一个计数器,访问进入计数器+1,如果超过阙值则拒绝、访问结束计数器-1。
  • 2、固定窗口,给定一个时间窗口,维护一个计数器、如果窗口内访问次数大于阙值则拒绝。
  • 3、滑动窗口,固定窗口有临界问题,所以提出滑动窗口思想,将一个大的时间窗口切分成更细粒度的子窗口,每个子窗口单独统计,每过一个子窗口的时间,就向右滑动一个子窗口。
  • 4、漏桶,滑动窗口不能解决平滑度问题,为了解决平滑度问题,提出漏桶算法进行流速控制。
  • 5、令牌桶: 跟漏桶算法相反,以很定的速度产生令牌,桶满则丢弃,请求访问时从桶里后去令牌,如果能够获取,则通过否则拒绝。

Sentinel中主要使用了计数器滑动窗口漏桶限流思想。比如对于线程数的流控,就是直接使用计数器进行限制,对于QPS的限制则使用了滑动窗口的思想,滑动窗口也是Sentinel的主要流控实现方式。

3.2、滑动窗口

image.png

Sentinel支持秒级的窗口,默认窗口大小1秒,假设分为5个子窗口,则每个子窗口的大小为200ms, 如上图,每个格子有个索引timeId, 那窗口是如何进行滑动呢?算法的关键实现就是在前面类图提到过的currentWindow()方法:

  • 1、根据当前时间戳计算timeId
  • 2、根据timdId获取当前子窗口
  • 3、如果子窗口不存在,则创建子窗口
  • 4、如果子窗口已经存在,则根据当前时间计算开始开始时间,对比子窗口的开始时间,如果开始时间一致,则说明此子窗口即当前窗口,如果大于子窗口时间,则需要滑动
  • 5、滑动其实就是resetWidow, 包括reset windowStartTime, window中的统计信息等,如上图所示

3.3、优先级

image.png

优先级的目的是为了区分正常流量和其他低优先级流量,比如压测流量。对于正常流量,如果超过阙值,不想直接拒绝,尝试借用将来窗口的指标,如果借用成功,则请求通过,如果借用失败,则拒绝。当时间窗口真正走到将来的时间点时,需要将借用窗口的计数器恢复设置。

那如何当前请求是否可以借用呢?如下图,主要维护一个借用时间阙值,计算一秒窗口内的总数有没有超过QPS,如果没有超过看借用时间是否超过借用阙值:

image.png

3.4、热点参数

一般资源的定义需要可枚举的,不能爆炸式的,假如对每个userId进行限流,这样会导致sentinel的存储空间爆炸式增长,最后导致OOM. 对于这种细粒度的参数限流,sentinel提供了热点参数方案。

如图:

热点参数就是在正常统计信息而外增加了参数统计,一样支持线程数和QPS两种限流方式,对于线程数,每个参数索引维护了一个LRU cache; 对于QPS,每个参数索引、每个子窗口、每个统计纬度维护了一个LRU cache, cache中维护了各种计数。通过LRU cache可以避免内存爆炸, 达到热点参数限流的目的。

image.png

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,我后面会每周都更新几篇高质量的大厂面试和常用技术栈相关的文章。感谢大伙能看到这里,如果这个文章写得还不错, 求三连!!! 创作不易,感谢各位的支持和认可,我们下篇文章见!

我是 九灵 ,有需要交流的童鞋可以 加我wx,Jayce-K,关注公众号:Java 补习课,掌握第一手资料! 如果本篇博客有任何错误,请批评指教,不胜感激 !