草庐IT

第二章 Sentine 核心工作原理

原水寒 2023-03-28 原文

源码分析

// 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 字段用于根据调用方进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:

  • default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。
  • {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者 caller1 的规则,那么当且仅当来自 caller1 对 NodeA 的请求才会触发流量控制。
  • other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。
    同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default

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


image.png

流控模式

  • 直接限流:直接对配置的资源进行限流。

  • 关联限流:当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 FlowRule.strategy 为 RuleConstant.RELATE 同时设置 FlowRule.ref_identity 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。


    image.png
  • 链路限流:假设来自入口 Entrance1 和 Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 FlowRule.strategy 为 RuleConstant.CHAIN,同时设置 FlowRule.ref_identity 为 Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而对来自 Entrance2 的调用漠不关心。调用链的入口是通过 API 方法 ContextUtil.enter(name) 定义的。假如当前是从 Entrance2 访问进来的,那么不做限流操作;假如当前访问是从 Entrance1 进来的,那么就要进行限流操作了

    image.png

流控效果

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

  • 直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式(计数器限流)。该方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。实际上,sentinel 还提供了一种“预占用”的机制,对于重要的访问,直接拒绝不一定合适,而这个访问又可以进行一定的等待后再返回,此时,如果当前 WindowWrapper 中的数据满了,可以占用下一个 WindowWrapper 中的名额(即当前访问线程sleep一定时间,到下一个WindowWrapper后,再进行业务逻辑处理)

  • 冷启动(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式。该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。冷启动的核心原理(令牌桶算法):核心就是控制每个时间窗口可以给出的令牌数(eg. 可以根据预热时长和QPS阈值来设定每个 WindowWrapper 的令牌数)。当 WindowWrapper 的QPS达到限流阈值时,触发冷启动,慢慢的pass更多请求,当pass的请求达到限流阈值时,冷启动结束,开始使用直接限流方式。

    image.png

  • 匀速器(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式。这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。匀速等待的核心原理(漏桶算法):根据QPS计算通过每两个请求的时间间隔(eg. QPS200,则每5ms通过一个请求,这个就是漏桶的固定速率)costTime,与上一个请求通过的时间 latestPassedTime(请求通过需要记录该值)相加可以计算出期待通过的时间,如果小于当前时间,直接通过。否则计算需要等待的时间,并且与配置的超时时间做比较,如果大于超时时间,直接拒绝;如果小于,则sleep一段时间(需等待时间)后,返回直接业务逻辑。(当然,实际上在并发中,latestPassedTime会被并发修改,所以需要考虑并发的问题,核心处理方式如下)

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 对这个问题采取了两种手段:

  • 通过并发线程数进行限制
    和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
  • 通过响应时间对资源进行降级
    除了对并发线程数进行控制以外,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流量数据统计入其中。

阈值类型

  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且 系统当前的并发线程数超过系统容量时(BBR) 才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。
  • CPU usage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

Load1 限流与 BBR 拥塞控制算法

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

  • load 是一个结果,如果根据 load 的情况来调节流量的通过率,那么就始终有延迟性。也就意味着通过率的任何调整,都会过一段时间才能看到效果。如果当前设置的阈值是使 load 恶化的一个动作,那么也至少要过 1 秒之后才能观测到(定时任务每秒获取一次 load1 和 cpu 使用率);同理,如果当前阈值调整是让 load 好转的一个动作,也需要 1 秒之后才能继续调整,这样就浪费了系统的处理能力。所以我们看到的曲线,总是会有抖动。
  • load1 恢复慢。想象一下这样的一个场景,出现了这样一个问题,下游应用不可靠,导致应用 RT 很高,从而 load 到了一个很高的点。过了一段时间之后下游应用恢复了,应用 RT 也相应减少。这个时候,其实应该大幅度增大流量的通过率;但是由于这个时候 load 仍然很高,通过率的恢复仍然不高。

解决方案:BBR


image.png

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

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

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

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

有关第二章 Sentine 核心工作原理的更多相关文章

  1. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  2. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  3. ruby - 无法让 RSpec 工作—— 'require' : cannot load such file - 2

    我花了三天的时间用头撞墙,试图弄清楚为什么简单的“rake”不能通过我的规范文件。如果您遇到这种情况:任何文件夹路径中都不要有空格!。严重地。事实上,从现在开始,您命名的任何内容都没有空格。这是我的控制台输出:(在/Users/*****/Desktop/LearningRuby/learn_ruby)$rake/Users/*******/Desktop/LearningRuby/learn_ruby/00_hello/hello_spec.rb:116:in`require':cannotloadsuchfile--hello(LoadError) 最佳

  4. ruby-on-rails - rspec should have_select ('cars' , :options => ['volvo' , 'saab' ] 不工作 - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion在首页我有:汽车:VolvoSaabMercedesAudistatic_pages_spec.rb中的测试代码:it"shouldhavetherightselect"dovisithome_pathit{shouldhave_select('cars',:options=>['volvo','saab','mercedes','audi'])}end响应是rspec./spec/request

  5. ruby-on-rails - s3_direct_upload 在生产服务器中不工作 - 2

    在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo

  6. ruby - JetBrains RubyMine 3.2.4 调试器不工作 - 2

    使用Ruby1.9.2运行IDE提示说需要gemruby​​-debug-base19x并提供安装它。但是,在尝试安装它时会显示消息Failedtoinstallgems.Followinggemswerenotinstalled:C:/ProgramFiles(x86)/JetBrains/RubyMine3.2.4/rb/gems/ruby-debug-base19x-0.11.30.pre2.gem:Errorinstallingruby-debug-base19x-0.11.30.pre2.gem:The'linecache19'nativegemrequiresinstall

  7. ruby - `rescue $!` 是如何工作的? - 2

    我知道全局变量$!包含最新的异常对象,但我对下面的语法感到困惑。谁能帮助我理解以下语法?rescue$! 最佳答案 此构造可防止异常停止您的程序并使堆栈跟踪冒泡。它还会将该异常作为值返回,这很有用。a=get_me_datarescue$!在此行之后,a将保存请求的数据或异常。然后您可以分析该异常并采取相应措施。defget_me_dataraise'Nodataforyou'enda=get_me_datarescue$!puts"Executioncarrieson"pa#>>Executioncarrieson#>>#更现实的

  8. ruby - File.read ("| echo mystring") 是如何工作的? - 2

    我在我正在处理的一些代码中发现了这一点。它旨在解决从磁盘读取key文件的要求。在生产环境中,key文件的内容位于环境变量中。旧代码:key=File.read('path/to/key.pem')新代码:key=File.read('|echo$KEY_VARIABLE')这是如何工作的? 最佳答案 来自IOdocs:Astringstartingwith“|”indicatesasubprocess.Theremainderofthestringfollowingthe“|”isinvokedasaprocesswithappro

  9. ruby - 这个 ruby​​ 注入(inject)魔术是如何工作的? - 2

    我今天看到了一个ruby​​代码片段。[1,2,3,4,5,6,7].inject(:+)=>28[1,2,3,4,5,6,7].inject(:*)=>5040这里的注入(inject)和之前看到的完全不一样,比如[1,2,3,4,5,6,7].inject{|sum,x|sum+x}请解释一下它是如何工作的? 最佳答案 没有魔法,符号(方法)只是可能的参数之一。这是来自文档:#enum.inject(initial,sym)=>obj#enum.inject(sym)=>obj#enum.inject(initial){|mem

  10. ruby-on-rails - attr_accessible 在 Rails 中如何工作? - 2

    我刚刚有一个关于RubyonRails和模型(Rails3)中的attr_accessible属性的一般性问题。有人可以解释应该在那里定义哪些模型属性吗?我记得一些关于批量分配风险的事情,虽然我在这方面不太了解......谢谢:) 最佳答案 想象一个带有一些字段的订单类:Order.new({:type=>'Corn',:quantity=>6})现在假设订单也有折扣代码,比如:price_off。您不想将:price_off标记为attr_accessible。这会阻止恶意代码制作最终会执行如下操作的帖子:Order.new({:

随机推荐