头图

使用Sentinel集群限流的,如果使用嵌入模式,在异地多活专线抖动情况下会出现服务调用超时的情况,本文从限流概念和集群限流的实现方式出发整理了该知识点,特别是网络抖动情况下,对服务造成影响情况进行详细说明。

集群限流原理

Sentinel是一个系统性的高可用保障工具,提供了限流、降级、熔断等一系列的能力,基于这些能力做了语意化概念抽象,这些概念对于理解实现机制特别有帮助,所以这里也复述一下。

对于流量控制,有个一个模型:
image.png
流量控制有以下几个角度:

  • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
  • 运行指标,例如 QPS、线程池、系统负载等;
  • 控制的效果,例如直接限流、冷启动、排队等。

Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。

那么,集群限流只是控制限流里面的一个属性,在单机限流的基础上进行加强,对于需要精确控制qps阈值的场景特别适用。

言归正传,Sentinel的集群限流也没那么神秘,核心设计就是采用一个中心化的Token Server来分配令牌来实行,每次请求都会通过实例的Token Client请求Token Server获取token并调用。如下图所示:

image.png

注意这里Client和Server是通过tcp长连接的方式通信的,需要有reconnect的机制,比如这里Client连接不上Server,会等待 n * 2000ms 的方式一直尝试连接。

Sentinel 集群限流服务端有两种启动方式:

  • 嵌入模式(Embedded)适合应用级别的限流,部署简单,但对应用性能有影响;
  • 独立模式(Alone)适合全局限流,需要独立部署。

集群限流使用场景

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

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

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

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

场景三、机器配置不一样,不同实例需要根据整体qps和机器水位综合限流

这种场景比较特殊,一般业务上不会遇到,不做展开讨论。

集群限流可用性诊断

在生产实践配置集群限流过程中,如果遇到TokenClient和TokenServer网络抖动的情况下,会出现什么问题呢?情况一:网络问题;情况二:TokenServer内部异常;我们先看情况一。

TokenServer通信网络问题

假设是网络问题,那么不用去关注TokenServer返回的TokenResult具体信息了,只需要看下对超时的判断逻辑了。

FlowRuleChecker

private static boolean passClusterCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                        boolean prioritized) {
    try {
        TokenService clusterService = pickClusterService();
        if (clusterService == null) {
            return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
        }
        long flowId = rule.getClusterConfig().getFlowId();
        TokenResult result = clusterService.requestToken(flowId, acquireCount, prioritized);
        return applyTokenResult(result, rule, context, node, acquireCount, prioritized);
    } catch (Throwable ex) {
        RecordLog.warn("[FlowRuleChecker] Request cluster token unexpected failed", ex);
    }
    // 如果failback不可用直接paass
    return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
}

private static boolean applyTokenResult(/*@NonNull*/ TokenResult result, FlowRule rule, Context context,
                                                         DefaultNode node,
                                                         int acquireCount, boolean prioritized) {
        switch (result.getStatus()) {
            case TokenResultStatus.OK:
                return true;
            case TokenResultStatus.SHOULD_WAIT:
                // 没有达到采样数量,等待1000/SampleCount
                try {
                    Thread.sleep(result.getWaitInMs());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return true;
            case TokenResultStatus.NO_RULE_EXISTS:
            case TokenResultStatus.BAD_REQUEST:
            case TokenResultStatus.FAIL:
            case TokenResultStatus.TOO_MANY_REQUEST:
                return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
            case TokenResultStatus.BLOCKED:
            default:
                return false;
        }
    }

private static boolean fallbackToLocalOrPass(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                             boolean prioritized) {
    if (rule.getClusterConfig().isFallbackToLocalWhenFail()) {
        return passLocalCheck(rule, context, node, acquireCount, prioritized);
    } else {
        // 不是配置了降级到本地限流,就直接pass
        return true;
    }
}

DefaultClusterTokenClient

public TokenResult requestToken(Long flowId, int acquireCount, boolean prioritized) {
    if (notValidRequest(flowId, acquireCount)) {
        return badRequest();
    }
    FlowRequestData data = new FlowRequestData().setCount(acquireCount)
        .setFlowId(flowId).setPriority(prioritized);
    ClusterRequest<FlowRequestData> request = new ClusterRequest<>(ClusterConstants.MSG_TYPE_FLOW, data);
    try {
        TokenResult result = sendTokenRequest(request);
        logForResult(result);
        return result;
    } catch (Exception ex) {
        //异常情况下会直接返回TokenResultStatus.FAIL
        ClusterClientStatLogUtil.log(ex.getMessage());
        return new TokenResult(TokenResultStatus.FAIL);
    }
}

在requestToken超时失败后,会返回TokenResultStatus.FAIL。我们知道默认TokenServer请求的超时时间是20ms,那么网络出问题,此处会阻塞20ms,然后降级到单机限流规则限流,而且每次都会请求TokenServer拿token。

如果网络连接断了的情况呢?

NettyTransportClient

@Override
public ClusterResponse sendRequest(ClusterRequest request) throws Exception {
    //如果连接状态异常,则TokenClient端快速返回异常
    if (!isReady()) {
        throw new SentinelClusterException(ClusterErrorMessages.CLIENT_NOT_READY);
    }
  ...
}
@Override
public boolean isReady() {
    return channel != null && clientHandler != null && clientHandler.hasStarted();
}

分析可以看出来,第一种情况,如果网络异常情况,会直接返回异常,那么最外层还是会走到本机限流的逻辑,不会添加调用耗时。如果网络慢,则等待超时时间到达后返回。

如果是第二种情况TokenServer返回不正常状态,当TokenResultStatus.SHOULD_WAIT,则等待返回的时间,当TokenResultStatus.BLOCKED,直接返回false,被限流,当TokenResultStatus.TOO_MANY_REQUEST,也是降级到本机限流。

总结

在集群限流的时候,如果是嵌入模式TokenServer切换的瞬间不会造成访问报错,如果访问网络超时,但是TokenClient和TokenServer网络未中断,还是会等待访问结果,所以集群Server访问超时时间一定不要设置太长,比如设置成3s,那么在网络抖动情况下,每个请求都会加上这个3s,造成大量超时。

这里给出下,如何配置集群限流超时时间:

ClusterClientConfig clusterClientConfig = new ClusterClientConfig();
clusterClientConfig.setRequestTimeout(20);
ClusterClientConfigManager.applyNewConfig(clusterClientConfig);

(本文作者:朱云辉)

image.png

本文系哈啰技术团队出品,未经许可,不得进行商业性转载或者使用。非商业目的转载或使用本文内容,敬请注明“内容转载自哈啰技术团队”。

哈啰技术
89 声望51 粉丝

哈啰官方技术号,不定期分享哈啰的相关技术产出。