写点什么

Sentinel

作者:苏格拉格拉
  • 2022-11-11
    浙江
  • 本文字数:7083 字

    阅读完需:约 23 分钟

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。

一、Sentinel 功能

1.流量控制

流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。


Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图:

2.熔断降级

除了流量控制以外,及时对调用链路中的不稳定因素进行熔断也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,可能会导致请求发生堆积,进而导致级联错误。



Sentinel 和 Hystrix 的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,如让请求快速失败,避免影响到其它的资源而导致级联故障。

二、QuickStart

STEP 1. 在应用中引入 Sentinel Jar 包

如果应用使用 pom 工程,则在  pom.xml  文件中加入以下代码即可:

    <dependency>      <groupId>com.alibaba.csp</groupId>      <artifactId>sentinel-core</artifactId>      <version>1.8.5</version>    </dependency>
复制代码


注意: Sentinel 仅支持 JDK 1.8 或者以上版本。

STEP 2. 定义资源

接下来,我们把需要控制流量的代码用 Sentinel API  SphU.entry("HelloWorld")  和  entry.exit()  包围起来即可。在下面的例子中,我们将  System.out.println("hello world");  这端代码作为资源,用 API 包围起来(埋点)。参考代码如下:


public static void main(String[] args) {	initFlowRules();	while (true) {		Entry entry = null;		try {       entry = SphU.entry("HelloWorld");			/*您的业务逻辑 - 开始*/      System.out.println("hello world");      /*您的业务逻辑 - 结束*/		} catch (BlockException e1) {       	/*流控逻辑处理 - 开始*/       System.out.println("block!");       /*流控逻辑处理 - 结束*/		} finally {      if (entry != null) {          entry.exit();      }		}	}}
复制代码


完成以上两步后,代码端的改造就完成了。当然,也支持注解,可以以低侵入性的方式定义资源。

STEP 3. 定义规则

接下来,通过规则来指定允许该资源通过的请求次数,例如下面的代码定义了资源  HelloWorld  每秒最多只能通过 20 个请求。

    private static void initFlowRules(){      List<FlowRule> rules = new ArrayList<>();      FlowRule rule = new FlowRule();      rule.setResource("HelloWorld");      rule.setGrade(RuleConstant.FLOW_GRADE_QPS);      // Set limit QPS to 20.      rule.setCount(20);      rules.add(rule);      FlowRuleManager.loadRules(rules);    }
复制代码


完成上面 3 步,Sentinel 就能够正常工作了。

STEP 4. 检查效果

Demo 运行之后,我们可以在日志  ~/logs/csp/${appName}-metrics.log.xxx  里看到下面的输出:

    |--timestamp-|------date time----|-resource-|p |block|s |e|rt    1529998904000|2018-06-26 15:41:44|HelloWorld|20|0    |20|0|0    1529998905000|2018-06-26 15:41:45|HelloWorld|20|5579 |20|0|728    1529998906000|2018-06-26 15:41:46|HelloWorld|20|15698|20|0|0    1529998907000|2018-06-26 15:41:47|HelloWorld|20|19262|20|0|0    1529998908000|2018-06-26 15:41:48|HelloWorld|20|19502|20|0|0    1529998909000|2018-06-26 15:41:49|HelloWorld|20|18386|20|0|0
复制代码


其中  p  代表通过的请求,  block  代表被阻止的请求,  s  代表成功执行完成的请求个数,  e  代表用户自定义的异常,  rt  代表平均响应时长。可以看到,这个程序每秒稳定输出 "hello world" 20 次,和规则中预先设定的阈值是一样的。

STEP 5. 启动 Sentinel 控制台

您可以参考 Sentinel 控制台文档 启动控制台,可以实时监控各个资源的运行情况,并且可以实时地修改限流规则。


三、工作原理

1.概述

在 Sentinel 里面,所有的资源都对应一个资源名称( resourceName ),每次资源调用都会创建一个  Entry  对象。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:

  • NodeSelectorSlot  负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;

  • ClusterBuilderSlot  则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;

  • StatisticSlot  则用于记录、统计不同纬度的 runtime 指标监控信息;

  • FlowSlot  则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;

  • AuthoritySlot  则根据配置的黑白名单和调用来源信息,来做黑白名单控制;

  • DegradeSlot  则通过统计信息以及预设的规则,来做熔断降级;

  • SystemSlot  则通过系统的状态,例如 load1 等,来控制总的入口流量;总体的框架如下:


2.代码层面的解读

SphU.entry()

Sph 的默认实现类是 CtSphSphU.entry() 方法最终会执行到 entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException 这个方法。

    public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {        Context context = ContextUtil.getContext();        if (context instanceof NullContext) {            // Init the entry only. No rule checking will occur.            return new CtEntry(resourceWrapper, null, context);        }            if (context == null) {            context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());        }            // Global switch is close, no rule checking will do.        if (!Constants.ON) {            return new CtEntry(resourceWrapper, null, context);        }            // 获取该资源对应的SlotChain        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);            /*         * Means processor cache size exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE}, so no         * rule checking will be done.         */        if (chain == null) {            return new CtEntry(resourceWrapper, null, context);        }            Entry e = new CtEntry(resourceWrapper, chain, context);        try {          // 执行Slot的entry方法            chain.entry(context, resourceWrapper, null, count, args);        } catch (BlockException e1) {            e.exit(count, args);            // 抛出BlockExecption            throw e1;        } catch (Throwable e1) {            RecordLog.info("Sentinel unexpected exception", e1);        }        return e;    }
复制代码


这个方法可以分为以下几个部分:

1.获取上下文

2.根据包装过的资源对象获取对应的 SlotChain

3.执行 SlotChain 的 entry 方法

3.1.如果 SlotChain 的 entry 方法抛出了 BlockException,则将该异常继续向上抛出

3.2.如果 SlotChain 的 entry 方法正常执行了,则最后会将该 entry 对象返回

如果上层方法捕获了 BlockException,则说明请求被限流了,否则请求能正常执行

创建 SlotChain

lookProcessChain方法最终走到 Env.slotsChainbuilder.build()


    public ProcessorSlotChain build() {        ProcessorSlotChain chain = new DefaultProcessorSlotChain();        chain.addLast(new NodeSelectorSlot());        chain.addLast(new ClusterBuilderSlot());        chain.addLast(new LogSlot());        chain.addLast(new StatisticSlot());        chain.addLast(new SystemSlot());        chain.addLast(new AuthoritySlot());        chain.addLast(new FlowSlot());        chain.addLast(new DegradeSlot());            return chain;    }
复制代码


将所有的节点都加入到ProcessorSlotChain链表中后,整个链表的结构变成了如下图所示:



知道了 SlotChain 是如何创建的了,那接下来就要看下是如何执行 Slot 的 entry 方法的了。

执行 SlotChain 的 entry 方法


最终的执行效果是:会依次调用 Slot 的 entry 方法

执行 Slot 的 entry 方法

SlotChain 中的第一个节点 NodeSelectorSlot 的 entry 方法中去了,具体的代码如下:

    @Override    public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)        throws Throwable {                DefaultNode node = map.get(context.getName());        if (node == null) {            synchronized (this) {                node = map.get(context.getName());                if (node == null) {                    node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);                    HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());                    cacheMap.putAll(map);                    cacheMap.put(context.getName(), node);                    map = cacheMap;                }                // Build invocation tree                ((DefaultNode)context.getLastNode()).addChild(node);            }        }            context.setCurNode(node);        // 由此触发下一个节点的entry方法        fireEntry(context, resourceWrapper, node, count, args);    }
复制代码


从代码中可以看到,NodeSelectorSlot 节点做了一些自己的业务逻辑处理。各个 Slot 的职责在前面概述也有所介绍。

四、流量控制(限流)

1.概述

流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

FlowSlot  会根据预设的规则,结合前面  NodeSelectorSlotClusterBuilderSlotStatisticSlot  统计出来的实时信息进行流量控制。

限流的直接表现是在执行  Entry nodeA = SphU.entry(resourceName)  的时候抛出  FlowException  异常。 FlowException  是  BlockException  的子类,您可以捕捉  BlockException  来自定义被限流之后的处理逻辑。

同一个资源可以创建多条限流规则。 FlowSlot  会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource :资源名,即限流规则的作用对象

  • count : 限流阈值

  • grade : 限流阈值类型(QPS 或并发线程数)

  • limitApp : 流控针对的调用来源,若为  default  则不区分调用来源

  • strategy : 调用关系限流策略

  • controlBehavior : 流量控制效果(直接拒绝、Warm Up、匀速排队)

2.流量控制

流量控制主要有两种统计类型,一种是统计并发线程数,另外一种则是统计 QPS。类型由  FlowRule  的  grade  字段来定义。其中,0 代表根据并发数量来限流,1 代表根据 QPS 来进行流量控制。其中线程数、QPS 值,都是由  StatisticSlot  实时统计获取的。

2.1.并发线程数控制

并发数控制用于保护业务线程池不被慢调用耗尽。

Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。

并发数控制通常在调用端进行配置。

2.2.QPS 流量控制

当 QPS 超过某个阈值的时候,则采取措施进行流量控制。

流量控制的效果包括以下几种:直接拒绝Warm Up匀速排队。对应  FlowRule  中的  controlBehavior  字段。


注意:若使用除了直接拒绝之外的流量控制效果,则调用关系限流策略(strategy)会被忽略。

控制效果:

  • 直接拒绝( RuleConstant.CONTROL_BEHAVIOR_DEFAULT )方式是默认的流量控制方式,当 QPS 超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出 FlowException 。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。

  • Warm Up( RuleConstant.CONTROL_BEHAVIOR_WARM_UP )方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加给冷系统一个预热的时间。



  • 匀速排队( RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER )方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。



这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

2.3.基于调用关系的流量控制

调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel 通过  NodeSelectorSlot  建立不同资源间的调用的关系,并且通过  ClusterBuilderSlot  记录每个资源的实时统计信息。

有了调用链路的统计信息,我们可以衍生出多种流量控制手段。如根据调用方限流、根据调用链路入口限流、关联流量控制。

3.集群流控

集群限流使用场景

场景一、qps 量小比机器数还少

假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。

场景二、机器弹性伸缩、数目变化频繁

假设一个服务访问量呈锯齿状,开启了弹性伸缩,机器数目不透明,那么这时候通过预估单实例 qps * 机器数的方式计算就会不准确.

五、熔断降级

功能

原理

六、限流算法

1.计数器限流算法

我们可以直接通过一个计数器,限制每一秒钟能够接收的请求数。比如说 qps 定为 1000,那么实现思路就是从第一个请求进来开始计时,在接下去的 1s 内,每来一个请求,就把计数加 1,如果累加的数字达到了 1000,那么后续的请求就会被全部拒绝。等到 1s 结束后,把计数恢复成 0 ,重新开始计数。



优点:实现简单

缺点:如果 1s 内的前半秒,已经通过了 1000 个请求,那后面的半秒只能请求拒绝,我们把这种现象称为“突刺现象”。

2.滑动时间窗算法


为了解决计数器法统计精度太低的问题,引入了滑动窗口算法。滑动时间窗其实就是把计数器限流算法的时间窗口再做进一步划分,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

假设某个服务最多只能每秒钟处理 100 个请求,可以设置一个 1 秒钟的滑动时间窗口,该窗口分为 10 个格子,每个格子 100 毫秒;

每 100 毫秒移动一次,每次移动都需要记录当前服务 100ms 内请求的次数 counter 到格子中,counter 的值是累计请求的值,不会被重置;

如果格子数大于 10 个,删除最前边的格子,始终保留 10 个;

用最后一个格子的 counter 值减去最前边格子的 counter 值,如果大于限流请求数,则会被限流。

3.漏桶算法


首先,需要有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。

将算法中的水换成实际应用中的请求,就可以看到漏桶算法天生就限制了请求的速度。 当使用了漏桶算法,可以保证接口会以一个常速速率来处理请求。所以漏桶算法天生不会出现临界问题。

4.令牌桶算法


首先,我们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的,token 以 一个固定的速率 r 往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话,请求无法通过。


漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。 因为默认的令牌桶算法,取走 token 是不需要耗费时间的,也就是说,假设桶内有 100 个 token 时,那么可以瞬间允许 100 个请求通过。

限流算法总结

计数器 VS 时间窗

时间窗算法的本质也是通过计数器算法实现的。

时间窗算法格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确,但是也会占用更多的内存存储。

漏桶 VS 令牌桶

漏桶算法和令牌桶算法本质上是为了做流量整形或速率限制,避免系统因为大流量而被打崩,但是两者的核心差异在于限流的方向是相反的

  • 漏桶:限制的是流量的流出速率,是相对固定的。

  • 令牌桶 : 限制的是流量的平均流入速率,并且允许一定程度的突然性流量,最大速率为桶的容量和生成 token 的速率。


在某些场景中,漏桶算法并不能有效的使用网络资源,因为漏桶的漏出速率是相对固定的,所以在网络情况比较好并且没有拥塞的状态下,漏桶依然是会有限制的,并不能放开量,因此并不能有效的利用网络资源。而令牌桶算法则不同,其在限制平均速率的同时,支持一定程度的突发流量。


参考

https://juejin.cn/post/6844903758388805645

https://juejin.cn/post/6965406931066290206

发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2018-08-22 加入

还未添加个人简介

评论

发布
暂无评论
Sentinel_分布式_苏格拉格拉_InfoQ写作社区