Sentinel 集群限流深度解析(上):架构设计与配置实战

图片[1]-Sentinel 集群限流深度解析(上):架构设计与配置实战 - 速优课-速优课

本文导读

在前面的文章中,我们学习的都是单机限流。但在微服务架构下,一个服务通常会部署多个节点。由于负载均衡算法的不确定性,流量不可能均匀地分发到每个节点上,这就导致单机限流存在一个痛点:

总流量还没到阈值,有些节点就已经开始限流了。

举个例子:

  • 服务 A 部署了 3 个节点
  • 单机限流阈值配置为 200 QPS
  • 理想情况下集群总阈值是 600 QPS
  • 但实际可能某个节点先到 200 QPS 开始限流,而其他节点只有 100 QPS
  • 此时集群总 QPS 只有 400,远低于预期的 600

为了解决这个问题,Sentinel 从 1.4.0 版本开始引入了集群限流功能,可以精确控制整个集群的总 QPS。

本文作为集群限流系列的上篇,将从以下几个方面展开:

  1. 集群限流的整体架构:客户端、服务端、通信协议
  2. 服务端的两种部署模式:嵌入模式 vs 独立模式
  3. 集群限流规则配置:阈值类型、回退机制
  4. 客户端与服务端配置实战:从零开始搭建集群限流
  5. 动态配置与角色切换:如何动态调整节点角色

通过本文的学习,你将全面了解 Sentinel 集群限流的架构设计,并能够动手搭建一套完整的集群限流环境。


一、集群限流整体架构

1.1 为什么需要集群限流

在分布式环境下,单机限流存在以下问题:

问题 说明
流量不均 负载均衡无法保证绝对均匀,导致部分节点先限流
阈值不准 无法精确控制整个集群的总流量
资源浪费 有些节点限流了,有些节点还很空闲

集群限流的核心目标就是:精确控制整个集群的总 QPS,而不是每个节点各自为战。

1.2 模块划分

Sentinel 集群限流的代码在 sentinel-cluster 目录下,包含以下几个核心模块:

模块 作用
sentinel-cluster-common-default 公共模块,定义通信协议、编解码接口、请求响应实体
sentinel-cluster-client-default 客户端模块,基于 Netty 实现通信,支持自动重连
sentinel-cluster-server-default 服务端模块,基于 Netty 实现通信,提供 TokenService 扩展接口

设计亮点:

  • 通信协议与底层框架解耦:公共模块只定义接口,不依赖具体的通信框架
  • 目前默认实现基于 Netty,但理论上可以替换为其他通信框架

1.3 单机限流 vs 集群限流

我们先回顾一下单机限流的工作流程:

  1. FlowSlot 作为流量切入点,调用 FlowRuleChecker#checkFlow 判断是否限流
  2. FlowRuleChecker 根据资源名获取限流规则,遍历规则
  3. 根据规则的 clusterMode 决定走本地限流还是集群限流
  4. 本地限流则调用流量效果控制器判断是否拒绝

集群限流就是在第 3 步做文章:当 clusterMode = true 时,不是本地判断,而是向集群限流服务端发起远程调用,由服务端统一判断。

整体流程如下图所示:

图片[2]-Sentinel 集群限流深度解析(上):架构设计与配置实战 - 速优课-速优课

核心思想:

  • 客户端发起 Token 请求
  • 服务端统一计算,判断是否有剩余配额
  • 服务端返回结果(允许通过 / 拒绝)
  • 客户端根据结果决定是否放行

类比一下:单机限流就像每个人自己管自己的钱,花完了就不能花了;集群限流就像大家共用一个钱包,统一管理总预算。


二、集群限流服务端的两种模式

Sentinel 支持两种模式启动集群限流服务端:嵌入模式(Embedded)独立模式(Alone)

2.1 嵌入模式(Embedded)

嵌入模式下,集群限流服务端作为应用的内置服务,和应用在同一个进程中启动。可以动态挑选集群中的一个节点作为服务端。

图片[3]-Sentinel 集群限流深度解析(上):架构设计与配置实战 - 速优课-速优课

优点:

  • 无需单独部署,运维成本低
  • 可动态切换服务端节点

缺点:

  • 作为服务端的节点需要处理所有客户端的限流请求,会影响应用自身性能
  • 没有主从自动切换功能,服务端挂了就只能退回本地限流

适用场景:

  • 单个微服务内部实现集群限流
  • 对性能要求不高的场景
  • 服务节点数量不多的场景

2.2 独立模式(Alone)

独立模式下,集群限流服务端作为一个独立的应用部署,和业务应用完全隔离。

图片[4]-Sentinel 集群限流深度解析(上):架构设计与配置实战 - 速优课-速优课

优点:

  • 与业务应用隔离,不影响应用性能
  • 可以为所有服务提供统一的集群限流服务

缺点:

  • 需要单独部署,运维成本稍高
  • 需要考虑服务端的高可用

适用场景:

  • 多个服务都需要集群限流
  • 对性能要求较高的场景
  • 生产环境推荐使用

2.3 性能与可用性

性能方面:

  • 客户端与服务端之间保持一个长连接
  • 底层基于 Netty 实现,自定义通信协议
  • 数据包设计得足够小,网络 IO 影响很低
  • 服务端处理请求都是内存操作,计算量小,响应快

可以类比 Redis 的一次 hget 操作对应用性能的影响,集群限流的性能开销是类似的。

可用性方面:

  • Sentinel 集群限流对服务端可用性要求不高
  • 服务端挂掉时,客户端可以自动回退为本地限流
  • 嵌入模式没有主从自动切换,服务端挂了客户端不能自动选举新的服务端

选择哪种模式,更多是考虑:嵌入模式是否会严重影响应用性能,以及业务是否严重依赖集群限流的精确性。

2.4 Kubernetes 环境下的考量

如果服务部署在 Kubernetes 集群上,使用嵌入模式会有很多问题:

  • Pod IP 是动态变化的,静态配置服务端地址不现实
  • 节点挂了重启后 IP 会变,客户端需要重新配置
  • 半夜服务端节点挂了,新起的 Pod 是客户端角色,所有客户端都连不上
  • 只能退回本地限流,失去了集群限流的意义

在 Kubernetes 环境下,更推荐使用独立模式,或者基于第三方组件(如 Redis)实现集群限流。


三、集群限流规则详解

3.1 规则配置

集群限流规则本质上还是 FlowRule,只是多了集群相关的配置。当 FlowRuleclusterModetrue 时,这就是一个集群限流规则。

集群限流的具体配置在 clusterConfig 字段中,类型为 ClusterFlowConfig

public class ClusterFlowConfig {
    private Long flowId;
    private int thresholdType = ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL;
    private boolean fallbackToLocalWhenFail = true;
    // 当前版本未使用
    private int strategy = ClusterRuleConstant.FLOW_CLUSTER_STRATEGY_NORMAL;
    private int sampleCount = ClusterRuleConstant.DEFAULT_CLUSTER_SAMPLE_COUNT;
    private int windowIntervalMs = RuleConstant.DEFAULT_WINDOW_INTERVAL_MS;
}

各字段说明:

字段 说明
flowId 集群限流规则的全局唯一 ID
thresholdType 集群限流阈值类型(见下文)
fallbackToLocalWhenFail 失败时是否回退为本地限流,默认 true
sampleCount 滑动窗口的窗口数量
windowIntervalMs 滑动窗口的总时长(毫秒)

滑动窗口的参数和单机限流类似:windowIntervalMs / sampleCount = 每个小窗口的时间长度。

3.2 两种阈值类型

当规则配置为集群模式时,限流规则的 grade(阈值类型)不再使用,改用 ClusterFlowConfigthresholdType

支持两种阈值类型:

阈值类型 常量名 说明
单机均摊 FLOW_THRESHOLD_AVG_LOCAL 集群总阈值 = 客户端节点数 × 规则配置的 count
集群总阈值 FLOW_THRESHOLD_GLOBAL 规则配置的 count 就是整个集群的总阈值

举例说明:

假设规则配置的 count = 200,有 3 个客户端节点连接到服务端:

  • 单机均摊模式:集群总阈值 = 3 × 200 = 600 QPS
  • 集群总阈值模式:集群总阈值 = 200 QPS(所有节点加起来 200)

3.3 失败回退机制

fallbackToLocalWhenFail 参数控制当集群限流请求失败时的行为:

  • true:失败时回退为本地限流(默认值,推荐)
  • false:失败时直接放行(相当于没有限流,风险较高)

建议保持默认值 true,这样即使服务端挂了,也有单机限流作为兜底,保证系统安全。


四、集群限流的动态配置

4.1 规则配置的一致性

集群限流有一个重要原则:客户端和服务端必须使用同一份规则配置

  • 客户端需要规则来判断是否走集群限流模式
  • 服务端需要规则来计算和分配 Token
  • 两边规则不一致会导致各种奇怪的问题

为了保证一致性,最好的方式就是使用动态配置,让客户端和服务端从同一个数据源读取规则。

Sentinel 支持使用名称空间(namespace) 来隔离不同应用的集群限流规则。比如服务 A 和服务 B 的规则使用不同的 namespace 互相隔离。

4.2 简单动态数据源示例

为了便于理解和测试,我们来实现一个简单的动态数据源 SimpleLocalDataSource

它继承 AbstractDataSource,构造方法传入 namespace,用于指定只加载某个名称空间的规则。

public class SimpleLocalDataSource
        extends AbstractDataSource<String, List<FlowRule>> 
        implements Runnable {

    public SimpleLocalDataSource(String namespace) {
        super(new SimpleConverter<String, List<FlowRule>>() {});
        // 模拟延迟加载规则(等注册完成后再加载)
        new Thread(this).start();
    }

    @Override
    public String readSource() throws Exception {
        // 实际项目中从配置中心读取
        return "";
    }

    @Override
    public void close() throws Exception {
    }

    @Override
    public void run() {
        try {
            // 休眠 6 秒,等注册完成后再模拟加载一次规则
            Thread.sleep(6000);
            getProperty().updateValue(loadConfig());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Converter 中构造一个测试用的集群限流规则:

public class SimpleConverter implements Converter<String, List<FlowRule>> {
    @Override
    public List<FlowRule> convert(String source) {
        List<FlowRule> flowRules = new ArrayList<>();
        FlowRule flowRule = new FlowRule();
        flowRule.setCount(200);
        flowRule.setResource("GET:/hello");
        // 开启集群限流模式
        flowRule.setClusterMode(true);
        ClusterFlowConfig clusterFlowConfig = new ClusterFlowConfig();
        clusterFlowConfig.setFlowId(10000L); // 全局唯一 ID
        flowRule.setClusterConfig(clusterFlowConfig);
        flowRules.add(flowRule);
        return flowRules;
    }
}

注意:这只是测试用例。实际项目中,动态数据源可以通过定时拉取配置中心、或监听配置变更事件来实现。


五、集群限流客户端配置

5.1 添加依赖

在需要使用集群限流的微服务项目中添加客户端依赖:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-cluster-client-default</artifactId>
    <version>${sentinel.version}</version>
</dependency>

5.2 设置角色和基础配置

指定当前节点身份为集群限流客户端,并配置客户端参数:

@SpringBootApplication
public class WebMvcDemoApplication {
    static {
        // 指定当前身份为 Token Client(集群限流客户端)
        ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
        
        // 集群限流客户端配置
        ClusterClientConfig clientConfig = new ClusterClientConfig();
        clientConfig.setRequestTimeout(1000); // 请求超时时间 1 秒
        ClusterClientConfigManager.applyNewConfig(clientConfig);
    }
}

5.3 注册动态数据源

在 Spring 容器启动完成后,创建动态数据源并注册到 FlowRuleManager

@SpringBootApplication
public class WebMvcDemoApplication implements ApplicationListener<ContextRefreshedEvent> {
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 指定名称空间为 serviceA,只加载这个 namespace 下的规则
        SimpleLocalDataSource ruleSource = new SimpleLocalDataSource("serviceA");
        FlowRuleManager.register2Property(ruleSource.getProperty());
    }
}

5.4 配置服务端连接信息

最后,配置服务端的连接信息,让客户端知道去哪里连接服务端:

@SpringBootApplication
public class WebMvcDemoApplication {
    static {
        // 配置服务端地址
        ClusterClientAssignConfig assignConfig = new ClusterClientAssignConfig();
        assignConfig.setServerHost("127.0.0.1");
        assignConfig.setServerPort(11111);
        
        // 注册名称空间提供者(非常重要!)
        ConfigSupplierRegistry.setNamespaceSupplier(() -> "serviceA"); 
        
        // 应用配置,会触发连接建立
        ClusterClientConfigManager.applyNewAssignConfig(assignConfig);
    }
}

关键点解读:

  1. 调用 applyNewAssignConfig 会触发连接建立:不需要手动启动客户端,配置变更时会自动初始化或重新连接
  2. 支持自动重连:客户端与服务端意外断开时,会自动重试重连
  3. namespace 必须在连接前设置:客户端连上服务端时会立即发一个 PING 消息,namespace 就携带在 PING 数据包中,服务端以此识别每个客户端属于哪个应用

namespace 的作用:服务端根据 namespace 找到对应的规则配置,实现多应用共享一个集群限流服务端。


六、集群限流服务端配置

6.1 添加依赖

  • 嵌入模式:直接在微服务项目中添加服务端依赖
  • 独立模式:单独创建一个项目,添加服务端依赖
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-cluster-server-default</artifactId>
    <version>${sentinel.version}</version>
</dependency>

6.2 启动服务端(独立模式)

独立模式下,需要手动创建并启动 ClusterTokenServer

public class ClusterServerDemo {

    public static void main(String[] args) throws Exception {
        ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();
        
        // 配置服务端传输层参数
        ClusterServerConfigManager.loadGlobalTransportConfig(
            new ServerTransportConfig()
                .setIdleSeconds(600)    // 连接最大空闲时间 600 秒
                .setPort(11111)         // 监听端口 11111
        );
        
        // 启动服务
        tokenServer.start();
    }
}

6.3 注册规则动态数据源

服务端也需要加载限流规则,但方式和客户端不同。服务端通过 ClusterFlowRuleManager.setPropertySupplier 注册一个提供者函数:

ClusterFlowRuleManager.setPropertySupplier(new Function<String, SentinelProperty<List<FlowRule>>>() {
    // ClusterFlowRuleManager 会给 apply 方法返回的 SentinelProperty 注册监听器
    @Override
    public SentinelProperty<List<FlowRule>> apply(String namespace) {
        // 根据 namespace 创建对应的动态数据源
        SimpleLocalDataSource source = new SimpleLocalDataSource(namespace);
        // 返回数据源的 SentinelProperty
        return source.getProperty();
    }
});

这里注册的是一个 Java 8 的 Function 函数:

  • 输入:namespace(名称空间)
  • 输出:该 namespace 对应的 SentinelProperty

当注册 namespace 时,会触发 apply 方法创建对应的动态数据源。

6.4 注册名称空间

最后,为服务端注册名称空间,触发动态数据源的创建:

// 注册名称空间,多个应用对应多个名称空间
ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton("serviceA"));

工作流程:

  1. 注册 namespace “serviceA”
  2. 触发 setPropertySupplier 注册的 Function
  3. 创建 “serviceA” 对应的 SimpleLocalDataSource
  4. ClusterFlowRuleManager 给数据源的 SentinelProperty 注册监听器
  5. 延迟 6 秒后,SimpleLocalDataSource 加载规则并通知更新

如果有多个 namespace,就会多次调用 Function 的 apply 方法,为每个 namespace 创建独立的数据源。应用之间通过 namespace 完全隔离。


七、动态配置与角色切换

7.1 嵌入模式的痛点

如果使用嵌入模式,并且节点 IP 是动态变化的(比如 Kubernetes 环境),静态配置就行不通了。我们需要动态调整节点角色和连接配置。

Sentinel 提供了 HTTP API 来动态修改这些配置。

7.2 动态修改节点角色

HTTP API 格式:

http://<节点IP>:<节点Port>/setClusterMode?mode={state}

其中 state 的取值:

  • 0:集群限流客户端(CLUSTER_CLIENT)
  • 1:集群限流服务端(CLUSTER_SERVER)

当一个新节点被选为服务端后:

  1. 旧的服务端节点应该切换为客户端角色
  2. 其他所有客户端节点都需要更新连接配置,指向新的服务端

7.3 动态修改客户端配置

HTTP API 格式:

http://<节点IP>:<节点Port>/cluster/client/modifyConfig?data={body}

body 是 JSON 格式的字符串,支持的参数:

参数 说明
serverHost 集群限流服务端的 IP 地址
serverPort 集群限流服务端的端口
requestTimeout 请求超时时间

7.4 动态数据源方式

除了 HTTP API,Sentinel 还支持通过动态数据源方式修改配置。这也是更推荐的方式。

支持动态配置的项包括:

配置项 说明 动态变更效果
ClusterClientAssignConfig 客户端连接服务端的配置 重新创建客户端连接
ServerTransportConfig 服务端传输层配置(端口、空闲时间) 重启服务端
ClusterClientConfig 客户端配置(请求超时) 动态生效
ClusterState 节点状态(客户端/服务端) 切换角色,关闭旧连接/启动新服务

嵌入模式下角色切换的完整流程:

  1. 动态配置改变了某个节点的 ClusterState(从客户端变为服务端)
  2. 该节点关闭客户端连接,启动服务端监听
  3. 其他节点监听到配置变更,更新服务端地址
  4. 其他节点重新连接到新的服务端

虽然不能自动选举主节点,但 Sentinel 通过动态数据源 + 观察者模式提供了灵活的切换能力。你可以基于这个能力自己实现自动选举逻辑。


总结与思考

本文作为集群限流系列的上篇,详细介绍了 Sentinel 集群限流的架构设计、部署模式、规则配置和使用方式。

核心要点回顾

1. 架构设计

  • 客户端-服务端模式,基于 Netty 通信
  • 公共模块定义协议,与具体通信框架解耦
  • Token 请求模式:客户端申请 Token,服务端统一分配

2. 两种部署模式

模式 优点 缺点 适用场景
嵌入模式 无需单独部署 影响应用性能,无自动切换 单服务内、小规模
独立模式 性能隔离,统一服务 需要额外部署 多服务、生产环境

3. 两种阈值类型

  • 单机均摊:总阈值 = 节点数 × 单机阈值
  • 集群总阈值:配置值就是整个集群的总阈值

4. 配置三要素

  • 客户端需要:规则配置 + 服务端地址 + namespace
  • 服务端需要:规则配置 + namespace 注册 + 端口配置
  • 规则必须一致,推荐使用动态配置

5. 失败回退

  • 服务端挂了自动回退为本地限流(默认开启)
  • 保证系统可用性,不因为限流组件故障导致服务不可用

实践建议

  1. 生产环境推荐独立模式:避免影响业务应用性能
  2. 务必开启失败回退fallbackToLocalWhenFail = true
  3. 使用动态配置管理规则:保证客户端和服务端规则一致
  4. 合理设置超时时间:避免集群限流成为性能瓶颈
  5. K8s 环境谨慎使用嵌入模式:Pod 动态变化会导致配置管理困难

下篇文章我们将深入集群限流的源码,剖析客户端和服务端的实现细节,包括通信协议、请求处理流程、Token 分配算法等,敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞15
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!