
在Sentinel里做限流绝大多数人一开始接触的都是QPS和线程数这两个阈值类型。这两个指标看着简单但选错维度造成的线上事故我见过不止一次。有个很典型的案例一个订单查询接口平时接口响应5msQPS限流阈值设了1000一切正常。某天数据库一条慢SQL把响应时间拉到200msQPS并没有超标甚至因为系统变慢QPS还降下来了但接口内部的并发线程数从5左右一路堆到几百最后Tomcat线程池被打满整个服务全部超时。真正出事的不是流量太快而是请求处理得太慢每个请求都占着线程不放。这篇文章我想把Sentinel这两个限流维度掰开揉碎讲清楚它们各自解决什么问题什么场景用QPS、什么场景用线程数两种模式在性能和防护效果上的差异以及规则如何持久化到Redis集群避免“重启即丢”。如果你在用Spring Cloud Alibaba或者正在做微服务稳定性治理这篇文章里的踩坑经验应该能帮你少走不少弯路。1. 为什么限流要分QPS和线程数一次事故讲透两个维度的本质1.1 复盘一次“流量不高但服务被打垮”的线上故障还是开头那个订单查询接口。它本身的逻辑不复杂查订单主表、查状态表再补几个字典字段正常情况下平均响应时间5毫秒单机QPS压测到1200都没问题。我们当时给它配的Sentinel规则是QPS阈值1000流控效果快速失败。大促当天运营做了一个批量活动导致订单表里某些数据行被频繁更新数据库突然出现一条慢SQL。这个接口的RT从5毫秒涨到200毫秒甚至更高。诡异的事情发生了监控面板上QPS其实只有300多远没到阈值1000但服务线程池却被打满CPU飙升接口大量超时。事后复盘才发现QPS限流看的是“每秒能放进来多少个请求”它完全不关心一个请求在系统里停留多久。RT变成200毫秒之后每个请求占用Tomcat线程的时间变长了40倍原来并发只需要5个线程现在需要60个线程才能维持同样的吞吐。线程池一旦被这些慢请求占满后续请求全部排队等待服务自然就雪崩了。这个案例给我最大的教训是只看QPS并不等于保护了系统。QPS代表的请求数量线程数代表的并发资源占用前者管入口后者管出口两者必须分开理解。1.2 QPS看入口流速线程数看现场占用用个生活化的类比。你把服务当成一个银行网点QPS限流就是门口保安在数“每秒进来多少人”超过人数阈值就不让进线程数限流则是看“网点里有多少客户正在窗口办业务”只要柜台前的等待人数超过阈值就不放新客户进来。这两个数值在正常情况下是线性相关的接口响应越快同样并发下能扛住的QPS就越高。比如一个接口平均RT是10毫秒理论上单线程每秒可以处理100个请求如果系统能支撑10个并发线程这个接口的QPS上限就是1000左右。反过来QPS是1000的时候平均并发线程数也在10左右。但一旦接口变慢这种线性关系就断了。RT从10毫秒涨到1秒同样的并发线程数下QPS会从1000暴跌到10。这时候如果你还用固定QPS阈值去限流等于给进来的每一个慢请求都发放了“长期占用线程”的许可证线程池迟早被拖垮。线程数限流的本质是给并发资源设硬上限不管请求快还是慢占用超过阈值就拒绝。1.3 选错维度的代价一个管不住慢调用一个挡不住突刺只配QPS不配线程数风险在于慢调用场景下限流失效。比如下游依赖抖动、数据库锁等待、网络重试都会让RT变长这时QPS数值可能不高系统却已经危在旦夕。我见过不少团队给所有接口统一配了QPS规则结果流量洪峰没来被一次第三方接口的慢响应全干趴了。只配线程数不配QPS风险在于瞬时流量突刺。比如秒杀开始的一瞬间网关打进来一万个请求但接口本身只要50个并发就能跑满线程数规则确实会快速拒绝超额流量。问题是线程数规则对“放行的请求”和“拒绝的请求”之间缺少时间维度上的平滑如果业务侧更在意的是每秒请求总量不要超过某个安全线单靠线程数可能拦不住突发大流量打爆下游。所以我的观点很明确两个维度不是二选一而是按链路分层配。网关层看QPS入口挡住洪峰服务层看线程数内部防止慢调用堆积。两者配合才是完整的限流方案这一点后面章节会展开讲。2. QPS限流什么时候用适用场景与控制台配置要点2.1 适合QPS限流的业务画像QPS限流最适合的场景总结下来有三个特征接口响应时间短且稳定、请求处理本身不消耗稀缺资源、业务允许快速失败。典型的例子是读多写少的查询接口。比如商品详情、配置查询、用户信息查询这些接口逻辑简单RT通常稳定在几毫秒到几十毫秒QPS高意味着压力大按QPS设限流简单直观。另一个典型是网关层限流你的下游服务能力有限网关只知道“请求从哪个路径进来”很难判断每个请求内部会占用多少线程这时给路径配置QPS限流能挡住大流量入口。反过来说如果接口RT波动很大或者内部会调用第三方服务、会写数据库、会做大数据量的计算光用QPS限流就不够了。因为同样一个请求有时2毫秒就返回有时要等2秒QPS只能告诉你“来了多少”告诉不了你“正在消耗多少”。我自己的习惯是给所有对外网关和纯查询接口先配一把QPS限流阈值根据线上P99流量上浮20%左右再在这把锁下面给核心服务配线程数限流兜底。2.2 控制台配置QPS规则的完整步骤先说一下Sentinel的基本埋点方式。最省事的是用SentinelResource注解加一个BlockHandler方法代码大致是这样的SentinelResource(value queryOrderList, blockHandler queryOrderListBlock) public ListOrder queryOrderList(Long userId) { // 业务逻辑 return orderDao.listByUser(userId); } public ListOrder queryOrderListBlock(Long userId, BlockException ex) { // 被限流后的兜底逻辑可以返回空列表、缓存数据或者抛业务异常 if (ex instanceof FlowException) { logger.warn(queryOrderList flow limited, userId {}, userId); } return Collections.emptyList(); }这里有个很多人忽略的细节SentinelResource只会在进入方法时触发规则校验方法内部如果自己catch了异常Sentinel默认不会统计业务异常但限流逻辑依然有效。如果你用的是SphU.entry()手动埋点一定要在finally里调用exit()释放否则调用线程数的计数会一直累加这个坑后面会细说。埋点做完之后打开Sentinel控制台在“流控规则”页面点新增资源名queryOrderList要和注解里的value保持一致。针对来源默认default如果不做多来源区分就不用改。阈值类型选QPS。单机阈值1000这个值最好来自压测数据而不是拍脑袋。流控效果快速失败、Warm Up、排队等待三选一。保存后规则会实时推送到接入Sentinel的客户端不需要重启应用。控制台上能看到实时监控曲线确认限流是否生效。2.3 配QPS限流最容易踩的三个坑冷启动、突刺、排队第一个坑是冷启动。刚重启的应用JVM还没完全预热很多类第一次加载会很慢。如果你一上来就用1000的阈值放开流量很可能还没跑到1000 QPS服务就先因为GC或者类加载被拖垮。Sentinel的Warm Up功能就是干这个的阈值从冷启动因子默认3慢慢爬升到预设值我一般给核心接口配置5分钟的预热周期效果接近“渐变放量”。第二个坑是突刺流量。QPS限流的统计是基于滑动窗口的默认统计窗口是1秒。如果流量在窗口的临界点集中爆发可能出现实际QPS超过阈值一小截才被拦截的情况。对要求特别精准的场景可以配合排队等待模式。排队等待会把超阈值的请求先放队列里按固定速率放行适合削峰填谷代价就是增加了一点延迟得看你业务能不能接受。第三个坑是阈值设置完全依赖经验值。我见过有人直接把QPS阈值设成2000理由是“压测能到2000”结果线上流量稍微集中一点就瞬间打满。合理做法是先压测找出接口的拐点RT和对应QPS再往后退20%到30%作为生产阈值。比如压测发现500 QPS时RT开始抬升就可以把阈值定在350到400留出足够的buffer。下面这个表格是三种流控效果的对比配置时可以直接参考流控效果适用场景行为表现注意事项快速失败大部分接口超过阈值立即拒绝对突发流量最直接但容易误伤Warm Up刚启动或有预热需求阈值逐渐爬升到预设值冷启动因子默认3可修改排队等待流量周期性明显的场景请求排队匀速通过需配置超时时间超时后丢弃3. 线程数限流什么时候用从慢接口到线程池保护的实战3.1 适合线程数限流的业务画像线程数限流的使用场景一句话概括凡是请求处理时间不可控的接口都应该考虑线程数限流。我总结过几类典型业务。第一类是调用第三方服务的接口比如拉起支付、调用风控、调用外部物流查询这些下游接口质量不在你掌控范围内一个第三方卡顿几十秒都是常事。第二类是内部批量处理接口比如报表导出、大数据量导入、给会员批量发消息单次任务执行时间长并发稍微一高线程资源立刻吃紧。第三类是核心写链路比如下单、支付回调这类接口对成功率和RT要求高宁愿快速失败扔掉一部分流量也不能让慢请求把线程池占死。这三类业务有一个共同点你很难用一个固定的QPS阈值去描述它的压力。因为同样一次调用慢的时候是快的的时候耗时几十倍QPS限流很难正确反映真实压力线程数限流天然就是按并发资源来衡量的。3.2 线程数阈值怎么定从目标RT和预期QPS反推线程数阈值不是拍脑袋定的它有一个非常直观的换算公式单机目标并发线程数 目标QPS × 目标平均RT举个例子。某个写接口业务要求单机支撑500 QPS压测时平均RT是40毫秒那并发线程就是500乘以0.04等于20。生产环境为了留buffer可以把这个阈值适当调大比如25到30因为线上RT大概率比压测环境高。如果RT本身不稳定就不要只看平均值。我曾经接过一个报表导出接口平均RT是300毫秒但P99高达3秒。如果按平均RT算目标并发只需要30左右实际上因为部分请求卡在3秒并发线程会堆到300。这种接口我的做法是先压测统计线程数曲线找到并发拐点再按拐点的70%配置。线程数阈值设得太小会频繁快速失败影响正常用户设得太大又起不到保护作用。我一般会结合基线数据分段调整先设置一个保守值观察一周再根据监控面板上的实时线程数逐步放松。理想状态是阈值刚好能让CPU核心吃满同时不让请求排队堆积。3.3 线程数限流保护Tomcat线程池的原理理解线程数限流为什么能防慢调用关键要明白服务端线程池的工作方式。以Spring Boot默认的Tomcat为例核心线程池默认大小是200每个HTTP请求会占用一个Tomcat工作线程执行你的业务逻辑。如果不做线程数限流当一个接口的RT从50毫秒涨到5秒时同一个线程可以处理100个请求的时间被压缩成了1个请求的时间。外部流量稍微增加Tomcat的200个线程很快就会被慢请求占满。更要命的是Tomcat线程池满了之后其它接口的请求也没线程可用整个应用陷入“线程饥饿”。线程数限流是在业务代码入口处加了一个并发计数器。每次请求进来计数器加一处理完减一。当计数器的值超过你配置的阈值新请求直接快速失败根本不会进入Tomcat工作线程执行业务逻辑。这样慢请求最多占用你设定的那几十个线程其它线程依然可以处理别的接口。我在生产上给核心链路配置过一组对照线程数阈值50Tomcat默认200。下游接口卡顿上线了一次未保护的接口全部雪崩被保护的接口靠着快速失败把错误率控制在2%以内服务整体稳如老狗。这是线程数限流最值钱的地方。下面这个速查表可以帮你在配置前快速判断该用哪个维度判断维度QPS限流线程数限流核心指标单位时间请求数并发调用线程数关注点入口流量大小资源占用情况响应时间稳定非常适合也能用但优势不明显响应时间波动大容易失效强烈建议使用瞬时大流量最直接有效需要配合合理阈值慢调用拖垮线程池基本防不住天然防护适用层级网关、读接口写接口、第三方依赖4. QPS与线程数限流的性能差异实现机制与压测数据4.1 底层实现差别滑动窗口计数器 vs 并发计数很多人关心两种限流模式在性能开销上的差异这个要先从Sentinel的底层机制说起。QPS限流走的是滑动窗口统计。Sentinel把时间切分成一个一个的小格子默认每个格子500毫秒两个格子构成一个1秒的统计窗口。每次请求进来先在当前格子上累加计数然后检查窗口内的总请求数是否超过阈值。这个统计涉及时间戳计算、格子定位、原子累加虽然性能很高但每次请求都有额外开销。线程数限流走的是并发计数器。Sentinel在资源入口维护一个线程数计数器请求进入时执行原子递增退出时原子递减判断当前并发值是否达到阈值。从指令数量上看线程数限流的统计开销比QPS限流更小因为它不用切分时间窗口不用处理格子滚动只要维护一个整数就行。但实际性能差异在绝大多数业务系统里几乎感觉不到因为单次请求的开销远大于限流判断本身的开销。除非你的接口QPS已经压到几万以上否则这两个模式本身的耗时都在微秒级别可以忽略不计。真正需要关注的性能差异在防护效果而不是CPU开销。4.2 一组压测对比数据快接口和慢接口的表现差异我在测试环境做过一组对照压测机器是8核16G接口逻辑很简单分别测了快接口平均RT 5ms和慢接口平均RT 50ms和200ms两种情况。先看快接口。QPS阈值1000线程数阈值按公式换算约等于50。压测打到1500 QPS时两种模式的表现接近QPS限流大概通过1000左右线程数限流大概通过950到1000之间被拒绝的请求都会立刻返回Block异常RT平均值都在5到6毫秒。线程数模式因为多了信号量获取和释放在高并发下会有轻微抖动但整体可控。再看慢接口。接口平均RT变成200msQPS限流阈值依然1000线程数阈值依然50。压测打到800 QPS时QPS模式下实际并发线程会跑到160左右服务器CPU占用接近饱和RT涨到400毫秒以上整个系统已经接近不稳定线程数模式下50个并发占满后后续请求全部快速失败实际吞吐稳定在250 QPS左右RT保持在200ms附近系统状态非常平稳。压测场景QPS限流表现线程数限流表现快接口 5ms1500 QPS通过约1000RT稳定通过约950-1000RT轻微抖动慢接口 200ms800 QPS并发160CPU高RT劣化并发50吞吐250快速失败所以你会发现在响应时间稳定的场景下两个模式几乎等价一旦响应时间劣化QPS限流基本失去保护能力线程数限流仍然守得住。4.3 高并发慢调用场景为什么线程数限流更稳为什么慢调用场景下线程数限流更稳因为QPS限流依赖的是“时间维度”而线程数限流依赖的是“资源维度”。时间维度的特点是只看数量不看质量。接口变慢后QPS可能只有200但并发线程数已经飙到500系统实际承受的压力远大于200 QPS对应的压力。这时候QPS阈值1000根本不会触发限流形同虚设。资源维度的特点是直接卡住系统最大的瓶颈点。无论接口快还是慢并发线程数一旦超过系统能承载的上限线程调度、内存、数据库连接这些资源都会跟着告急。线程数限流提前把这个上限锁死保证系统无论如何都不会进入不可控状态。我在很多分享里反复强调一个观点服务被压垮的瞬间QPS往往不是最高的时刻而是RT最长、并发最高的时刻。想明白这一点你就知道为什么线程数限流是慢链路里不可或缺的一道保险。5. 规则持久化到Redis集群告别“重启即丢规则”5.1 控制台改规则只存在内存集群实例全靠手工同步Sentinel控制台默认把规则配置在内存里这有一个非常坑的副作用应用重启规则全部丢失。我见过不止一个团队线上辛辛苦苦配了十几条限流规则某次发版时Pod滚动重启规则清空等于裸奔跑了一上午直到流量高峰才被发现。更麻烦的是多实例场景。假如一个服务有5个实例你在控制台上改了一条规则Sentinel控制台是推送到每个连接的客户端的正常情况下5个实例都能收到。但如果某个实例当时刚好断连或者控制台和应用之间的网络有抖动那这条规则就漏推了集群里就会出现有的实例限流、有的实例不限流的情况。解决这个问题只有一条路规则不能只放在内存里要把数据源接到外部存储。常见的选择是Nacos或者Redis这里重点说Redis集群的方案因为有些团队基础组件里已经有Redis不想再额外引入配置中心。5.2 Redis数据源初始化与规则热更新机制Sentinel官方提供了一个sentinel-datasource-redis扩展包用来对接Redis数据源。它的工作机制并不复杂核心就两步应用启动时从Redis指定的key读取一份规则JSON初始化到内存。应用订阅一个指定的Redis Channel收到消息后重新从key读取规则实现热更新。所以你要做的就是在Redis里准备两个东西一个用来存规则数据的key比如sentinel:flow:rules一个用来发变更通知的channel比如sentinel-rule。应用侧初始化的代码骨架大致是这样的// 连接配置按你的Redis客户端构造单机和集群模式略有差异这里略过细节 RedisConnectionConfig connectionConfig RedisConnectionConfig.builder() .withHost(10.0.0.11) .withPort(6379) .withDatabase(0) .build(); // ruleKey 是规则存储的 keychannel 是变更通知通道 RedisDataSourceListFlowRule redisDataSource new RedisDataSource( connectionConfig, sentinel:flow:rules, sentinel-rule, source - JSON.parseObject(source, new TypeReferenceListFlowRule() {})); // 注册到 RuleManager之后规则变更会自动生效 FlowRuleManager.register2Property(redisDataSource.getProperty());规则数据格式是一个JSON数组每个元素对应一条流控规则核心字段包括resource、grade、count、limitApp、strategy、controlBehavior。其中grade为0代表线程数为1代表QPS这个别填反了。改规则的操作流程是先用redis-cli或者管理后台把新的规则JSON写入sentinel:flow:rules这个key然后往sentinel-rule这个channel发一条消息比如PUBLISH sentinel-rule update。客户端收到消息后会重新拉取规则并刷新内存整个过程不需要重启应用。5.3 生产环境用Redis集群做规则源的几个坑用Redis集群做Sentinel数据源有三个坑是必须提前预防的。第一个坑是Redis Pub/Sub在集群模式下的可用性问题。Redis集群里PUBLISH命令只会广播到当前节点连接的订阅者如果应用实例连的节点和发布消息的节点不一致消息可能丢失。所以我在生产上除了走Pub/Sub之外还会在客户端加一个定时兜底任务每30秒重新从Redis拉一次规则对比版本号发现变化就刷新。消息丢了不致命但规则必须最终一致。第二个坑是流量不均时的全局感知问题。Redis数据源本身只负责下发规则限流还是每个实例各自统计。如果服务有5个实例但流量因为负载均衡策略不均一个实例打到了50%的流量单机QPS阈值很容易被触发。这种情况要么压测时按最差流量分布留buffer要么上Sentinel的集群流控把多实例的统计汇总到中心节点。第三个坑是Redis本身不可用。限流规则放在Redis上如果Redis故障客户端初始化数据源失败应用会直接没有限流规则。我的做法是本机额外缓存一份规则快照文件启动时先从本地加载再尝试连接Redis万一Redis不可用也能用缓存规则顶着。限流保护不能依赖外部组件的高可用这是稳定性设计里很基本的一条。6. 常见问题与排查技巧QPS和线程数限流实战速查6.1 规则不生效的第一排查顺序限流规则配了却没生效是最常见的问题。我的排查顺序基本固定从代码到控制台一层层找。第一确认资源名是否完全一致。很多人SentinelResource里写的是queryOrder控制台里配的却是queryOrderList两个名字对不上规则自然不生效。第二确认埋点是否真的触发了。Sentinel不是全自动拦截所有请求的如果不加注解也不手动SphU.entry()它根本不知道有这个资源。第三确认有没有在finally里调用exit()。如果只进不出线程数统计会一直累加表现就是莫名其妙被限流。第四确认控制台和应用版本是否兼容。Sentinel的版本对不上控制台推送规则可能失败客户端又没有报错这种情况最容易让人抓狂。另外还有一个隐蔽问题在异步线程里做Sentinel埋点。比如业务逻辑用了Async或者委托线程池你在主线程里记了一次线程数实际执行在另一个线程里计数归属就乱了。正确做法是在异步执行体内部重新埋点或者把异步调用改造成同步调用再包一层限流。6.2 线程数限流是否要配超时和熔断线程数限流能解决并发堆积但解决不了单个请求卡死的问题。最典型的情况一个接口调了一个永远不返回的第三方服务线程数阈值5050个线程全卡在等待上新请求进来全部快速失败表面上系统没有雪崩但吞吐量变成了0。所以线程数限流一定要和超时控制配合使用。Feign和RestTemplate都需要显式配置连接超时和读取超时比如读取超时设成2秒到5秒超过就快速失败。超时之外我还会给第三方弱依赖配Sentinel的熔断规则比如异常比例超过50%时熔断让请求不再进入调用逻辑直接走降级兜底。线程数限流管并发超时管单请求上限熔断管整体可用性三者叠加才完整。我的建议是线程数阈值不要设得太满给超时重试留一点余量。比如压测算出来并发上限是30可以设成25或者26留出几个线程给重试、心跳、健康检查之类的请求避免被业务流量彻底堵死。6.3 线上限流配置SOP速查表我给自己团队的限流配置流程总结成了一页纸每次新建接口按这个步骤走基本不会翻车排查项操作要点明确业务链路层级网关层配QPS限流核心服务配线程数限流先压测后配置压出拐点RT和QPS阈值按拐点打七到八折资源命名统一注解值、控制台资源名、监控报表保持一致线程数阈值换算QPS乘目标RT取并发值再加鼓politik不写“再按1.2倍放宽”配置持久化接入Redis或者Nacos数据源避免重启丢失观察期滚动调整先保守上线看一周曲线再逐步放宽兜底原则Redis挂了用本地缓存规则控制台挂了不影响已配规则在实际操作中我还会把每条规则的limitApp配置成default这样无论哪个调用来源都会受控。如果某些内部系统需要更高的配额再用单独的来源单独配置优先级更高规则之间是叠加生效的关系一个请求必须通过所有相关规则的校验才能放行。最后说点个人的体会这几年处理过的线上故障里因为限流维度选错导致的故障比阈值定错的多得多。很多人拿到Sentinel第一件事就是配QPS配完以为万事大吉结果慢调用一起来照样雪崩。我现在的习惯是新接口上线前先问自己一句它到底是怕流量太猛还是怕并发资源被占满怕流量猛就上QPS怕链路慢就上线程数两个都怕就都配。最后分享一个小技巧。Sentinel控制台里的“实时监控”页能看到每个资源的通过QPS和拒绝QPS但默认看不到线程数曲线。我会在业务埋点里顺手把当前并发线程数打到日志里和监控系统联动这样能更早发现线程堆积的趋势。限流配置不是一次性工作它需要跟着流量和下游健康状况持续调整保持对指标的敏感比任何规则模板都管用。