做微服务的人应该都体会过这种瞬间线上告警突然炸成一片某个接口的TP99从几十毫秒一路涨到两三秒紧接着数据库连接池被打满整个应用开始成片拒绝服务。我第一次碰上这种场面时第一反应是加机器、加连接池、加超时时间折腾到后半夜发现真正解决问题的其实不是加资源而是把流量控制住。这篇文章就围绕Sentinel最核心的三块能力展开流控规则QPS阈值/线程数阈值、热点参数限流、系统自适应保护。里面既有规则原理拆解也有我从实际项目中踩坑换来的配置经验和排错清单适合正在用Spring Cloud Alibaba做微服务、或者打算把Sentinel引入生产环境的读者参考。1. 一次线上事故让我意识到流量控制不是可有可无的1.1 事故经过一个慢接口拖垮了整条服务链路那次事故发生在一次优惠活动期间。白天流量都还正常晚上八点流量开始爬坡先是商品详情接口偶尔超时半小时后订单服务整体的可用率直线下降。查监控时发现Tomcat的线程池几乎被打满一个查询历史订单的接口因为一条SQL没走索引单次耗时从30ms恶化到2秒多。问题在于这个慢接口占用了大量线程还不释放后续所有请求都在线程池里排队哪怕是想快速返回一个兜底数据的接口也拿不到线程。事后复盘时我们列了三个关键结论第一慢调用如果不被拦截会像滚雪球一样吃掉整条链路的线程资源第二单纯靠超时时间并不可靠因为线程一旦被占用超时后的拒绝动作本身也需要线程去执行第三数据库这些公共资源是有限的应用层不设防压力最终都会传导到最底层。流量控制要做的就是在请求还没有真正占用宝贵资源之前先把超出的部分挡在门外。1.2 为什么加机器加资源解决不了这个问题很多人第一反应是服务扛不住就扩容嘛。但加机器只能解决平均水平解决不了瞬时脉冲和尾部延迟。同一个集群里如果某个实例因为GC或者网络抖动静默掉流量会立刻倾斜到其他实例单点压力反而更大。更何况订单服务依赖的数据库、Redis这些公共组件扩容应用实例并不能等比扩容它们的能力。所以我在后来的方案评审里反复强调一句话限流不是让系统不处理请求而是让系统在能力边界内处理请求。你可以把它理解成一个水龙头水管口径就那么大后面接再多水桶也没用真正有效的是在水管入口装一个可以自动调节的阀门。1.3 Sentinel解决这个问题的核心思路资源规则实时统计Sentinel的核心抽象是资源。一个接口、一个方法、一段代码块都可以声明成资源然后为这个资源配置各种保护规则。规则的作用时机是在请求进入资源执行之前的插槽链slot chain里经过的一系列检查包括授权规则、系统保护规则、流控规则、降级规则、热点参数规则等。相比之前用过的HystrixSentinel在整体设计上更贴近实际流量治理场景这里我列一个简单的对比对比项HystrixSentinel统计模型固定时间窗口滑动窗口精度更高流控模式比较单一直接、关联、链路热点参数限流不支持原生支持系统自适应保护无原生支持控制台已停更多年活跃维护可视化配置持久化机制较弱支持Nacos、Apollo等多种DataSourceSentinel统计QPS用的是滑动窗口不是传统的固定窗口。固定窗口的问题是临界值容易被打穿比如每秒限制100个请求前900毫秒进来99个最后100毫秒又进来100个一秒钟内其实已经超过100按固定窗口看却发现没超。滑动窗口把时间切成更小的时间片能大幅减少这种误差应对脉冲流量更稳。2. 流控规则怎么配QPS阈值、线程数阈值与三种防御姿势2.1 阈值类型怎么选QPS和线程数各自的脾气新增流控规则时第一个要选的就是阈值类型默认是QPS。这两种类型看起来差不多实际语义完全不同选错会导致规则要么形同虚设要么误伤严重。QPS指的是每秒钟允许通过的请求数量它描述的是流量的速率。适合用来防突发流量比如某个接口平时只有几百QPS大促时可能冲到几千设一个QPS阈值能直接挡住洪峰。QPS限流的优点是很直观压测报告里就有数据缺点是它不关心每个请求到底慢还是快如果一个新上线的慢接口把RT从50ms拉到500msQPS不变的情况下线程占用其实已经翻了十倍这时候单看QPS是防不住的。线程数阈值描述的是同时正在处理的请求数量也就是并发占用的线程数。注意它统计的不是排队中的请求而是进入了处理阶段、正在占用执行资源的请求。这个指标对慢调用特别敏感因为接口变慢单个请求占用线程的时间变长并发线程数就会持续上涨。把线程数阈值设为比如50意味着同一时刻最多50个请求在处理再多就直接拒绝线程池就不会被拖垮。我的选择经验是接口RT稳定、只担心流量洪峰时优先用QPS接口RT波动大、依赖下游比较慢的写接口和查询接口优先用线程数。如果你的服务已经被慢调用坑过一次那就别犹豫先用线程数规则兜底。2.2 流控模式直接、关联、链路分别管什么规则里还有流控模式选项默认是直接。直接模式最直白当前资源达到阈值就限当前资源自己。适合像订单创建接口每秒最多500QPS这种独立约束。关联模式是指当关联资源达到阈值时对当前资源进行限流。比如一个接口同时有读操作和写操作写操作占用数据库连接比较多我们希望当写操作流量过大时限制读操作的流量给写操作腾出资源。配置时当前资源是读接口关联资源是写接口阈值作用于写接口但限流的动作发生在读接口上。这个模式在保护核心写链路不被非核心读流量拖垮的场景里非常好用。链路模式则是针对调用链路做限制。同一份资源可能被多个入口调用链路模式只统计从指定入口到当前资源的流量。假设有个公共方法getUserInfo被订单接口和营销接口同时调用链路模式可以做到只从订单入口进来时对该资源限流营销接口不受影响。这个模式需要配合注解和链路追踪才能正确统计配置成本略高但语义最精确。2.3 流控效果快速失败、Warm Up与排队等待选完阈值类型和模式还要选流控效果这直接决定超限请求怎么被拒绝。快速失败是默认方式一旦超限立即抛异常通过BlockException让请求快速返回弊端是瞬间大量请求被拒绝适合对用户体验要求不那么极致的管理端接口。Warm Up也叫冷启动适合那些刚上线的接口。它让通过的QPS从阈值的三分之一冷加载因子默认是3逐渐增长到完整阈值预热时间可以设几秒到几分钟。为什么要有这个设计因为系统长时间空闲后突然来一大波流量JIT编译、缓存初始化、连接池创建这些都还没就绪直接按满额放行容易把系统打死。预热就是给系统一个缓一缓爬起来的时间。排队等待则完全不同它不拒绝请求而是让请求先排队按设定的QPS匀速放行。比如阈值设100QPS超过的部分先排队只要等得起就能被处理。注意这里必须设置超时时间我一般建议不超过500ms否则被排队堵住的请求攒太多前端等不了体验会更差。这种模式适合削峰填谷比如定时任务、批量消息处理这类非实时路径。2.4 控制台实操与规则JSON示例在Sentinel控制台配置流控规则的路径一般是簇点链路 - 选择资源 - 新增流控规则。页面上把这几个字段填清楚点击新增就能生效。但如果你的规则要放到Nacos里统一管理就需要知道底层规则长什么样。一个完整的FlowRule JSON是这样的[ { resource: GET:/order/detail, limitApp: default, grade: 1, count: 500, strategy: 0, controlBehavior: 0, clusterMode: false } ]resource是资源名必须和实际拦截点的资源标识完全一致grade为1表示QPS为2表示线程数strategy为0直接模式为1关联模式为2链路模式controlBehavior为0快速失败为1 Warm Up为2排队等待。关于阈值的初始值怎么定我的做法是先压测。用压测工具把接口打到极限记录它不报错时的最大QPS然后取这个值的80%作为线上初始阈值再根据高峰期流量做微调。为什么要留20%的buffer因为压测环境模型和线上流量特征永远有差异阈值卡得太死一次小流量抖动就会误杀一堆正常请求。3. 热点参数限流把流量控制精确到单个参数值3.1 热点限流的适用场景和设计思路普通流控规则面向的是整个资源不管请求参数是什么总量超过阈值就一起限。但线上流量往往高度倾斜商品详情接口几千QPS其实90%都打在几个爆款SKU上用户查询接口的流量也经常集中在头部用户身上。热点参数限流就是专治这种同接口不同参数冷热不均的问题。它允许你针对某个参数的值做单独统计比如第一个参数是userId时针对userId10001这个值单独设置500QPS其他所有用户共享100QPS。这样既保住了核心热点用户的流量又防止某个key被打爆殃及整个接口。它的实现原理是在资源入口记录指定位置参数的取值为每个参数值维护独立的QPS计数器。注意这里统计的粒度不是资源而是资源参数值这个组合。3.2 控制台配置参数索引、阈值与参数例外项在Sentinel控制台里热点参数限流入口和普通流控是独立的。新增热点规则时需要指定参数索引这个索引从0开始对应方法入参的位置。比如getUserInfo(String userId, String channel)userId的索引是0channel的索引是1。单机阈值就是每个参数值每秒允许通过的QPS统计时长默认1秒建议根据接口特点调整如果是详情页1秒的窗口够用如果是低频操作可以适当拉长到几秒减少抖动。最实用的功能是参数例外项。意思是你可以给指定的参数值设置单独的阈值。拿活动抽奖接口举例普通用户每秒最多100次但是某个头部主播带来的流量特别大100QPS明显不够那就把该主播对应的参数值单独拎出来设为1000QPS。配置例外项时注意参数值的类型要和入参类型一致否则规则不会匹配。3.3 代码里怎么接入热点规则热点规则虽然可以通过控制台配置但在生产环境我更推荐用代码初始化这样规则能跟着应用配置走避免控制台丢了规则就没了的尴尬。接入时先给方法加SentinelResource注解SentinelResource(value productDetail, blockHandler productDetailBlockHandler) public ProductDetail getProductDetail(Long skuId, Long userId) { return service.query(skuId, userId); }然后初始化热点规则ParamFlowRule rule new ParamFlowRule(productDetail) .setParamIdx(0) .setCount(100); ParamFlowItem item new ParamFlowItem(); item.setObject(888888); item.setCount(500); rule.setParamFlowItemList(Collections.singletonList(item)); ParamFlowRuleManager.loadRules(Collections.singletonList(rule));这段代码的意思是getProductDetail方法第一个参数skuId的QPS默认最多100当skuId等于888888这个爆款商品时阈值放宽到500。如果被限流会进入productDetailBlockHandler方法注意blockHandler方法的签名必须包含BlockException参数并且和原方法返回值一致否则不会生效。3.4 用热点规则时常见的三个误操作第一个误操作是参数索引搞错。有些同学把参数索引理解成自定义参数名直接传字符串进去结果规则永远匹配不上。索引就是方法入参的位置从0开始数这在文档里写得很清楚但实操中踩的人特别多。第二个误操作是对复杂对象生效的期待过高。如果接口入参是一个请求体DTO热点参数规则只能拿到DTO这个整体对象没法自动识别DTO内部的字段。我一般会在入口层把需要的字段拆出来单独传参或者做一个专门用来限流的重载方法保证热点参数能被精确提取。第三个误操作是热点规则和普通流控规则同时配置时误以为热点规则更严格就没必要配普通规则。实际上两种规则维度不同热点只拦某个参数值的突发流量普通流控拦的是整体洪峰生产环境我建议两个都配上让它们各司其职。4. 系统自适应保护把兜底策略交给系统负载说话4.1 系统规则里的五类指标前两种规则都是站在某个资源的视角做保护系统自适应保护则站在了整个应用进程的高度。它监控的不是某个接口的QPS而是整个系统是否处于健康状态。Sentinel系统规则支持下面几类指标指标含义常见配置Load系统负载需要操作系统支持不超过CPU核数CPU使用率整机CPU占用60%-80%平均RT所有入口请求的平均响应时间按压测数据并发线程数所有入口的并发线程总量按压测数据入口QPS所有入口的总请求速率按整体容量估算这几项指标可以单独开启也可以组合使用。需要注意的是Load和CPU使用率依赖系统指标采集有些容器环境拿不到准确数据开启前要确认监控数据真的在上报。4.2 自适应保护的核心逻辑动态计算安全流量系统自适应保护和普通流控最大的区别在于它不是固定每秒最多N个请求而是当系统指标超过阈值后动态计算当前还能放行多少流量。可以这么理解正常情况下入口不限流所有请求都能通过一旦检测到系统负载飙高比如Load超过了阈值Sentinel会根据当前的指标比例估算一个安全QPS然后拒绝安全QPS之外的请求让系统从过载状态慢慢恢复。等到指标回落到正常范围它又会自动放开流量。这个过程不需要人工干预。我习惯把它当成所有规则的最后一道防线。因为流控规则只能防住你提前想到的瓶颈系统保护能防住那些没想到的某个接口的日志突然打爆IO、GC异常导致CPU飙升、依赖的Redis集群抖动导致平均RT恶化。这些情况下具体接口的QPS未必很高但系统整体已经不行了必须有全局兜底。4.3 阈值设定经验别把Load规则当成摆设给系统规则设置阈值时最怕的是照搬网上的配置。Load阈值和机器核数直接相关。8核的机器Load一般不要超过816核可以适当放宽到12左右但再多就需要警惕。Load超过核数意味着CPU长时间处于排队状态此时接口延迟会明显上升。CPU使用率我一般建议设在70%到80%之间留一部分给JVM的GC和突发算力需求。平均RT和并发线程数的阈值我会根据压测数据来定一般取压测最大值的70%左右。入口QPS是最后一道总闸设成所有业务容量估算之和宁可宽松一点也不要因为一个边缘接口触发全局限流把核心业务误伤。第一次配置时如果拿不准我建议先只开启CPU使用率或Load观察一段时间告警情况确认保护逻辑没有频繁误触发后再根据实际效果逐步加上其他指标。4.4 系统规则和前面两类规则一起开时的注意点三个维度的规则并不冲突它们处理的是不同层面的风险。系统规则管系统扛不扛得住热点规则管热点key爆不爆普通流控规则管单个接口超不超量。同时配置时请求会依次经过这几道检查任何一道不通过都会被拦截所以最终生效的是最严格的那道规则。这里想特别提醒一点系统规则的优先级非常高一旦触发影响面是整个应用不只是某个接口。所以在控制台里修改系统规则时我坚持的原则是小步灰度先观察再放大不要在流量高峰直接改一个很紧的阈值上去那样等于人为制造一场全局限流。5. 落地工程化的关键规则持久化与三个容易翻车的细节5.1 默认规则只存内存重启一次就丢Sentinel默认情况下所有规则都是保存在应用内存里的。控制台新增一条规则只是推送到客户端内存控制台本身并不持久化。这意味着每次应用重启规则全部清零服务将以裸奔状态接入流量。第一次用Sentinel时我没意识到这个问题直到一次发布后流量猛涨限流完全没触发排查半天才发现规则没了。所以生产环境必须解决规则持久化问题。Sentinel提供了DataSource机制规则可以通过配置中心统一管理常见的选项有Nacos、Apollo、ZooKeeper也可以自行扩展Redis等。配置中心作为规则的上游客户端启动时拉取规则运行期间还能感知变更。5.2 用Nacos持久化流控规则一份可以直接改的样例如果你的配置中心是Nacos最典型的方式是在Spring Cloud Config里接入Sentinel的Nacos数据源。下面是application.yml里的配置样例spring: cloud: sentinel: transport: dashboard: localhost:8080 port: 8719 datasource: flow-rules: nacos: server-addr: 127.0.0.1:8848 dataId: sentinel-flow-rules groupId: DEFAULT_GROUP rule-type: flow hotkey-rules: nacos: server-addr: 127.0.0.1:8848 dataId: sentinel-hotkey-rules groupId: DEFAULT_GROUP rule-type: param-flow system-rules: nacos: server-addr: 127.0.0.1:8848 dataId: sentinel-system-rules groupId: DEFAULT_GROUP rule-type: systemrule-type字段对应不同的规则类型flow是流控规则param-flow是热点参数规则system是系统保护规则。Nacos上的JSON格式就是我前面给出的FlowRule数组格式。有一点要说清楚默认控制台的新增规则按钮仍然是把规则推到客户端内存并不会自动回写到Nacos。生产上我们用两种方式控制规则变更一是直接修改Nacos配置客户端会收到推送并自动更新二是给控制台做定制改造在保存规则时同时发布到Nacos。如果你不想改控制台源码我建议把控制台定位成只读监控台所有变更都走Nacos这样规则变更还能走配置审批流程不会被随便乱动。5.3 扩展话题用Redis集群做规则数据源的适用场景昨天有同事问我团队没有Nacos能不能用Redis集群存Sentinel规则。答案是能Sentinel的DataSource本身是一个开放的接口业界也有不少基于Redis的实现。大体思路是把规则以JSON形式写入Redis的某个key客户端实现一个ReadableDataSource启动时加载Redis里的规则并定期轮询或监听变更把最新规则推给对应的RuleManager。这种方案的优点是复用已有的Redis集群少维护一套配置中心。缺点也很明显Redis本身是缓存组件拿它存规则要自己搞定版本管理和变更记录多个实例同时监听同一份规则如果推送逻辑写得不严谨容易出现规则传播不一致。我的建议是如果团队已经有Nacos或Apollo优先用配置中心只有基建里确实没有配置中心、又不想专门引入一套的团队才考虑Redis方案而且一定要把客户端侧的监听和异常恢复逻辑写完善。5.4 最容易翻车的三个细节第一个细节是资源名不一致导致规则不生效。Sentinel在拦截到请求时会拿请求对应的资源名和规则里的resource字段做匹配两者不一致规则就是摆设。用注解时要确保value值全局唯一且稳定用Spring Cloud Alibaba时URL资源默认是类似GET:/order/detail的形式别在规则里多写一个空格或者少写一个斜杠。我见过有人把资源名写成中文备注结果当然匹配不上。第二个细节是blockHandler和fallback用错。blockHandler负责处理被限流、被降级的情况触发原因是BlockException执行逻辑通常是快速返回兜底数据fallback负责处理方法本身抛业务异常的情况。有些人只写fallback不写blockHandler结果限流触发时异常一路抛到前端。这两个处理函数必须都写上并且方法的返回值类型要一致参数顺序也要符合签名要求。第三个细节是控制台端口冲突导致应用不上报。Sentinel客户端默认通过8719端口与控制台通信如果服务器上端口被占用客户端会自动尝试8719之后的下一个端口但控制台记录的可能是旧端口就会出现控制台看不到应用的诡异问题。解决办法是启动时显式指定端口比如在JVM参数里设置-Dcsp.sentinel.api.port8720或者查看启动日志确认实际使用的端口。最后一个我特别想分享的习惯所有限流阈值都不是配置完就一劳永逸的。我每季度会重新压测一次核心链路根据流量增长、服务拆分、数据库变更等情况同步调整规则。线上规则变更一律先在灰度实例观察10分钟确认没有误杀、没有告警异常后再全量推送。这套流程看起来麻烦但比起一次错误的限流导致核心业务大面积不可用这点麻烦实在太值了。