从单机限流在流量不均匀场景下经常误伤这点切入,把集群限流的必要性讲透,再逐步展开 Token Server 的部署难点和高可用方案。这篇我会结合实际踩坑经历,讲一下 Token Server 独立部署、Nacos 配置同步、以及客户端报错排查的完整链路。
1. 为什么单机限流不够用:流量不均带来的“误伤”
先说一个很多团队都会遇到的问题:明明配置了限流规则,压测和线上表现却完全不是一回事。我们曾经有个订单查询服务,单机 QPS 上限设了 500,总共 5 台机器,理论上集群容量是 2500 QPS。但实际情况是,流量分布根本没有我们想的那么均匀,高峰期某一台机器的 QPS 可能直接冲到 1200,另外几台还在 200 到 300 徘徊。结果就是那台机器疯狂触发限流,大量请求被拒绝,用户看到的就是接口时不时报错,但我们集群整体的 QPS 其实还远没到瓶颈。
这就是单机限流的死穴:限流规则只对单台机器生效,和集群真实的容量评估存在偏差。你按单机能力设置阈值,流量一旦倾斜,好机器闲着,忙机器被打爆,限流策略形同虚设。如果你为了照顾单机峰值把阈值调大,那遇到流量均匀的时候,整体超限又没法感知,风险更不可控。
集群限流要解决的就是这个问题。核心思路很简单:把原本分散在各台机器本地内存里的流量统计,收敛到一个统一的计数中心去,所有请求到达业务机器之后,先去这个计数中心“报个数”,确认没有超过集群总阈值,再放行。这样一来,不管流量怎么倾斜,只要整个集群的总 QPS 超过预设值,就会被拦住,而不是等着某一台机器被单独打爆。
说到这儿就要引出 Sentinel 集群限流里的两个角色:Token Server 和 Token Client。Token Client 内嵌在业务应用里,负责采集请求并把令牌申请请求通过网络发给 Token Server;Token Server 是独立的进程或嵌入在某台机器里,它维护着全局的计数器,根据规则决定给不给令牌。整体的请求链路大概是这样:业务请求进来 -> Token Client 向 Token Server 申请令牌 -> Token Server 根据集群统计结果返回放行或拒绝 -> 业务拿到结果后继续或降级。
这个模型的好处是计数逻辑集中可管理,缺点也立刻暴露出来——Token Server 成了整个限流体系里的单点。它一旦挂了或者网络不通,要么所有请求因为没有拿到令牌被熔断降级,要么 Token Client 进入了 fallback 逻辑直接放行导致限流形同虚设。无论哪种,都是生产事故。所以标题里的“高可用部署方案”才是真正的重头戏,后面我详细拆。
2. Token Server 高可用的真正难点:计数器在内存里
我在跟很多同行聊集群限流的时候发现一个共同误区:大家以为高可用就是多部署几台 Token Server,前面再加个负载均衡就完事了。实际上如果这么简单,Sentinel 官方文档里早就给出完整集群方案了。真正的难点在于:Sentinel Cluster Token Server 的流控计数器是完完全全存在内存里的,它不是无状态服务,你跟它谈简单横向扩容,是谈不动的。
2.1 为什么说 Token Server 是有状态服务
Sentinel 集群限流默认的计数实现是 AtomicInteger 之类的内存变量。每个滑动窗口的计数、每个资源的限流状态、每一条 Rule 的统计结果,都在这台 Token Server 的 JVM 堆内存里。如果我有两台 Token Server,一台统计到 QPS 800,另一台统计到 QPS 500,而我们的集群总阈值是 1000,请问到底听谁的?没有一台全局的协调者来合并双方的计数,这种“双主”状态就是对限流逻辑的根本性破坏。
所以 Sentinel 官方根本没有提供类似 Consul、Etcd 那种基于 Raft 协议的集群方案,它只把 Token Server 划分成两种部署角色:嵌入式模式是挂在某台业务机器上,独立模式是单独起的进程。但无论哪种方式,同一时刻对于同一组资源,真正有效处理令牌申请的只能有一个逻辑计数中心。如果你硬要在两台机器上同时起两个 Token Server,并且让不同业务机器各连各的,那本质上就是两套独立的限流系统,不能叫“集群限流”。
2.2 高可用不等于并行扩展,而是主备切换
这里必须把概念掰清楚。Token Server 高可用部署的合理形态,不是多活并行,而是“主备 + 快速切换”。主节点负责维护实时计数器和处理令牌请求,备节点保持存活、维持规则同步和心跳监测,一旦主节点不可用,流量立刻切换到备节点。
这个方案看起来像“降级”,因为备节点在切换前并没有实时统计能力,切换后计数器从零开始,会有一个精度漂移的窗口期。但考虑到限流本身是对突发流量的保护,短时间内的计数重置对大多数业务场景是可以接受的。你需要做的是在切换完成后,让规则以最快速度在备节点上生效。结合 Nacos 做规则动态推送,这个收敛时间可以被压缩到秒级甚至毫秒级,完全可以接受。
这里我必须说一句:如果业务对限流的精度要求苛刻到连切换那几秒的计数偏差都不能容忍,那你应该考虑外部化存储来实现集群限流,比如用 Redis 做计数、用 Sentinel 的扩展接口去适配,但这已经偏离了 Sentinel 开箱即用的玩法,属于定制开发范畴。我先说这个,是因为你不能指望官方方案解决 100% 场景。
3. 独立模式 + 负载均衡 + Nacos 的落地部署
明确了“主备 + 切换”的思路,接下来就是具体怎么落。我这个方案选择了独立模式部署 Token Server,搭配负载均衡的四层转发来屏蔽后端节点变化,再通过 Nacos 做配置下发的自动化。整套架构部署下来,业务无感知,运维可掌控,扩展性上也留了余地。
3.1 架构与组件清单
先说清楚整套方案里都有什么角色,每个角色干什么活了,再画一下拓扑关系。
| 组件 | 角色 | 说明 |
|---|---|---|
| Token Server A | 主节点 | 独立进程,运行 Sentinel 集群 Token Server,维护实时计数器 |
| Token Server B | 备节点 | 独立进程,与 A 保持规则同步,随时准备接管流量 |
| 负载均衡 | 流量的统一入口 | 四层 TCP 转发,把 11111 端口流量分发给后端 Token Server,开启健康检查 |
| 业务应用 | Token Client 角色 | 内嵌 Sentinel 集群客户端,通过负载均衡 VIP 访问 Token Server |
| Nacos | 规则配置中心 | 存储流控规则,动态推送给 Token Server 和业务应用 |
Token Server A 和 B 之间的漂移逻辑,是利用负载均衡的健康检查实现的。默认流量全部转发到主节点;主节点返回不正常时,负载均衡自动把它摘除,后续的新连接全部打到备节点上。备节点从“冷备”变为“热备”,开始处理令牌申请。
独立模式的启动方式比嵌入式要清爽很多,你是单独起一个 Spring Boot 工程也好,还是用 java -jar 起一个普通 jar 包也好,关键在于设置一个系统参数:
-Dcsp.sentinel.cluster.server.port=11111这个参数指定了 Token Server 监听客户端连接请求的端口。同时独立模式下还会占用一个 HTTP 端口用于 Sentinel 控制台通信,通常我们在独立模式下再指定:
-Dserver.port=7777如果是在 Kubernetes 环境部署,要特别注意 Service 的网络模型。Token Server 对客户端提供服务走的是 11111 TCP 端口,控制台探测走 7777 HTTP 端口,两个端口的健康检查探针要分开处理。很多人只配了一个 HTTP 存活探针,结果 11111 端口实际已经挂了但探针检测不到,服务一直不摘除,客户端全部卡死。
3.2 服务端部署步骤
我以 Spring Boot 工程为例,跑通一个最小可用的独立 Token Server。工程里只引入 Sentinel 集群相关的几个核心依赖:
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-cluster-server-default</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-transport-simple-http</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> </dependency>启动类里最关键的一步是手动把 Token Server 处于运行状态。Sentinel 集群模块默认是不会自动把服务端角色拉起来的,必须调用启动函数:
@SpringBootApplication public class TokenServerApplication { public static void main(String[] args) { SpringApplication.run(TokenServerApplication.class, args); // 启动集群 Token Server ClusterServerStateManager.applyState(ClusterServerStateManager.SERVER_STATE_STARTED); } }我没有把启动逻辑放在 CommandLineRunner 里,而是放在 main 方法中直接触发,原因是为了保证 Spring 上下文完全就绪后再启动 Token Server,避免出现端口已经监听但规则还没加载的中间状态。
然后在 Nacos 配置中心准备集群 Token Server 的全局配置。这里需要创建 namespace、group、dataId 三个维度都正确的配置文件,我在第 4 节专门讲,因为这里翻车的人实在太多了。规则配置准备好之后,还需要一个监听 Nacos 配置变化的代码段,把动态下发的规则实时注册到 Sentinel 的规则管理器中:
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>( "限流配置的namespace", "DEFAULT_GROUP", "限流规则dataId", source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}) ); FlowRuleManager.register2Property(flowRuleDataSource.getProperty());这一段代码的作用,就是把 Nacos 上配置的流控规则文件,实时同步到 Token Server 内存里。监听到文件变化后,Sentinel 内部会自行更新 Rule 管理器中的内容,不需要改代码、不需要重启。主备两台 Token Server 都要配这套监听逻辑,这样才能保证备节点被切换上位时,规则是最新状态。
3.3 客户端接入配置
业务应用作为 Token Client,接入的关键是让它的令牌申请请求能稳定到达 Token Server。在 Sentinel 的客户端上,你需要让集群限流模块处于客户端模式,同时给它指定一个可以到达负载均衡 VIP 的访问配置。
客户端 yml 里或者启动参数里有几个关键配置,我直接列出来:
# 集群客户端模式 csp.sentinel.cluster.client.assign-type=static # 指向负载均衡地址,而不是直接指向某一个 Token Server 节点 IP csp.sentinel.cluster.client.server-host=你的负载均衡VIP地址 csp.sentinel.cluster.client.server-port=11111 # 连接 Token Server 的请求超时时间,默认给个 200ms 比较稳妥 csp.sentinel.cluster.client.request-timeout=200这里有一个非常容易被忽略的点:我见过很多团队直接把 server-host 配成了主 Token Server 的 IP。这种配置工作正常的前提是主节点永远在线。一旦主节点宕机,客户端不会自动感知后端有一个备节点可用,因为客户端这边压根不知道备节点的存在,它只会拼命重连那个已经不通的 IP。每次重连失败的等待时间,就是一批请求被降级或者报错的时间。
所以墙裂建议把 VIP 作为客户端唯一的访问入口。负载均衡背后挂了几个 Token Server、哪个是主、哪个是备,这个信息对客户端完全透明。客户端只跟 VIP 建立长连接,TCP 四层转发会把连接调度到当前健康的后端节点上。这样主备切换是运维层面的事,业务应用无感。
4. 与 Nacos 集成的规则同步细节
如果你对 Sentinel 和 Nacos 的集成不太熟,这一节建议反复看,因为真的太多人在 Nacos 的 namespace、group、dataId 三个维度上踩坑。之前有网友提到“sentinel 限流配置 + nacos 样例”,核心诉求就是想要一套可以直接抄的完整配置例子。我这里给出我自己线上跑通的方案。
4.1 完整可复用的规则同步配置
先说命名空间设计。我在 Nacos 里专门为 Sentinel 建了一个独立的 namespace,比如叫 sentinel-config。这样做的好处是把限流规则和业务配置隔离,权限管理也能单独控制。
然后创建两个 dataId,分别对应流控规则和集群配置:
namespace: sentinel-config group: DEFAULT_GROUP dataId: sentinel-app-flow-rules规则文件的内容格式是这样的,我以一条集群流控规则为例:
[ { "resource": "order:queryOrderInfo", "grade": 1, "count": 5000, "clusterMode": true, "clusterConfig": { "flowId": 1001, "thresholdType": 1, "fallbackToLocalWhenFail": true, "sampleCount": 10, "windowIntervalMs": 1000 } } ]这里面几个关键字段必须解释清楚:
clusterMode设为 true,表示这是集群限流规则,不设这个字段客户端和服务器都不知道要走集群模式;thresholdType是 0 还是 1 决定了规则是单机均摊还是集群总阈值,一般我们设 1,代表聚合到 Token Server 的全局计数;fallbackToLocalWhenFail这个字段尤其要留意,它决定了当 Token Server 无法连通时,是放行到本地单机限流兜底,还是直接拒绝。考虑到业务可用性优先的原则,我通常把它设为 true,请求会被本地规则接住,而不是一窝蜂被拒,虽然没有全局限制那么精准,但至少不会因为限流组件自身的故障拖垮所有业务。
这条规则文件通过 Nacos 发布后,第 3 节里写的 NacosDataSource 监听代码就会自动感知,完成推送。业务侧和 Token Server 侧监听的 dataId 保持统一,保证规则一致。
4.2 namespace、group、dataId 不一致的典型翻车现场
我第一次搭这套环境时也翻车了。当时我发现规则只推送到了业务应用,Token Server 压根没收到,日志里也没有任何 Nacos 配置更改的记录。后来反复排查,发现一个问题:业务应用读取配置时用的是 Nacos 默认 namespace,Token Server 的客户端配置文件里指定了namespace=sentinel-config,二者各监听各的,自然没法互通。Nacos 的 namespace 一旦填错或没填,客户端就会连到一个完全不同的命名空间,不会自动融合,更不会有任何报错提示。
还有 group 的问题。Nacos 默认的 group 是 DEFAULT_GROUP,Sentinel 控制台生成的一些规则可能塞在 SENTINEL_GROUP 里。如果在客户端代码里没显式指定 group,它默认走的是 DEFAULT_GROUP,那你去监听一个 SENTINEL_GROUP 下的 dataId,永远都拿不到数据。
所以我的建议是:不管是在代码里还是在 Nacos 控制台上,把 namespace、group、dataId 的取值都显式写出来、写成常量,别依赖默认值。好记性不如烂笔头,这些隐性默认值是配置事故的高发源头。
5. token exchange failed 类报错的排查链路
有网友反馈过“登录失败:login server error: token exchange failed: token endpoint returned”这样的报错。这个错误字符串在网络上经常出现在 OAuth、网关登录鉴权场景,但在集群限流体系里,它对应的本质问题和 Login 无关,而是 Token Client 与 Token Server 之间的通信握手、令牌申请这一环出了问题。你抓住“token exchange failed”这个短语,结合上下文去理解,就知道是客户端在向令牌服务端要令牌时失败了。
5.1 报错现象与常见误导
我第一次遇到这类报错时,当时的反应是查 Nacos、查规则推送,全都没问题。后来发现掉进了惯性思维的坑——因为报错信息里有 “login” 和 “token” 这种词汇,人本能会往认证授权方向查,绕了一大圈才意识到问题出在 Sentinel 集群通信层。
在 Sentinel 的日志体系里,Token Client 申请令牌失败时,会输出类似这样的日志:
[SentinelClusterClient] Request token failed, server: xxx.xxx.xxx.xxx:11111, params: xxx, error message: token exchange failed以及:
[SentinelClusterClient] [Priority] Server 状态异常,切换到本地限流发生这两种日志的时候,业务方看到的现象大概率是接口偶发限流、响应变慢、请求被降级。尤其当fallbackToLocalWhenFail设为 false 时,你还会看到大量请求直接被拒,这时业务端报错明显增加。
5.2 从连接握手开始逐层排查
我自己的排查顺序是固定的,从链路最底层往上层查。先把负载均衡这一层的 TCP 连通性排除掉:
telnet 负载均衡VIP 11111如果连不通,先查负载均衡后端节点的健康状态,是四层健康检查把主节点摘除了,还是主节点服务真的挂了。这个阶段不要急着去动应用配置,很多时候问题根本不在业务侧。
如果连通性没问题,再往下就是 Token Client 的配置问题。优先检查客户端有没有正确指向负载均衡 VIP。有人图省事直接复制了某个 Token Server 节点 IP 到配置里,导致主节点切换以后,客户端依旧在向老 IP 发起连接。这种情况在日志里的表现是连接超时、反复重连,而不是直接报连接拒绝。
最后一步才是查规则层面。打开 Sentinel 控制台,看 Token Server 节点是否注册成功、是否处于健康状态,以及集群配置中的 flowId 是否和业务侧使用的资源对应。很多时候,客户端申请令牌时带上的规则 ID 在服务端根本不存在,服务端返回找不到规则,客户端就把它视为 token exchange failed。这个问题在控制台上很好定位,线上日志里却不明显。
5.3 主备切换时的连接中断处理
我下面再分享一个实战中频繁出现的场景:主 Token Server 所在的机器因为发布重启,负载均衡把连接切到备节点,但这瞬间正在处理中的令牌申请请求会立刻失败。此时客户端的报错不是超时,而是直接 RST。
这里需要明确一个取舍:如果你不允许这一秒内任何请求失败,可以在客户端把请求超时时间调长一点,但代价是正常的限流拒绝也会变慢,业务等待时间变长。我个人的做法是,接受切换那一瞬间的少量失败,但把客户端的重试机制做好。Sentinel 的 Token Client 本身不具备自动重试逻辑,如果你用的是服务框架,可以在框架层面做一次重试。但必须注意,重试只适用于读操作,像下单、转账这类写操作,重试会造成重复提交。
因此我还是建议在 Token Server 切换时配合发布流程控制:先摘流量、再重启、然后重新放流量。不要让切换发生在一个请求还在途中的瞬间。这不光是限流组件的问题,任何有状态服务做发布都应该遵循这个原则。
6. 高可用验证与我的实践心得
部署方案设计得再完整,最终还是要靠故障演练来验证。我每次搭完这套环境,都会分三步做验证。第一步是压测,用固定 QPS 打业务服务,同时观察控制台上的集群总流量数值,确认阈值生效且统计准确。第二步是模拟主节点宕机,手动杀掉 Token Server 主节点进程,观察负载均衡是否在健康检查间隔内摘除节点,客户端是否自动切换到了备节点,以及切换后集群限流是否继续生效。第三步是重新启动主节点,验证服务恢复后流量是否自动回切。
这三步里,最值得关注的是切换时间窗口。负载均衡的四层健康检查目前我是 2 秒一次,检测到主节点异常之后摘除节点,再到客户端重连备节点,整体耗时在 3 到 5 秒之间。这个窗口期内集群限流精度下降,但不至于中断。如果你的业务对连续性要求更高,健康检查间隔可以缩到 1 秒,代价是负载均衡的检测压力增大,这个根据团队运维能力来权衡。
部署完成后每天都会自动跑一次巡检脚本,探测 Token Server 进程状态、端口监听状态、规则更新时间、客户端连接数这几个指标,任何一个异常都会触发告警。这套体系上线后,集群限流导致的接口故障基本绝迹,而且后续再调整限流阈值时再也不用一台台机器改配置,在 Nacos 上改完直接全集群生效。
最后再分享一个小技巧:如果你给业务应用也挂上了 NacosDataSource,那更新限流规则的时候有个并发问题要小心。Sentinel 的规则更新是异步的,新规则要等当前时间窗口结束后才彻底生效。所以在 Nacos 上发布新规则之后,不要立刻用压测去验证效果,等一个完整的滑动窗口周期再检查,否则你看到的还是一个过渡态的统计结果,容易误判规则配置有问题。