// 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 的统计结果,做出相应的流控逻辑

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



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


以下内容摘抄自 sentinel官网
限流规则中的 limitApp 字段用于根据调用方进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:
举个使用案例,假设调用关系如下:被调用方的入口 IN 流控设置了200QPS(不区分调用来源),调用来源1和调用来源2均没有设置流控规则,那么假设调用来源1是一个集聚流量,马上打满了200qps,那么整个调用来源2就无法请求成功了。为了避免这种情况,可以使用 {some_origin_name} 为调用来源1配置一条规则,然后再设置一条 other 规则为其他调用源进行设置,而二者相加的阈值就是被调方的入口接口阈值。

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

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

当 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的请求达到限流阈值时,冷启动结束,开始使用直接限流方式。

匀速器(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;


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

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

应用级别的入口流量 进行控制,从单台机器的总体 Load1(1min内的 load 值)、CPU利用率、RT、入口 QPS 和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。sentinel 提供了一个全局的IN流量的统计节点ClusterNode(total_inbound_traffic),在 StatisticSlot 统计信息时,会将IN流量数据统计入其中。
系统当前的并发线程数超过系统容量时(BBR) 才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。为什么不用 Load1 的阈值直接限流?
解决方案:BBR

我们把系统处理请求的过程想象为一个水管,到来的请求是往这个水管灌水,当系统处理顺畅的时候,请求不需要排队,直接从水管中穿过,这个请求的RT是最短的;反之,当请求堆积的时候,那么处理请求的时间则会变为:排队时间 + 最短处理时间。
推论一: 如果我们能够保证水管里的水量,能够让水顺畅的流动,则不会增加排队的请求;也就是说,这个时候的系统负载不会进一步恶化。
我们用T 来表示(水管内部的水量),用RT来表示请求的处理时间,用P来表示进来的请求数,那么一个请求从进入水管道到从水管出来,这个水管会存在P * RT个请求。换一句话来说,当 T ≈ QPS * Avg(RT) 的时候,我们可以认为系统的处理能力和允许进入的请求个数达到了平衡,系统的负载不会进一步恶化。
接下来的问题是,水管的水位是可以达到了一个平衡点,但是这个平衡点只能保证水管的水位不再继续增高,但是还面临一个问题,就是在达到平衡点之前,这个水管里已经堆积了多少水。如果之前水管的水已经在一个量级了,那么这个时候系统允许通过的水量可能只能缓慢通过,RT会大,之前堆积在水管里的水会滞留;反之,如果之前的水管水位偏低,那么又会浪费了系统的处理能力。
推论二: 当保持入口的流量是水管出来的流量的最大的值的时候,可以最大利用水管的处理能力。
然而,和 TCP BBR 的不一样的地方在于,还需要用一个系统负载的值(load1)来激发这套机制启动。
我在从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""-
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我花了三天的时间用头撞墙,试图弄清楚为什么简单的“rake”不能通过我的规范文件。如果您遇到这种情况:任何文件夹路径中都不要有空格!。严重地。事实上,从现在开始,您命名的任何内容都没有空格。这是我的控制台输出:(在/Users/*****/Desktop/LearningRuby/learn_ruby)$rake/Users/*******/Desktop/LearningRuby/learn_ruby/00_hello/hello_spec.rb:116:in`require':cannotloadsuchfile--hello(LoadError) 最佳
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion在首页我有:汽车:VolvoSaabMercedesAudistatic_pages_spec.rb中的测试代码:it"shouldhavetherightselect"dovisithome_pathit{shouldhave_select('cars',:options=>['volvo','saab','mercedes','audi'])}end响应是rspec./spec/request
在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
使用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
我知道全局变量$!包含最新的异常对象,但我对下面的语法感到困惑。谁能帮助我理解以下语法?rescue$! 最佳答案 此构造可防止异常停止您的程序并使堆栈跟踪冒泡。它还会将该异常作为值返回,这很有用。a=get_me_datarescue$!在此行之后,a将保存请求的数据或异常。然后您可以分析该异常并采取相应措施。defget_me_dataraise'Nodataforyou'enda=get_me_datarescue$!puts"Executioncarrieson"pa#>>Executioncarrieson#>>#更现实的
我在我正在处理的一些代码中发现了这一点。它旨在解决从磁盘读取key文件的要求。在生产环境中,key文件的内容位于环境变量中。旧代码:key=File.read('path/to/key.pem')新代码:key=File.read('|echo$KEY_VARIABLE')这是如何工作的? 最佳答案 来自IOdocs:Astringstartingwith“|”indicatesasubprocess.Theremainderofthestringfollowingthe“|”isinvokedasaprocesswithappro
我今天看到了一个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
我刚刚有一个关于RubyonRails和模型(Rails3)中的attr_accessible属性的一般性问题。有人可以解释应该在那里定义哪些模型属性吗?我记得一些关于批量分配风险的事情,虽然我在这方面不太了解......谢谢:) 最佳答案 想象一个带有一些字段的订单类:Order.new({:type=>'Corn',:quantity=>6})现在假设订单也有折扣代码,比如:price_off。您不想将:price_off标记为attr_accessible。这会阻止恶意代码制作最终会执行如下操作的帖子:Order.new({: