一、Sentinel工作原理
-
工作流程:一个请求发送,会贯穿多个资源有序进行,最终对不同资源会有对用的插槽,进行流量监控和限制等操作
-
架构图解析
- 核心骨架(ProcessorSloatChain):将不同的slot按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级和保护)组合到一起,系统会为每一套资源创建一个slotChain
- slotChain分为两部分
- 统计数据构建(statistic)
- 判断部分(rule checking)
-
SPI机制
- Sentinel中各slot的执行顺序是固定的。Sentinel将ProcessorSlot作为SPI的接口进行扩展,使得SloatChain具备了扩展能力,用户可以自定义slot并插入到固定的slot的任意位置
-
在 Sentinel 里面,每一个资源都对应一个资源名称和Entry。
-
Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 API 显式创建
-
每一个 Entry 创建的时候,同时也会创建一系列功能插槽(slot chain)。这些插槽有不同的职责
- Slot介绍:用于对资源的流量监控、限制和降级
- NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
- StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
- FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
- AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
- DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
- SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
- Context介绍:贯穿于整个请求的资源的上下文
- 每一个资源必须属于一个上下文,如果没有创建,则会自动创建一个name为sentinel_default_context的默认context
- Context 代表调用链路上下文,贯穿一次调用链路中的所有 Entry。Context 维 持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息。Context 名称即为调用链路入口名称。
- Context 维持的方式:通过 ThreadLocal 传递,只有在入口 enter 的时候生效。由于 Context 是通过 ThreadLocal 传递的,因此对于异步调用链路,线程切换的时候会丢掉 Context,因此需要手动通过 ContextUtil.runOnContext(context, from) 来变换 context。
- ContextUtil.runOnContext(context, from):创建一个来自from资源的访问的上下文。
- from:请求来源的上级资源服务名称
- Entry entry = SphU.entry(xxx) :假设资源名(配置的监控的资源名称)为xxx,获取当前资源的Entry对象;Entry对象是否存在就代表是否有限流或者降级
- entry.exit():最终要释放上下文,避免线程之间出现上下文干扰
- Entry:存储每个资源的信息,以及当前节点和来源节点等信息
- 每一次资源调用都会创建一个 Entry。Entry 包含了资源名、curNode(当前统计节点)、originNode(来源统计节点)等信息。
- CtEntry 为普通的 Entry,在调用 SphU.entry(xxx) 的时候创建。特性:Linked entry within current context(内部维护着 parent 和 child)
- CtEntry 构造函数中会做调用链的变换,即将当前 Entry 接到传入 Context 的调用链路上(setUpEntryFor)。
- 资源调用结束时需要 entry.exit()。exit 操作会过一遍 slot chain exit,恢复调用栈,exit context 然后清空 entry 中的 context 防止重复调用。
- CtEntry 为普通的 Entry,在调用 SphU.entry(xxx) 的时候创建。特性:Linked entry within current context(内部维护着 parent 和 child)
- 每一次资源调用都会创建一个 Entry。Entry 包含了资源名、curNode(当前统计节点)、originNode(来源统计节点)等信息。
- Node:Sentinel 里面的各种种类的统计节点
- StaticNode:统计节点,滑动窗口的结构
- EntranceNode:入口节点,用于统计一个Context的总流量
- DefaultNode:默认节点,用于统计一个资源的流量
- ClusterNode:用于统计所有Context的同一资源的总流量
- Slot介绍:用于对资源的流量监控、限制和降级
二、Sentinel核心源码
2.1、SentinelResourceAspect:Sentinel切面拦截类
- 拦截@SentinelResource
- 通过注解获取 value:获取对应的资源名,若不存在,则反射获取方法名作为资源名、entryType和resourceType
- entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());//创建entry增强
- 内部进入:entryWithType(name, resourceType, entryType, count, false, args)
- count参数:代表当前请求的qps增长值,假设 是1,则代表,当前资源被访问流量+1
- 然后进入重载方法,构建资源对象StringResourceWrapper,进入entryWithPriority方法
- entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object… args)
- prioritized:代表是否进行优先级处理,false:代表直接通过,true:代表需要判定
- 等待时长,根据配置的流量规则进行计算
- prioritized:代表是否进行优先级处理,false:代表直接通过,true:代表需要判定
- entryWithPriority方法详解
- Context context = ContextUtil.getContext();//会从ThreaLocal中获取当前线程的上下文
- context instanceof NullContext:如果是此上下文,则代表当前线程已经超过了上下文的阈值,则值初始化entry对象,不再进行任何规则检测
- context == null:则创建一个默认的上下文
- !Constants.ON:如果关闭了全局检测,则直接返回entry对象,不再进行任何规则检测
- ProcessorSlot chain = lookProcessChain(resourceWrapper); //查找或创建slotChain,也就是获得一个完整的调用链路
- 先从缓存中获取,key代表资源,value就是整个slotChain 调用链路
- 如果没有找到或者超过了最大的链路规定数量,则chain == null,初始化Entry对象,不再进行任何规则检测
- 否则创建一个新的链路并放到缓存中,并返回ProcessorSlot对象
- 随后:chain.entry(context, resourceWrapper, null, count, prioritized, args); 对整个调用链路操作
- 按照整个固定的调用链路,再次调用各slot的entry方法,然后进入StaticSlot的entry方法
- StaticSlot的entry方法,就是流控规则的起始方法
- fireEntry(context, resourceWrapper, node, count, prioritized, args);
- 此方法就会调用后续的其他校验slot,验证流控规则方法(FlowSlot、DegradeSlot等流控降级等验证)- 会根据流控规则。抛出对应的错误异常
- 如果没有被限流,则代表允许通过,会将通过线程数和通过请求数,分别增加传递过来的count(通常就是1)
- FlowSlot(流控)分析:
- checkFlow(resourceWrapper, context, node, count, prioritized); //检测流控规则方法
- Collection rules = ruleProvider.apply(resource.getName());
- 获取当前资源的所有设定的流控规则,循环执行 canPassCheck 方法,只要遇到无法通过的直接抛出异常
- FLowRule对象就是配置的流控规则
- 资源名、来源(limitApp)、阈值类型(grade)、单机阈值(count)
- 流控模式(strategy)、关联资源(refResource)、流控效果(controlBehavior)
- 预热时长(warmUpPeriodes)、排队等待超时时间(maxQueueingTimeMs)
- 和管理端的配置一对一
- Collection rules = ruleProvider.apply(resource.getName());
- checkFlow(resourceWrapper, context, node, count, prioritized); //检测流控规则方法
- DegradeSlot(降级)分析:
- performChrck(resourceWrapper, context); //检测降级方法
- List circuitBreakers = DegradeRuleManager.getCircuitBreakers(resourceWrapper.getName());
- 获取当前资源的降级规则,循环执行tryPass方法,如果没有通过熔断规则,抛出异常
- CircuitBreaker对象就是配置的降级规则
- List circuitBreakers = DegradeRuleManager.getCircuitBreakers(resourceWrapper.getName());
- performChrck(resourceWrapper, context); //检测降级方法
- context创建:
- 先从缓存的Map查找,key就是context,value就是Entry
- 用到了双重检测,避免并发创建
- 创建成功后,放到缓存和ThreaLocal
- 用到了map迭代问题,为了避免其他地方迭代获取脏读数据,
- 因此,给map赋值需要使用复制并交换的策略,也就是创建一个新的map增加属性,并将旧的map对象指向新的map
- 用到了map迭代问题,为了避免其他地方迭代获取脏读数据,
- 先从缓存的Map查找,key就是context,value就是Entry
- slotChain创建:
- 先从缓存中找slotBuilder,如果有则直接返回,如果没有则创建一个默认的
- slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);
- 通过SPI创建一个规定顺序的slotChainBuilder
- slotChainBuilder.build(); 构建一个SlotChain
- slotChainBuilder.build()
- List sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
- slotChain.addLast(sortedSlot); //按照顺序依次添加到slotChain中
- 通过SPI构建默认的slot链路
- 先从缓存中找slotBuilder,如果有则直接返回,如果没有则创建一个默认的
三、滑动时间算法
3.1、算法原理
- 当某一个时间点接收到一个请求,会向前查找x时间的总请求量,判断和预定的请求阈值作比较
- 问题:频繁的获取x时间的请求量,会对整体效率有所影响
- 改进:将一整段时间窗口,再划分为多个样本窗口(样本窗口是某一段时间的数量统计)
- 假设时间窗口100,划分为了四个样本窗口,每25就作为一个统计范围,后续的某个时间点。直接获取样本窗口的统计量,只分析处理样本窗口中的请求总量即可
- 假设时间窗口100,划分为了四个样本窗口,每25就作为一个统计范围,后续的某个时间点。直接获取样本窗口的统计量,只分析处理样本窗口中的请求总量即可
3.2、相关源码解析
- 滑动时间算法在StatisticSlot 中
- entry方法中,执行node.addPassRequest(count)
- 内部对滑动计数器进行数据统计 addPass方法
- 获取当前时间所在的样本窗口:WindowWrap wrap = data.currentWindow();
- 计算样本窗口的下标:int idx = calculateTimeIdx(timeMillis);
- 样本窗口本质上是在一个数组中(LeadArray),每一个样本窗口,默认都是从0开始以此排序
- 获取时间当前时间所在样本窗口的时间起始点:long windowStart = calculateWindowStart(timeMillis);
- 因为设定的时间范围是固定的,所以整个过程是一个环形状,因此,当时间超过了设定的时间,会存在一种情况,当前时间计算的时间窗口回到了起始点的样本窗口,但是之前记录的样本窗口起始时间是之前的旧时间,因此在这里判断了样本窗口的起始时间,resetWindowTo(old, windowStart);方法对起始时间进行了更新
- 计算样本窗口的下标:int idx = calculateTimeIdx(timeMillis);
- 将当前样本窗口的请求数+count:wrap.value().addPass(count);
- 获取当前时间所在的样本窗口:WindowWrap wrap = data.currentWindow();
- 通过资源获取所有时间窗口样本中的请求量和总的时间窗口 相除。即可获取到当前通过的QPS
- 内部对滑动计数器进行数据统计 addPass方法
- entry方法中,执行node.addPassRequest(count)
- 改进:将一整段时间窗口,再划分为多个样本窗口(样本窗口是某一段时间的数量统计)