一、Sentinel 集群

为什么要使用集群流控呢?假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控的方式。
另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10QPS,理想情况下整个集群的限流阈值就为 100QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。
——Sentinel 官方文档

Sentinel 的集群和其他常见的集群为了解决的问题不一样,像是常见的 Redis 集群、MySQL 集群、Nacos 集群等,这些部署一主多从的集群,目的是为了高可用,即保证主节点挂了的情况下,仍然可以提供服务;而 Sentinel 集群限流中的集群,指的不是对Sentinel Dashboard 做集群部署,而是对分布式部署的资源(或者说服务)做统筹的管理,它是对集群做限流的管理,而不是把自己部署成限流

二、Sentinel 集群架构

  • Token Client: 集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • Token Server: 即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。
    Sentinel 集群架构图

这里需要明确一个非常重要的点:Token Client 自己管理限流规则,限流规则是自己设置的,可以写在代码中,也可以从 Nacos 等配置中心动态的获取,先会判断这个限流规则是否是集群模式,如果不是,就自己处理;如果是,就向 Token Server 请求token。Token Server 的作用只是校验这次请求是否允许通过,允许的话给就颁发token给Client。这样的话Token Server 如何感知限流规则呢,既然 Client 中有限流规则,是在请求token时把集群限流规则发给Server吗?不是这样的,真正的做法是让Server本地也存储同样一份一模一样的集群限流规则,Client与Token之间传递的参数,只有一个集群限流规则的flowId和申请的count数。源码讲解详见后文。

2.1 Token Server

  • 独立模式(Alone),即作为独立的 token server 进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。
    独立模式
    在独立模式下,直接创建对应的 ClusterTokenServer 实例并通过 start 方法启动 Token Server
  • 嵌入模式(Embedded),即Client(或者说应用实例) 作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。
    嵌入模式
    在嵌入模式下,可以通过http://<ip>:<port>/setClusterMode?mode=<xxx>转换集群流控身份,其中 mode 为 0 代表 client,1 代表 server,-1 代表关闭。注意应用端需要引入集群限流客户端或服务端的相应依赖

2.2 Token Client

Token Client 就是应用实例,如果在启动时未进行配置,则需要对客户端进行基本的配置,比如指定集群限流 token server等,可通过http://<ip>:<port>/cluster/client/modifyConfig?data=<config>进行配置,其中:data 是 JSON 格式的 ClusterClientConfig,对应的配置项:

  • serverHost: token server host
  • serverPort: token server 端口
  • requestTimeout: 请求的超时时间(默认为 20 ms)

当然也可以在启动时进行配置,详情请见后文。

三、Sentinel 集群限流中 namespace 源码级详解【本文的核心与关键】

Sentinel支持使用名称空间 (namespace)区分不同应用之间的集群限流规则配置,如服务A的集群限流规则配置和服务B的集群限流规则配置可以使用名称空间隔离。

  1. 对于服务端而言,namespace设置可以通过 ClusterServerConfigManager.loadServerNamespaceSet(Set<String> namespaceSet); 进行设置。
  2. 但是对于客户端而言,namespace是什么呢?为什么没有配置过呢?如何进行配置呢?
    查看com.alibaba.csp.sentinel.cluster.registry.ConfigSupplierRegistry源码可以得到,ConfigSupplierRegistry 中定义了namespaceSupplier,而默认的 DEFAULT_APP_NAME_SUPPLIER 定义为:
    private static final Supplier<String> DEFAULT_APP_NAME_SUPPLIER = new Supplier<String>() {
    	public String get() {
        	return AppNameUtil.getAppName();
    	}
    };
    
    可以看到,默认的 namespace 是应用名称,我们可以通过ConfigSupplierRegistry.setNamespaceSupplier(Supplier<String> namespaceSupplier); 进行自定义。
  3. 集群限流规则需要在集群限流客户端配置一份,同时集群限流服务端也需要配置一份,缺一不可,否则集群限流不能实现
    看到这儿可能会有一个疑问,在讲 namespace,为什么突然开始说限流规则的配置了呢?这是因为Token Client 向 Token server的请求体定义为:
    package com.alibaba.csp.sentinel.cluster.request.data;
    
    public class FlowRequestData {
        private long flowId;
        private int count;
        private boolean priority;
    }
    
    第一步 客户端连接服务端后,客户端会向服务端通过 Netty 发送 Ping,服务端收到消息后会将客户端的 namespace 注册到 com.alibaba.csp.sentinel.cluster.server.connection.ConnectionManager 中,源码如下:
    // 客户端类路径:com.alibaba.csp.sentinel.cluster.client.handler.TokenClientHandler
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        this.currentState.set(2);
        this.fireClientPing(ctx);
        RecordLog.info("[TokenClientHandler] Client handler active, remote address: {}", new Object[]{this.getRemoteAddress(ctx)});
    }
    
    private void fireClientPing(ChannelHandlerContext ctx) {
        ClusterRequest<String> ping = (new ClusterRequest()).setId(0).setType(0).setData((String)ConfigSupplierRegistry.getNamespaceSupplier().get());
        ctx.writeAndFlush(ping);
    }
    
    // 服务端类路径:package com.alibaba.csp.sentinel.cluster.server.handler.TokenServerHandler
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        this.globalConnectionPool.refreshLastReadTime(ctx.channel());
        if (msg instanceof ClusterRequest) {
            ClusterRequest request = (ClusterRequest)msg;
            if (request.getType() == 0) {
                this.handlePingRequest(ctx, request);
                return;
            }
    
            RequestProcessor<?, ?> processor = RequestProcessorProvider.getProcessor(request.getType());
            if (processor == null) {
                RecordLog.warn("[TokenServerHandler] No processor for request type: " + request.getType(), new Object[0]);
                this.writeBadResponse(ctx, request);
            } else {
                ClusterResponse<?> response = processor.processRequest(request);
                this.writeResponse(ctx, response);
            }
        }
    
    }
    
    private void handlePingRequest(ChannelHandlerContext ctx, ClusterRequest request) {
        if (request.getData() != null && !StringUtil.isBlank((String)request.getData())) {
            String namespace = (String)request.getData();
            String clientAddress = this.getRemoteAddress(ctx);
            int curCount = ConnectionManager.addConnection(namespace, clientAddress).getConnectedCount();
            int status = 0;
            ClusterResponse<Integer> response = new ClusterResponse(request.getId(), request.getType(), status, curCount);
            this.writeResponse(ctx, response);
        } else {
            this.writeBadResponse(ctx, request);
        }
    }			
    
    第二步 流量到达客户端,此时客户端向服务端发送申请token的请求,此时请求中只包含 集群限流规则的flowId需要申请的count数 ,客户端收到请求后,会在 ClusterServerConfigManager 中通过 flowId 查找 namespace此处的namespace 是在服务端注册的 ),再在ClusterFlowRuleManager 通过 namespace 查找集群限流规则,通过 GlobalRequestLimiter 进行 token 分发( GlobalRequestLimiter 通过 ClusterServerConfigManager 的监听事件进行初始化),源码如下:
    // com.alibaba.csp.sentinel.cluster.flow.DefaultTokenService
    public TokenResult requestToken(Long ruleId, int acquireCount, boolean prioritized) {
        if (this.notValidRequest(ruleId, acquireCount)) {
            return this.badRequest();
        } else {
            FlowRule rule = ClusterFlowRuleManager.getFlowRuleById(ruleId);
            return rule == null ? new TokenResult(3) : ClusterFlowChecker.acquireClusterToken(rule, acquireCount, prioritized);
        }
    }
    
    // com.alibaba.csp.sentinel.cluster.flow.ClusterFlowChecker
    static boolean allowProceed(long flowId) {
        String namespace = ClusterFlowRuleManager.getNamespace(flowId);
        return GlobalRequestLimiter.tryPass(namespace);
    }
    
    static TokenResult acquireClusterToken(FlowRule rule, int acquireCount, boolean prioritized) {
        Long id = rule.getClusterConfig().getFlowId();
        if (!allowProceed(id)) {
            return new TokenResult(-2);
        } else {
            ClusterMetric metric = ClusterMetricStatistics.getMetric(id);
            if (metric == null) {
                return new TokenResult(-1);
            } else {
                double latestQps = metric.getAvg(ClusterFlowEvent.PASS);
                double globalThreshold = calcGlobalThreshold(rule) * ClusterServerConfigManager.getExceedCount();
                double nextRemaining = globalThreshold - latestQps - (double)acquireCount;
                if (nextRemaining >= (double)0.0F) {
                    metric.add(ClusterFlowEvent.PASS, (long)acquireCount);
                    metric.add(ClusterFlowEvent.PASS_REQUEST, 1L);
                    if (prioritized) {
                        metric.add(ClusterFlowEvent.OCCUPIED_PASS, (long)acquireCount);
                    }
    
                    return (new TokenResult(0)).setRemaining((int)nextRemaining).setWaitInMs(0);
                } else {
                    if (prioritized) {
                        double occupyAvg = metric.getAvg(ClusterFlowEvent.WAITING);
                        if (occupyAvg <= ClusterServerConfigManager.getMaxOccupyRatio() * globalThreshold) {
                            int waitInMs = metric.tryOccupyNext(ClusterFlowEvent.PASS, acquireCount, globalThreshold);
                            if (waitInMs > 0) {
                                ClusterServerStatLogUtil.log("flow|waiting|" + id);
                                return (new TokenResult(2)).setRemaining(0).setWaitInMs(waitInMs);
                            }
                        }
                    }
    
                    metric.add(ClusterFlowEvent.BLOCK, (long)acquireCount);
                    metric.add(ClusterFlowEvent.BLOCK_REQUEST, 1L);
                    ClusterServerStatLogUtil.log("flow|block|" + id, acquireCount);
                    ClusterServerStatLogUtil.log("flow|block_request|" + id, 1);
                    if (prioritized) {
                        metric.add(ClusterFlowEvent.OCCUPIED_BLOCK, (long)acquireCount);
                        ClusterServerStatLogUtil.log("flow|occupied_block|" + id, 1);
                    }
    
                    return blockedResult();
                }
            }
        }
    }	
    
    所以这块儿有个BUG(可能也不叫BUG):客户端A自定义了namespaceappB 时,从Nacos动态获取到了 flowId10001 的集群限流规则,但是在服务端注册 namespace 时,appA 对应的 flowId 应该是 10000appB 对应的 flowId10001,恰好 appAappB 的某个资源名又是相同的,此时就会打破不同应用之间的隔离。

四、SpringBoot + Nacos 实现Sentinel 集群限流

本Demo实现了通过Nacos动态获取集群限流规则,接入Sentinel Dashboard等,基于 Alibaba Sentinel 的集群流控,包含集群 Token Server、Token Client Starter、Token Client Demo、Nacos 配置与 jMeter 压测脚本等模块,方便本地演示与二次扩展。

4.1 环境

  • JDK : 21
  • SpringBoot : 3.3.12
  • Nacos : 2022.0.0.0
  • Sentinel : 1.8.8

4.2 项目结构

sentinel-cluster
    ├── jMeter-script                      		# jMeter 脚本与测试相关文件
    │   └── 集群限流测试.jmx                 	# 示例压测脚本(访问客户端 /hello 接口)
    ├── nacos-config                       		# Nacos 配置(如导出 zip 包)
    │   └── nacos_config.zip               		# Sentinel 相关 DataId / Group 配置打包
    ├── sentinel-cluster-client            		# Sentinel 集群客户端父工程(Maven 聚合)
    │   ├── sentinel-cluster-client-starter    	# Spring Boot Starter 与自动装配
    │   ├── sentinel-cluster-client-demo-A     	# 客户端 Demo A 示例服务(/hello)
    │   └── sentinel-cluster-client-demo-B     	# 客户端 Demo B 示例服务(/hello)
    └── sentinel-cluster-server            		# Sentinel 集群 Token Server 示例服务

4.3 项目地址

sentinel-cluster:https://github.com/M-soldier/sentinel-cluster

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐