一些收藏

第二章 Sentine 核心工作原理

2022-06-04  本文已影响0人  原水寒

源码分析

// 1. 为调用链入口创建Context + 为调用链入口创建入口节点(EntranceNode实例)
ContextUtil.enter("myEntrance", "myOrigin-main");
Entry entry = null;
try {
    // 2. 包装资源为ResourceWrapper + 为资源创建执行链(ProcessorSlotChain)
    // + 为资源创建CtEntry并赋值给当前调用链Context.curEntry + 执行ProcessorSlotChain实现统计限流等逻辑
    entry = SphU.entry("myResource1");
    System.out.println(new HelloSentinel().sayHello("baby"));
} catch (BlockException ex) {
    // 资源访问阻止,被限流或被降级
    System.out.println("blocked");
} catch (Exception ex) {
    // 若需要配置降级规则,需要通过这种方式记录业务异常
    Tracer.traceEntry(ex, entry);
    System.out.println("Tracer.traceEntry");
} finally {
    if (entry != null) {
        // 3. 调用ProcessorSlotChain.exit
        entry.exit();
    }
    // 4. 清除上下文
    ContextUtil.exit();
}
1.为调用链入口创建Context + 为调用链入口创建入口节点(EntranceNode实例)
ContextUtil.enter(String name, String origin)
--> 从上下文获取Context,若有,直接返回,若无,进行创建
// 包装资源 + 为调用链创建入口节点EntranceNode
--> EntranceNode node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null)
// 将入口节点加入根节点ROOT的childList中
--> Constants.ROOT.addChild(node)
// 创建Context,设置到上下文中
--> Context context = new Context(node, name)  context.setOrigin(origin)
--> ThreadLocal<Context> contextHolder.set(context)

2.包装资源为ResourceWrapper + 为资源创建执行链(ProcessorSlotChain)+ 执行ProcessorSlotChain实现统计限流等逻辑
SphU.entry
--> Sph.entry(String name, EntryType type, int batchCount, Object... args)
  --> CtSph.entryWithPriority(new StringResourceWrapper(name, type), int count, boolean prioritized, Object... args)
    // 先从Map<ResourceWrapper, ProcessorSlotChain>找是否资源对应的chain(每个资源对应一个chain),没有则创建
    --> ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper)
      --> new DefaultProcessorSlotChain() + SPI 加载 Slot + 组装成链
    --> Entry e = new CtEntry(resourceWrapper, chain, context)
    // 依次执行调用链中的slot
    --> chain.entry(context, resourceWrapper, null, count, prioritized, args)

3.调用ProcessorSlotChain.exit
entry.exit()
--> chain.exit(context, resourceWrapper, count, args)
--> 执行默认调用链上下文(如果不自己指定调用链入口上下文,则会自动创建)自动清除逻辑

4.清除当前调用链的上下文(如果不自己指定调用链入口上下文,则会在 entry.exit() 自动清除)
ContextUtil.exit()

可以看出,实际上 sentinel 的核心原理就是:为每个资源创建一条链,链上包含一系列的 slot,这些 slot 分两部分,前一部分 slot 用于做各种统计,后一部分 slot 基于前一部分 slot 的统计结果,做出相应的流控逻辑

slot 链

image.png 图片来自sentinel官网
从上图可以看出 slot 链中的 slot 分两部分,前一部分 slot 用于做各种统计,后一部分 slot 基于前一部分 slot 的统计结果,做出相应的流控逻辑。

数据统计

sentinel 的数据统计是基于 Node 结构来做,首先看下四种 Node 的类结构。

image.png
Node 相关介绍在 官网 有详细介绍。之后,来看下整个slot链中的三个数据统计相关的slot:NodeSelectorSlot/ClusterBuilderSlot/StatisticSlot 的核心作用以及滑动时间窗口(窗口可循环复用)的核心设计。
image.png

限流

image.png

在 FlowSlot 执行限流逻辑时,会根据来源(limitApp)和流控模式(strategy)选择相关的统计 Node 节点,之后再使用该 Node 节点 + 流控效果(controlBehavior)+ 限流阈值类型(QPS/并发线程数)执行限流操作。限流设计如下,后续的流控设计基本类似。


image.png

Entry 类型

image.png

以下内容摘抄自 sentinel官网

针对来源限流

限流规则中的 limitApp 字段用于根据调用方进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:

举个使用案例,假设调用关系如下:被调用方的入口 IN 流控设置了200QPS(不区分调用来源),调用来源1和调用来源2均没有设置流控规则,那么假设调用来源1是一个集聚流量,马上打满了200qps,那么整个调用来源2就无法请求成功了。为了避免这种情况,可以使用 {some_origin_name} 为调用来源1配置一条规则,然后再设置一条 other 规则为其他调用源进行设置,而二者相加的阈值就是被调方的入口接口阈值。


image.png

流控模式

流控效果

当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的手段包括下面 3 种,对应 FlowRule 中的 controlBehavior 字段:

long oldTime = latestPassedTime.addAndGet(costTime);
waitTime = oldTime - TimeUtil.currentTimeMillis();
// 可能被并发修改,将latestPassedTime值恢复到从前,本次请求直接拒绝
if (waitTime > maxQueueingTimeMs) {
    latestPassedTime.addAndGet(-costTime);
    return false;
}
// in race condition waitTime may <= 0
if (waitTime > 0) {
    Thread.sleep(waitTime);
}
return true;
image.png

熔断降级

image.png
在1000ms内,至少有100个请求,且有(100*0.1)个请求的RT达到300ms,则进行熔断。等待2s后,开始尝试通过请求(半开),如果ok,则关闭熔断,恢复业务请求;如果不OK,则继续熔断,等待下一次的半开周期。
在 sentinel 中,熔断从半开到关闭的逻辑是在 DegradeSlot.exit 中做的(此时,业务逻辑执行完成了,可以根据其结果来处理熔断开关的状态)。
在 sentinel 中,熔断降级是通过熔断器机制来做的,慢调用熔断器和异常熔断器都有自己的滑动时间窗口(LeapArray)来进行熔断指标的单独统计。
对于异常熔断来讲,其仅关注业务异常,我们需要在程序中显示调用 Tracer.traceEntry(ex, entry) 来统计异常。
异常熔断器
public void onRequestComplete(Context context) {
    Entry entry = context.getCurEntry();
    if (entry == null) {
        return;
    }
    // 通过Tracer.traceEntry(ex, entry)写入到Entry.error中
    Throwable error = entry.getError();
    SimpleErrorCounter counter = stat.currentWindow().value();
    if (error != null) {
        counter.getErrorCount().add(1);
    }
    counter.getTotalCount().add(1);

    handleStateChangeWhenThresholdExceeded(error);
}

与 Hystrix 的对比,摘抄自 官网
Hystrix 通过 线程池隔离 的方式,来对依赖(在 Sentinel 的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本(过多的线程池导致线程数目过多),当业务调用资源时,需要将自身线程切换到给资源分配的线程池中的线程,还需要预先给各个资源做线程池大小的分配。(实际上,Hystrix 也支持信号量隔离,Hystrix 官方推荐线程池隔离方式)

Sentinel 对这个问题采取了两种手段:

授权流控

image.png

通过看程序传入的 Context.origin 是否在配置的流控应用(limitApp)中,再根据授权类型(白名单/黑名单)来判断是否可以需要流控。这里可以根据想要控制的目标来灵活的设计 origin。

自适应限流(系统保护规则)

image.png
以下内容摘抄自 官网
系统保护规则是从 应用级别的入口流量 进行控制,从单台机器的总体 Load1(1min内的 load 值)、CPU利用率、RT、入口 QPS 和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。sentinel 提供了一个全局的IN流量的统计节点ClusterNode(total_inbound_traffic),在 StatisticSlot 统计信息时,会将IN流量数据统计入其中。

阈值类型

Load1 限流与 BBR 拥塞控制算法

为什么不用 Load1 的阈值直接限流?

解决方案:BBR


image.png

我们把系统处理请求的过程想象为一个水管,到来的请求是往这个水管灌水,当系统处理顺畅的时候,请求不需要排队,直接从水管中穿过,这个请求的RT是最短的;反之,当请求堆积的时候,那么处理请求的时间则会变为:排队时间 + 最短处理时间。

推论一: 如果我们能够保证水管里的水量,能够让水顺畅的流动,则不会增加排队的请求;也就是说,这个时候的系统负载不会进一步恶化。
我们用T 来表示(水管内部的水量),用RT来表示请求的处理时间,用P来表示进来的请求数,那么一个请求从进入水管道到从水管出来,这个水管会存在P * RT个请求。换一句话来说,当 T ≈ QPS * Avg(RT) 的时候,我们可以认为系统的处理能力和允许进入的请求个数达到了平衡,系统的负载不会进一步恶化。

接下来的问题是,水管的水位是可以达到了一个平衡点,但是这个平衡点只能保证水管的水位不再继续增高,但是还面临一个问题,就是在达到平衡点之前,这个水管里已经堆积了多少水。如果之前水管的水已经在一个量级了,那么这个时候系统允许通过的水量可能只能缓慢通过,RT会大,之前堆积在水管里的水会滞留;反之,如果之前的水管水位偏低,那么又会浪费了系统的处理能力。

推论二: 当保持入口的流量是水管出来的流量的最大的值的时候,可以最大利用水管的处理能力。
然而,和 TCP BBR 的不一样的地方在于,还需要用一个系统负载的值(load1)来激发这套机制启动。

上一篇 下一篇

猜你喜欢

热点阅读