Sentinel架构篇 - 流量控制

流量控制

用于调整网络包发送的数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个适配器,可以根据需要将随机的请求调整成合适的形状,如下:

在这里插入图片描述

流量控制有两种统计类型 - 统计线程数、统计 QPS。可以通过如下命令查看实时统计信息:

curl http://localhost:8719/cnode?id=resourceName

输出内容的格式如下:

idx id   thread  pass  blocked   success  total Rt   1m-pass   1m-block   1m-all   exeption
2   abc647 0     46     0           46     46   1       2763      0         2763     0
  • thread: 代表当前处理该资源的线程数;
  • pass: 代表一秒内到来到的请求;
  • blocked: 代表一秒内被流量控制的请求数量;
  • success: 代表一秒内成功处理完的请求;
  • total: 代表到一秒内到来的请求以及被阻止的请求总和;
  • RT: 代表一秒内该资源的平均响应时间;
  • 1m-pass: 代表一分钟内到来的请求;
  • 1m-block: 代表一分钟内被阻止的请求;
  • 1m-all: 代表一分钟内到来的请求和被阻止的请求的总和;
  • exception: 代表一秒内业务本身异常的总和。

流量规则的定义

实际代码中对应 FlowRule。

同一个资源可以同时有多个流量规则。

Field 说明 默认值
resource 资源名称
count 限流阈值
grade 流量控制的指标(0 - 线程数、1 - QPS) 1 (即 QPS)
limitApp 流量控制针对的调用来源 default(不区分调用来源)
strategy 资源的调用关系(0 - 直接、1 - 关联、2 - 链路) 0(即直接)
controlBehavior 流量控制的效果(0 - 直接拒绝、1 - 冷启动、2 - 匀速器、3 - 冷启动 + 匀速器) 0(即直接拒绝)

流量控制的指标

基于并发线程数

简单统计当前请求上下文的线程个数,如果超过阈值,则新的请求会被拒绝。用于保护业务线程数不被耗尽。

比如,应用依赖的下游应用由于某种原因导致服务不稳定、响应时间增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下导致线程池耗尽。为应对高线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同的线程池来隔离业务之间的资源争抢(线程池隔离),或者使用信号量来控制同时请求的数量(信号量隔离)。信号量隔离的策略虽然能够控制线程数量,但是无法控制请求的排队时间。当请求过多时,排队也是无益的,直接拒绝能够迅速降低系统压力。Sentinel 线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程个数,如果超过阈值,新的请求会被立即拒绝。

流量控制效果只有直接拒绝。

[
    {
    
    
        "resource": "sayHello",
        "limitApp": "default",
        "grade": 0,
        "count": 4,
        "strategy": 0,
        "controlBehavior": 0
    }
]

基于QPS

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

流量控制的效果

直接拒绝

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

[
    {
    
    
        "resource": "sayHello",
        "limitApp": "default",
        "grade": 1,
        "count": 4,
        "strategy": 0,
        "controlBehavior": 0
    }
]

冷启动

对应 RuleConstant.CONTROL_BEHAVIOR_WARM_UP。该方式用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位,这种情况可能瞬间把系统压垮。冷启动方式让通过的流量缓慢增加,在一定时间内(warmUpPeriodSec 参数控制预热时长,单位秒)逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。

在这里插入图片描述

[
    {
    
    
        "resource": "sayHello",
        "limitApp": "default",
        "grade": 1,
        "count": 10,
        "strategy": 0,
        "controlBehavior": 1,
        "warmUpPeriodSec": 3
    }
]

匀速器

对应 RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER。严格控制请求通过的间隔时间,也就是让请求以均匀的速度通过,对应的是漏桶算法。这种方式主要用于处理间隔性突发的流量,比如消息队列。想象一下这种的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

在这里插入图片描述

[
    {
    
    
        "resource": "sayHello",
        "limitApp": "default",
        "grade": 1,
        "count": 10,
        "strategy": 0,
        "controlBehavior": 2,
        "maxQueueingTimeMs": 500
    }
]

每秒请求数大于 5 时,多余的请求排队等待。如果 500 毫秒内这些请求得不到处理,则请求会被限流导致访问失败。

冷启动+匀速器

对应 RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER。冷启动加匀速器组合的方式。

[
    {
    
    
        "resource": "sayHello",
        "limitApp": "default",
        "grade": 1,
        "count": 10,
        "strategy": 0,
        "controlBehavior": 3,
        "maxQueueingTimeMs": 500,
        "warmUpPeriodSec": 5
    }
]

资源的调用关系

调用关系包括调用方、被调用方;方法又可能会调用其它方法,形成一个调用链路的层次关系。

根据调用方限流

可以通过如下命令查看不同调用方对于同一个资源的调用数据。

curl http://localhost:8719/origin?id=nodeA

调用数据示例如下:

id: nodeA
idx origin  threadNum passedQps blockedQps totalQps aRt   1m-passed 1m-blocked 1m-total 
1   caller1 0         0         0          0        0     0         0          0
2   caller2 0         0         0          0        0     0         0          0

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

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

同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default

[
    {
    
    
        "resource": "sayHello",
        "limitApp": "spring-cloud-demo-consumer",
        "grade": 1,
        "count": 5,
        "controlBehavior": 2,
        "maxQueueingTimeMs": 500
    },
    {
    
    
        "resource": "sayHello",
        "limitApp": "other",
        "grade": 1,
        "count": 5,
        "controlBehavior": 2,
        "maxQueueingTimeMs": 500
    }
]

额外需要注册一个 RequestOriginParser 类型的 Bean。

@Component
public class MyRequestOriginParser implements RequestOriginParser {
    
    
    @Override
    public String parseOrigin(HttpServletRequest httpServletRequest) {
    
    
        String origin = httpServletRequest.getHeader("origin");
        if (StringUtils.isBlank(origin)) {
    
    
            origin = "default";
        }
        return origin;
    }
}

然后请求头携带 origin 属性以及属性值。

链路限流

一棵典型的调用树如下图所示:

     	          machine-root
                    /       \
                   /         \
             Entrance1     Entrance2
                /             \
               /               \
      DefaultNode(nodeA)   DefaultNode(nodeA)

上图中来自入口 Entrance1Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 FlowRule.strategyRuleConstant.CHAIN,同时设置 refResourceEntrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而对来自 Entrance2 的调用漠不关心。

[
    {
    
    
        "resource": "sayHello",
        "limitApp": "default",
        "strategy": 2,
        "refResource": "/hello/say",
        "grade": 1,
        "count": 3,
      	"controlBehavior": 2,
        "maxQueueingTimeMs": 500
    }
]

关联限流

当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。

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

[
    {
    
    
        "resource": "/hello/say",
        "limitApp": "default",
        "strategy": 1,
        "refResource": "/hello/say2",
        "grade": 1,
        "count": 1
    }
]

/hello/say2 接口访问达到限流阈值时(不对它限流),对 /hello/say 接口根据配置的限流手段进行限流。

FlowSlot

负责限流规则的判断。

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    
    
  	// 校验限流规则
    checkFlow(resourceWrapper, context, node, count, prioritized);
	// 交给下一个ProcessorSlot处理
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
    throws BlockException {
    
    
  	// 交给FlowRuleChecker继续处理
    checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

checkFlow

public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                      Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
    
    
    if (ruleProvider == null || resource == null) {
    
    
        return;
    }
    Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
    if (rules != null) {
    
    
      	// 只要有一个流量规则校验失败,则抛出FlowException异常
        for (FlowRule rule : rules) {
    
    
            if (!canPassCheck(rule, context, node, count, prioritized)) {
    
    
                throw new FlowException(rule.getLimitApp(), rule);
            }
        }
    }
}

canPassCheck

public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node,
                                                int acquireCount) {
    
    
    return canPassCheck(rule, context, node, acquireCount, false);
}

public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                                boolean prioritized) {
    
    
    String limitApp = rule.getLimitApp();
    if (limitApp == null) {
    
    
        return true;
    }
	// 如果是集群模式
    if (rule.isClusterMode()) {
    
    
      	// 进行集群模式下的流量规则校验(该部分在集群流控文章中进行分析)
        return passClusterCheck(rule, context, node, acquireCount, prioritized);
    }
	// 进行本地模式下的流量规则校验
    return passLocalCheck(rule, context, node, acquireCount, prioritized);
}

passLocalCheck

private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                      boolean prioritized) {
    
    
  	// 根据限流规则中的limitApp、strategy参数值,选择合适的节点(DefaultNode、ClusterNode)
    Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
  	// 如果选择的节点为空,则返回true,表示校验成功
    if (selectedNode == null) {
    
    
        return true;
    }
	// 根据具体的TrafficShapingController接口的实现类进行校验
    return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}

selectNodeByRequesterAndStrategy

static Node selectNodeByRequesterAndStrategy(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node) {
    
    
    String limitApp = rule.getLimitApp();
    int strategy = rule.getStrategy();
    String origin = context.getOrigin();
	// 如果限流规则中的limitApp参数值等于上下文记录的请求来源,并且上下文记录的请求来源不是DEFAULT、OTHER
    if (limitApp.equals(origin) && filterOrigin(origin)) {
    
    
      	// 如果调用关系是根据调用方限流
        if (strategy == RuleConstant.STRATEGY_DIRECT) {
    
    
          	// 返回上下文记录的起源节点
            return context.getOriginNode();
        }

        return selectReferenceNode(rule, context, node);
    // 如果限流规则中的limitApp参数值等于DEFAULT  
    } else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) {
    
    
      	// 如果调用关系是根据调用方限流
        if (strategy == RuleConstant.STRATEGY_DIRECT) {
    
    
          	// 返回对应的集群节点
            return node.getClusterNode();
        }

        return selectReferenceNode(rule, context, node);
    // 如果限流规则中的limitApp参数值等于OTHER
    } else if (RuleConstant.LIMIT_APP_OTHER.equals(limitApp)
        && FlowRuleManager.isOtherOrigin(origin, rule.getResource())) {
    
    
      	// 如果调用关系是根据调用方限流
        if (strategy == RuleConstant.STRATEGY_DIRECT) {
    
    
          	// 返回上下文记录的起源节点
            return context.getOriginNode();
        }

        return selectReferenceNode(rule, context, node);
    }
	// 其余情况返回空
    return null;
}

isOtherOrigin

public static boolean isOtherOrigin(String origin, String resourceName) {
    
    
    if (StringUtil.isEmpty(origin)) {
    
    
        return false;
    }

    List<FlowRule> rules = flowRules.get().get(resourceName);

    if (rules != null) {
    
    
        for (FlowRule rule : rules) {
    
    
            if (origin.equals(rule.getLimitApp())) {
    
    
                return false;
            }
        }
    }

    return true;
}

selectReferenceNode

static Node selectReferenceNode(FlowRule rule, Context context, DefaultNode node) {
    
    
  	// 获取流量控制规则的refResource参数值
    String refResource = rule.getRefResource();
  	// 获取流量控制规则的strategy参数值
    int strategy = rule.getStrategy();
	// 如果refResource参数值为空,则返回空
    if (StringUtil.isEmpty(refResource)) {
    
    
        return null;
    }
	// 如果是关联限流
    if (strategy == RuleConstant.STRATEGY_RELATE) {
    
    
      	// 获取refResource参数值对应的集群节点
        return ClusterBuilderSlot.getClusterNode(refResource);
    }
	// 如果是链路限流
    if (strategy == RuleConstant.STRATEGY_CHAIN) {
    
    
      	// 如果refResource参数值不等于上下文名称,则返回空
        if (!refResource.equals(context.getName())) {
    
    
            return null;
        }
      	// 返回DefaultNode
        return node;
    }
    // 返回空
    return null;
}

根据流量控制的效果,有四种与之对应的 TrafficShapingController 接口的实现类。

todo

TrafficShapingController 接口的实现类的解析。

猜你喜欢

转载自blog.csdn.net/qq_34561892/article/details/129673589