Sentinel内部原理与生产治理
Sentinel 不只是“加一个注解后配置 QPS”。它在每个应用实例内部维护资源、调用上下文、调用树和滑动窗口统计,再通过一条可扩展的 SlotChain 判断请求能否进入业务。Dashboard 和 Nacos 等配置中心主要处在控制面;真正对每次请求做判断的是应用进程中的 Sentinel 数据面。
本文以企业中广泛使用的 Sentinel 1.8.x 核心模型讲原理,同时说明 JDK 8/Spring Boot 2 存量线与 Java 17+/Spring Boot 3 现代线。精确依赖和属性必须服从当前 Spring Cloud Alibaba BOM;不要把某个版本的 Starter 配置复制到另一条发布列车。
一、学完必须回答什么
SphU、CtSph、Context、Entry、ResourceWrapper、DefaultNode和ClusterNode分别是什么。- 一次请求从
entry()到业务方法、再到exit()的完整过程。 LeapArray为什么只用固定数量桶就能维护滑动窗口。- QPS、并发数、Warm Up、匀速排队和热点参数限流的算法边界。
- 熔断器怎样从 CLOSED 进入 OPEN、再由一次探测进入 HALF_OPEN。
- Dashboard、客户端命令端口、配置中心和本地 RuleManager 怎样协作。
- 本地流控与集群流控有什么本质区别,Token Server 挂了怎么办。
- 业务异常为什么可能没有计入熔断统计,异步调用为什么容易丢上下文。
- 扩容后阈值怎样变化,规则误发布、控制台失联和大量 Block 怎样恢复。
二、版本与生态边界
| 技术线 | 常见环境 | 学习重点 |
|---|---|---|
| 存量维护线 | JDK 8、Boot 2.x、匹配的Spring Cloud Alibaba | Sentinel 1.8.x Core、旧版Starter配置、Feign与Dashboard接入 |
| 现代项目线 | Java 17+、Boot 3.x、匹配的Spring Cloud Alibaba | Spring 6/Jakarta生态、现代BOM、Micrometer/Tracing、容器化部署 |
| 组件独立线 | 非Spring应用或SDK | 直接使用 sentinel-core 的 SphU.entry() 和 RuleManager |
Sentinel 的资源、SlotChain、滑动窗口和规则判断思想不会因为 Boot 版本改变,但自动配置类、配置属性、Feign集成、指标桥接和依赖版本会变化。现代项目要同时锁定:
Java版本 -> Spring Boot版本 -> Spring Cloud发布列车
-> Spring Cloud Alibaba版本 -> Sentinel依赖版本Java 17没有正式虚拟线程;虚拟线程在Java 21正式发布。即使使用虚拟线程,数据库连接、HTTP连接、CPU和下游配额仍然有限,Sentinel的并发控制、限流和熔断仍然有价值。
三、控制面和数据面
flowchart TD
A["规则发布"] --> B["Dashboard"]
B --> C["配置中心"]
C --> D["实例订阅"]
D --> E["本地RuleManager"]
E --> F["请求本地判断"]- 控制面负责规则编辑、持久化、发布、审计和回滚。
- 数据面位于每个应用进程内,正常请求不应每次远程调用 Dashboard。
- Dashboard不可用时,本地已有规则通常仍会工作,但查看实时资源、临时推送或修改规则会受影响。
- 配置中心不可用时,应用能否启动、是否沿用最后规则、恢复后怎样重订阅,取决于数据源和应用配置。
- 各实例分别接收规则,因此“配置中心写入成功”不等于所有实例同一毫秒生效。
四、核心对象不是同一个东西
| 对象 | 主要职责 | 生命周期或范围 | 常见误解 |
|---|---|---|---|
SphU | 给业务代码提供静态入口API | 进程级门面 | 它自己保存全部统计 |
CtSph | 创建Entry、查找SlotChain、触发检查 | 默认核心实现 | 只是一层注解代理 |
ResourceWrapper | 封装资源名、EntryType、资源类型 | 每次进入时引用,按资源缓存链 | 资源名可以无限动态生成 |
Context | 表示一次调用入口和当前Entry栈 | 通常绑定当前线程 | 等于HTTP请求对象 |
Entry | 表示一次资源进入,记录开始、错误、Block和父子关系 | 单次调用 | 只负责计数,不需要exit |
DefaultNode | 统计资源在某个Context调用路径下的数据 | Context与资源组合 | 等于资源全局统计 |
ClusterNode | 聚合同一资源跨Context的总体统计 | 进程内每个资源 | 等于跨机器集群统计 |
OriginNode | 按调用来源细分统计 | 资源与origin组合 | 自动知道真实租户或调用方 |
ProcessorSlotChain | 串联节点构建、统计和规则检查 | 通常按ResourceWrapper缓存 | 每次请求重新创建整条链 |
这里的 ClusterNode 只是“当前JVM内同一资源的聚合节点”,不是多机器集群。多实例总配额要使用集群流控或在网关等统一入口治理,不能把 ClusterNode 名字误解为跨JVM共享计数。
资源名和Context名必须是低基数稳定标识,例如 inventory-query。若把订单号、用户ID、URL查询参数拼进资源名,会不断创建资源和SlotChain;达到内部链缓存上限后,新资源甚至可能不再经过正常规则检查,同时还会造成内存和指标基数膨胀。
五、Spring应用启动时发生什么
flowchart TD
A["应用启动"] --> B["Starter自动装配"]
B --> C["注册调用适配器"]
C --> D["创建规则数据源"]
D --> E["加载并转换规则"]
E --> F["RuleManager生效"]
F --> G["启动心跳与命令服务"]
G --> H["Dashboard发现实例"]Sentinel核心通常按需初始化。Spring Cloud Alibaba可通过当前版本支持的 eager 配置提前建立通信和初始化基础组件,但“客户端已出现在Dashboard机器列表”不代表所有资源已经出现;资源通常要真正进入并产生统计后才能展示。
启动失败要先分层:
- Starter根本没生效:查BOM、条件装配和类路径。
- 应用启动了但Dashboard无机器:查Dashboard地址、心跳、容器网络和客户端IP。
- 机器在线但看不到资源:查资源是否真正被调用、注解代理是否生效、Web适配器是否注册。
- 资源存在但规则为空:查数据源订阅、DataId/Group/Namespace、JSON转换和RuleManager日志。
六、一次 SphU.entry() 的完整执行链
flowchart TD
A["SphU.entry"] --> B["取得Context"]
B --> C["取得SlotChain"]
C --> D["创建并压入Entry"]
D --> E["逐Slot检查"]
E --> F["执行业务或阻断"]
F --> G["Entry.exit"]
G --> H["完成退出统计"]关键源码主线可以概括为:
SphU.entry
-> CtSph.entryWithPriority
-> ContextUtil.getContext
-> lookProcessChain
-> new CtEntry
-> ProcessorSlotChain.entry
-> 业务代码
-> Entry.exit
-> ProcessorSlotChain.exitentry() 和 exit() 必须成对。漏掉 exit() 不只是少一行清理代码,它会导致当前线程Entry栈、并发线程数、RT和完成统计不正确。手工API必须在 finally 中退出;异步API要在真正完成的回调中退出。
七、Context、Entry和节点树怎样配合
假设同一个 loadProduct 方法分别从开放API和管理后台进入:
flowchart TD
A["不同Context各建路径节点"] --> B["共同关联资源ClusterNode"]NodeSelectorSlot按Context名维护当前资源对应的DefaultNode,所以链路流控能区分不同入口。ClusterBuilderSlot为同一资源关联进程级ClusterNode,并可按origin建立来源统计。Context内部维护当前Entry和父子关系,嵌套资源会形成调用树。ContextUtil.enter(name, origin)中的origin必须由可信方式解析;不能直接相信外部请求随便传入的Header。
如果所有请求都落到默认Context,链路流控就无法准确区分入口。如果Context名使用动态用户ID,又会产生高基数节点。正确做法是使用稳定入口名,例如 public-api、admin-api、scheduled-task。
八、SlotChain真实职责和顺序
Sentinel 1.8.x 的默认链由 DefaultSlotChainBuilder 通过SPI加载并按order排序。典型核心顺序如下;热点参数等扩展模块可插入额外Slot,未来版本也可能演进,因此不能把列表背成永远不变的协议。
| 顺序 | Slot | 真正做什么 |
|---|---|---|
| 1 | NodeSelectorSlot | 按Context构建调用路径节点 |
| 2 | ClusterBuilderSlot | 关联进程内资源聚合节点和origin节点 |
| 3 | LogSlot | 记录规则阻断等日志 |
| 4 | StatisticSlot | 包裹后续Slot,分别记录通过、阻断和退出统计 |
| 5 | AuthoritySlot | 按调用来源执行黑白名单判断 |
| 6 | SystemSlot | 依据入口QPS、线程、RT、Load、CPU等保护系统 |
| 7 | FlowSlot | 执行直接、关联、链路、本地或集群流控 |
| 8 | CircuitBreaker相关Slot | 检查熔断状态,并在请求结束后更新熔断器 |
StatisticSlot 为什么排在规则Slot之前,却只统计“通过请求”?因为它先调用后面的 fireEntry():
flowchart TD
A["进入StatisticSlot"] --> B["先执行后续检查"]
B --> C["分别记录block或pass"]
C --> D["业务完成后exit"]
D --> E["记录RT、异常和线程数"]这是一种“洋葱式”调用。若只按类的排列顺序理解,很容易误以为还没检查规则就已经把请求算作通过。
九、LeapArray滑动窗口内部结构
Sentinel不会为每个毫秒创建对象。LeapArray<T> 把统计周期均分为固定数量小窗口,并使用 AtomicReferenceArray<WindowWrap<T>> 循环复用桶。
设:
统计周期 intervalInMs = 1000ms
桶数量 sampleCount = 2
单桶长度 windowLengthInMs = 1000 / 2 = 500ms窗口下标和开始时间近似为:
timeId = currentTimeMillis / windowLengthInMs
index = timeId % sampleCount
windowStart = currentTimeMillis - currentTimeMillis % windowLengthInMsflowchart TD
A["请求到达并取得当前时间"] --> B["计算环形数组index"]
B --> C["读取对应WindowWrap"]
C --> D["按状态选择复用、CAS创建或加锁重置"]
D --> E["在当前有效桶增加统计"]每个 MetricBucket 会维护通过、阻断、异常、成功、响应时间等计数器。查询最近一段时间指标时,只汇总仍在统计周期内的有效桶。空间复杂度受固定桶数约束,不随运行时间无限增长。
9.1 为什么比固定窗口平滑
固定窗口若在每秒整点清零,00:00:00.900 到 00:00:01.100 的200ms内可能分别吃满两个窗口阈值。滑动窗口通过多个小桶汇总最近区间,降低边界突刺,但桶越细,更新与汇总成本越高;桶越粗,统计越不平滑。
9.2 滑动统计不等于严格全局配额
单机窗口只看当前JVM。三个实例各配置100 QPS,在流量均匀时总通过量大约可到300 QPS;负载不均时某个实例可能先拒绝,而其他实例还有余量。若业务要求全局固定配额,应使用集群流控、网关统一限流或外部配额服务。
十、FlowSlot怎样选择节点和算法
一条FlowRule不只有 resource + count,还包括:
| 字段维度 | 作用 |
|---|---|
resource | 匹配哪个资源 |
grade | 按QPS还是当前线程数判断 |
strategy | 直接、关联或链路控制 |
refResource | 关联资源或入口资源 |
limitApp | 按调用来源控制 |
controlBehavior | 快速失败、Warm Up、匀速排队等整形行为 |
clusterMode | 是否请求集群Token Server判断 |
10.1 QPS限流
直接快速失败模式会读取目标节点最近统计,在“已有通过量 + 本次申请量”超过阈值时拒绝。它适合执行时间短、主要风险是吞吐突增的接口。
阈值默认是单实例阈值。扩容会提高集群总配额,缩容会降低总配额,除非使用集群流控或发布系统动态重算每实例规则。
10.2 并发线程数限流
线程数模式读取当前资源尚未退出的通过请求数。它保护的是并发占用,不是线程池本身:
并发量 ≈ QPS × 平均响应时间秒数若下游从50ms变成5s,即使QPS没变,并发也可能扩大100倍。并发规则能更直接地阻止慢调用继续占满Servlet线程或业务执行器,但它不能替代HTTP连接、读取超时。
10.3 关联和链路流控
- 关联流控:当
refResource压力达到阈值,限制当前资源,常用于让查询为核心写操作让路。 - 链路流控:同一资源从不同Context入口进入时,只限制指定入口链路。
- 两者依赖资源名、Context和调用树准确;Web适配器配置或Context丢失会让规则看起来“不生效”。
十一、Warm Up和匀速排队不是一回事
11.1 Warm Up
Warm Up用于系统刚启动、缓存未热、JIT尚未充分优化、连接池刚建立时逐步放量。其控制器维护类似令牌累积状态,通过 coldFactor、预热时长和目标阈值计算当前可通过速率,随后逐步接近稳定阈值。
它解决“冷系统瞬间吃满目标QPS”的问题,不解决下游已经故障的问题。熔断、客户端超时和容量隔离仍需单独配置。
11.2 匀速排队
Sentinel 1.8.x 的匀速控制器会根据目标速率计算相邻请求的期望通过时间,并维护 latestPassedTime。若预计等待时间不超过 maxQueueingTimeMs,当前调用线程会等待到计划时刻;超过最大等待时间则拒绝。
flowchart TD
A["请求申请"] --> B["计算期望通过时间"]
B --> C["超过最大等待则拒绝"]
C --> D["未超过则预占时间位置"]
D --> E["当前线程等待后执行"]它不是一个独立、持久、可恢复的消息队列。等待期间仍占用调用线程,进程崩溃后等待任务也会丢失。因此:
- 短时、可等待的后台写入可以使用匀速排队。
- 长时间削峰、必须不丢、需要重试的任务应使用Kafka、RocketMQ或RabbitMQ。
- 用户请求总Deadline必须大于排队等待加真实执行时间,否则调用方早已超时而服务端仍在执行。
十二、热点参数限流内部边界
热点参数规则通过额外的参数流控Slot读取 entry() 传入的参数,根据参数索引取得值,再为不同参数值维护独立统计和Token状态。
总QPS 500并不高
其中 sku=A 的QPS 450
其余所有sku合计QPS 50只做资源总限流无法保护 sku=A 对应的缓存分片或数据库行。热点规则可以为普通值设置默认阈值,并为特殊爆款设置例外阈值。
生产注意:
@SentinelResource必须把参数正确传入Sentinel Entry,参数索引从0开始。- 参数类型支持范围受版本和适配器约束,常见基础类型和String最稳妥。
- 用户ID、搜索词等高基数参数会扩大统计缓存,需评估容量和淘汰行为。
- 热点限流只保护进入该规则的调用,不会自动修复Redis热Key、数据库热点行或分片倾斜。
- 参数例外规则属于业务配置,也必须审计和回滚。
十三、熔断器状态机和统计闭环
flowchart TD
A["CLOSED放行并统计"] --> B["指标超阈值后转OPEN"]
B --> C["OPEN期间快速失败"]
C --> D["到期后CAS抢探测资格"]
D --> E["HALF_OPEN仅放行探测调用"]
E --> F["成功回CLOSED,失败回OPEN"]核心状态用原子引用维护。OPEN到期并不意味着所有请求同时放行;通常只有一个请求通过CAS抢到HALF_OPEN探测资格,其余请求继续失败,避免恢复瞬间再次压垮下游。
13.1 三类熔断规则
| 类型 | 统计口径 | 适用风险 |
|---|---|---|
| 慢调用比例 | RT超过慢调用阈值的请求占比 | 下游不报错但越来越慢 |
| 异常比例 | 异常请求数/总请求数 | 持续5xx、连接失败、业务异常 |
| 异常数 | 统计窗口内异常绝对数量 | 低QPS但错误数量有明确红线 |
最小请求数防止小样本误判;统计窗口决定观察范围;熔断时长决定多久后探测。三者必须结合真实QPS、P95/P99和下游恢复时间设置。
13.2 业务异常会自动计入吗
规则阻断产生的是 BlockException,它应计入block,不应当作下游业务异常。业务异常是否进入异常统计取决于适配器是否把Throwable记录到当前Entry:
@SentinelResourceAOP会按配置处理异常并记录。- 手工
SphU.entry()时要显式使用Tracer.traceEntry或给当前Entry记录错误。 exceptionsToIgnore等配置会改变统计口径。- 在另一个线程抛出的异常若没有关联原AsyncEntry,不会自动算到原资源。
如果监控显示下游大量500而Sentinel异常比例始终为0,不能先怀疑熔断算法,应先检查异常有没有被正确Trace、是否已在业务层吞掉、Feign解码后变成了正常返回对象。
十四、系统自适应保护是什么
SystemRule关注整个应用入口,而不是某个业务资源。常见指标包括:
- 系统Load。
- CPU使用率。
- 全局入口QPS。
- 全局并发线程数。
- 全局平均RT。
Load保护不是“load一高就无条件拒绝”的简单开关。经典实现还会结合系统当前入口线程和由 maxQps × minRt 估算的容量边界做类似BBR的判断,减少偶发Load抖动造成误伤。
系统规则是最后防线,不应代替资源级容量规划。整机CPU很低但数据库连接池已满时,系统CPU规则可能完全不触发;仍需对具体数据库、下游调用和慢接口配置资源级限流、Bulkhead和超时。
十五、动态规则怎样进入RuleManager
以FlowRule为例,数据源完成的链路是:
flowchart TD
A["配置中心内容变化"] --> B["DynamicDataSource收到原始文本"]
B --> C["Converter反序列化为FlowRule列表"]
C --> D["SentinelProperty通知PropertyListener"]
D --> E["FlowRuleManager按resource构建索引"]
E --> F["替换本地规则容器"]
F --> G["后续请求读取新规则"]FlowRuleManager.loadRules(newRules) 的语义是用新列表替换旧规则,不是增量追加。空列表、错误DataId或错误反序列化策略可能让保护规则消失。
单个规则管理器会在监听回调中先构建新索引再更新本地容器,但以下事情不是一个全局原子事务:
- FlowRule、DegradeRule、ParamFlowRule和SystemRule分别更新。
- 不同应用实例分别收到通知。
- 配置中心、Dashboard和应用本地可能短暂显示不同版本。
- 规则与代码发布也不是天然原子切换。
生产发布应给规则包增加版本、校验、审批、灰度和回滚。先在少量实例验证block率和业务错误码,再扩大发布;不要直接覆盖全量生产DataId。
十六、Dashboard怎样发现和控制客户端
flowchart TD
A["Sentinel客户端启动"] --> B["启动本地命令服务端口"]
B --> C["周期性向Dashboard发送Heartbeat"]
C --> D["Dashboard记录app、IP、端口和时间"]
D --> E["用户在Dashboard查看资源或规则"]
E --> F["Dashboard调用客户端命令端点"]
F --> G["客户端返回实时指标或更新本地规则"]这解释了几个常见现象:
- 应用到Dashboard是心跳方向,但Dashboard还必须能回连应用客户端端口。
- 在Docker/Kubernetes中上报错误IP,会出现“机器在线但控制台拉不到数据”。
- 多副本环境不能让所有实例抢占相同客户端端口映射。
- Dashboard的机器列表有心跳时效,不是服务注册中心,也不负责业务流量转发。
- 原生Dashboard直接推到客户端内存适合学习,不适合作为唯一生产规则源。
推荐的生产Push模式是让规则管理平台写入Nacos/Apollo等配置中心,再由所有客户端订阅。这样Dashboard或平台是编辑入口,配置中心才是可恢复的规则事实来源。
十七、本地流控与集群流控
17.1 本地流控
每个实例用自己的滑动窗口判断,优点是无网络依赖、延迟低、Token Server故障不影响;缺点是无法严格控制多实例总配额,并受负载不均影响。
17.2 集群流控
集群流控把全局Token判断集中到Token Server。Token Client收到业务请求后向Server申请Token,Server按namespace、resource和ruleId维护全局统计并返回通过或拒绝。
flowchart TD
A["任一实例收到请求"] --> B["Token Client申请Token"]
B --> C["Token Server判断全局配额"]
C --> D["有余量则通过,无余量则拒绝"]Token Server可独立部署,也可由某个应用实例承担,具体能力看版本。引入后新增了网络跳数和控制服务故障域,必须明确:
- Token Server怎样发现、扩容和高可用。
- Client请求超时是多少。
- Server不可达时是回退本地判断、直接拒绝还是放行;策略必须显式验证,不能靠默认值猜测。
- 集群规则怎样持久化,ruleId和namespace是否稳定。
- Token Server本身的CPU、网络、RT和拒绝率怎样监控。
对于支付、开放平台租户配额等严格全局额度,集群流控更合适;对普通接口防雪崩,本地流控通常更稳健。它仍不是计费账本,不能替代数据库中的强一致额度扣减。
十八、注解、Web和Feign适配器做了什么
18.1 @SentinelResource
SentinelResourceAspect 拦截代理方法,在方法前创建Entry,捕获 BlockException 后查找blockHandler,捕获符合条件的业务异常后查找fallback,最终退出Entry。
常见失效原因:
- 同类内部
this.method()自调用绕过Spring代理。 - 方法不是代理可拦截的调用路径。
- blockHandler参数、返回类型或最后的
BlockException不匹配。 - fallback类不是静态方法或类声明方式不符合当前注解约定。
- 业务先catch并返回成功对象,Sentinel看不到异常。
18.2 Web入口
Web适配器通常在过滤器层把URL或路由模板转换为资源,进入 EntryType.IN,并由统一 BlockExceptionHandler 返回429或业务限流错误码。不要把原始、含订单号的URL直接作为资源名,否则产生高基数资源。
18.3 OpenFeign
flowchart TD
A["业务调用Feign接口"] --> B["Feign动态代理"]
B --> C["Sentinel适配器进入资源"]
C --> D["规则阻断则进入FallbackFactory"]
D --> E["通过则由LoadBalancer选择实例"]
E --> F["HTTP客户端执行真实调用"]
F --> G["记录RT、异常和熔断统计"]Sentinel只决定是否允许尝试和怎样记录结果;连接超时、读取超时仍由HTTP客户端负责。Feign、Gateway、客户端和业务层多层重试会放大物理请求数,熔断统计看到的可能是每次Attempt,也可能是最终结果,取决于装饰顺序和适配器位置,必须用Trace和指标证明。
写操作Fallback不能返回伪成功。支付、库存、发货超时后真实结果可能已成功,应返回 UNKNOWN/PENDING_CONFIRMATION 并通过幂等键查询事实或补偿。
十九、异步和响应式调用为什么容易统计错
普通Context主要依赖线程本地状态。提交到线程池、CompletableFuture、Reactor切线程后,原线程的Context不会凭空传播。
Sentinel提供 SphU.asyncEntry() 和 AsyncEntry:创建后会把异步Entry从当前线程栈移除,业务在异步完成回调中记录错误并调用 exit()。这样RT覆盖真正异步耗时,而不是只统计“任务提交用了1ms”。
flowchart TD
A["创建AsyncEntry"] --> B["提交异步任务"]
B --> C["工作线程执行调用"]
C --> D["完成回调记录结果"]
D --> E["AsyncEntry.exit"]错误做法是在请求线程中创建普通Entry,任务刚提交就exit。这样并发数和RT严重偏小,慢调用熔断无法反映真实下游耗时。Reactor项目应使用与当前版本匹配的适配器和上下文机制,不要手工把ThreadLocal对象跨线程复用。
二十、Java 17+与Spring Boot 3.x接入示例
依赖版本由匹配当前Boot 3.x的Spring Cloud Alibaba BOM统一管理:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 使用Nacos持久化Sentinel规则时还需要匹配版本的数据源适配器 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>示例配置的属性名适用于常见Starter结构,落地时必须用当前依赖版本的配置元数据核对:
spring:
cloud:
sentinel:
eager: true
transport:
dashboard: sentinel-dashboard.monitoring.svc:8858
port: 8719
datasource:
flow:
nacos:
server-addr: nacos.infrastructure.svc:8848
namespace: production
group-id: SENTINEL_GROUP
data-id: order-service-flow-rules
data-type: json
rule-type: flowJava 17示例明确用 record 表示响应状态:
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class InventoryController {
@GetMapping("/inventories/{sku}")
@SentinelResource(
value = "inventory-query",
blockHandler = "blocked",
fallback = "failed")
public InventoryResult inventory(@PathVariable String sku) {
if (sku.startsWith("FAIL")) {
throw new IllegalStateException("remote inventory unavailable");
}
return new InventoryResult(sku, 12, "CONFIRMED", null);
}
public InventoryResult blocked(String sku, BlockException cause) {
return new InventoryResult(sku, null, "RATE_LIMITED",
cause.getClass().getSimpleName());
}
public InventoryResult failed(String sku, Throwable cause) {
return new InventoryResult(sku, null, "UNKNOWN",
cause.getClass().getSimpleName());
}
public record InventoryResult(
String sku, Integer quantity, String status, String reason) {
}
}RATE_LIMITED 表示调用根本没进入业务,调用方可按Retry-After和幂等性决定是否重试;UNKNOWN 表示业务调用失败或结果不确定,不能等价成“库存为0”。这段代码使用Java 17的record,不能复制到JDK 8项目。
二十一、JDK 8可运行Demo:直接观察通过和阻断
依赖示例:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.8</version>
</dependency>import java.util.Collections;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
public final class SentinelCoreDemo {
public static void main(String[] args) {
FlowRule rule = new FlowRule();
rule.setResource("inventory-query");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(2.0d);
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
FlowRuleManager.loadRules(Collections.singletonList(rule));
int passed = 0;
int blocked = 0;
for (int i = 0; i < 6; i++) {
Entry entry = null;
try {
entry = SphU.entry("inventory-query");
passed++;
} catch (BlockException ex) {
blocked++;
} finally {
if (entry != null) {
entry.exit();
}
}
}
System.out.println("passed=" + passed);
System.out.println("blocked=" + blocked);
}
}在同一统计时段连续请求6次、QPS阈值为2时,典型结果是前2次通过、后4次被阻断。这个Demo用于观察Core主链;生产阈值、并发和时间分布必须通过压测验证。
二十二、商业场景:订单提交、库存与营销查询
订单系统有三类完全不同的保护目标:
| 资源 | 主要风险 | 建议治理 |
|---|---|---|
submit-order | 核心写链路,重复提交和数据库容量 | 网关租户限流、服务本地并发保护、幂等键,不做伪成功Fallback |
lock-inventory | 下游变慢导致上游线程堆积 | HTTP超时、慢调用熔断、Bulkhead、结果未知查询 |
promotion-query | 大促突发流量但可降级 | Warm Up、QPS限流、缓存和空优惠降级标记 |
product-detail | 少量爆款SKU形成热点 | 热点参数限流、Redis热Key治理、本地缓存 |
容量设计示例:单实例压测在P99小于200ms时稳定承受300 QPS,部署4实例,安全系数0.7:
估算总安全吞吐 = 300 × 4 × 0.7 = 840 QPS若使用本地限流,可先按每实例约210 QPS配置,但必须考虑负载不均和滚动发布时实例数变化;若业务合同规定租户全局最多800 QPS,应在网关或集群流控做全局配额,数据库仍保留最终业务约束。
订单提交总Deadline为1500ms时,不能让Sentinel排队1000ms后再由Feign读取超时1000ms,更不能外层和内层各重试3次。预算要从入口向下游递减,并记录每个逻辑请求对应的物理Attempt数。
二十三、必须认识的失败窗口
| 失败窗口 | 表面现象 | 真正风险 | 恢复原则 |
|---|---|---|---|
| Dashboard不可达 | 看不到机器或规则 | 临时运维入口失效,本地旧规则仍可能运行 | 查心跳与回连,不要先重启全部应用 |
| 配置中心不可达 | 新规则不下发 | 实例规则版本分裂 | 保留最后可用规则、告警版本差、恢复后校验收敛 |
| 错误规则发布为空列表 | block突然归零 | 保护能力整体消失 | 版本化规则、非空校验、快速回滚 |
| 阈值误改过小 | 大量FlowException | 正常流量被误伤 | 灰度规则、按实例看block率、回滚版本 |
| Token Server不可达 | 集群规则超时或行为变化 | 全局配额失控或全拒绝 | 预先验证failover策略并演练 |
| 异常被业务吞掉 | 下游500但不熔断 | Sentinel统计口径失真 | 检查Tracer、Fallback和统一返回体 |
| 匀速排队过长 | 服务端线程等待 | 调用方超时后服务端仍执行 | 统一Deadline,长削峰改MQ |
| 扩容后本地阈值未重算 | 总通过量上升 | 下游被扩容流量打垮 | 集群配额或自动按容量计算规则 |
| 动态资源名爆炸 | 内存和指标上涨 | 新资源链无法正常创建 | 使用模板化低基数资源名 |
| 异步提前exit | RT很低但下游很慢 | 熔断永远看不到真实慢调用 | 使用AsyncEntry并在完成回调退出 |
二十四、线上大量Block或熔断Runbook
24.1 先确定谁拒绝了请求
- 从统一错误码、异常类和日志区分
FlowException、DegradeException、ParamFlowException、SystemBlockException、AuthorityException。 - 记录应用、实例、资源名、规则版本、ruleId、origin、入口Context和traceId。
- 确认请求有没有真正到业务方法、Feign和下游,避免把Block当成下游500。
24.2 再确定为什么命中
flowchart TD
A["大量请求被Block"] --> B["先识别具体异常类型"]
B --> C["再检查该规则对应指标"]
C --> D["对照实例、规则版本和发布时间"]
D --> E["判断误发布还是真实过载"]24.3 必须收集的证据
- 最近15分钟QPS、pass QPS、block QPS和线程数。
- RT平均值、P95、P99、慢调用比例、异常比例。
- 应用实例数、各实例流量分布、发布和扩缩容事件。
- Servlet/Netty线程、业务线程池、HTTP连接池、数据库连接池。
- Feign/Gateway/客户端重试次数和物理Attempt。
- Dashboard机器心跳、客户端命令端口、规则源版本与实例本地规则。
- 下游慢SQL、GC暂停、CPU节流、网络错误和依赖限流。
24.4 恢复顺序
- 若是错误规则发布,回滚到已验证规则版本,不要手工逐实例修改内存。
- 若是真实流量超过容量,先保护核心链路、关闭非核心入口、停止异常重试源。
- 若是下游变慢,限制并发和重试,保持熔断,修复下游瓶颈后小流量探测。
- 若需要扩容,确认下游数据库和配额也能承受,并处理本地阈值随实例数放大的问题。
- 恢复后验证所有实例规则版本一致、block率回落、业务成功率和下游容量稳定。
二十五、监控不能只看Block QPS
| 指标或证据 | 要回答的问题 |
|---|---|
| pass/block QPS | 规则是否在工作,拒绝比例是多少 |
| 当前线程数 | 慢调用是否导致并发占用 |
| RT与慢调用比例 | 是否接近熔断阈值 |
| 异常数与异常比例 | 业务错误是否被正确记录 |
| CircuitBreaker状态 | 哪个资源何时OPEN/HALF_OPEN/CLOSED |
| 规则版本和更新时间 | 是否刚发布错误规则 |
| 客户端心跳和命令端口 | Dashboard是否能发现并回连客户端 |
| Token Server RT/错误 | 集群流控是否成为新瓶颈 |
| Fallback调用量 | 是否把真实失败隐藏成表面成功 |
指标要按应用、实例、资源、结果类型和规则版本拆分,但标签不能使用订单号、用户ID等高基数值。日志中也不要输出Token、身份证、手机号等敏感数据。
二十六、常见错误与后果
| 错误做法 | 为什么错 | 后果 |
|---|---|---|
| 把Sentinel当线程池 | 它主要做统计和准入判断 | 接受的慢请求仍可能耗尽连接池 |
| 所有资源都配置同一个QPS | 不同接口成本不同 | 核心接口被误伤或昂贵接口无保护 |
| blockHandler返回正常成功码 | 规则阻断被伪装为成功 | 上游停止补偿,业务事实错误 |
| 只在Dashboard内存改规则 | 重启后不可恢复 | 保护能力漂移或丢失 |
| 用订单号作为资源名 | 资源和SlotChain高基数 | 内存上涨、监控不可用、规则失配 |
| 匀速排队代替MQ | 调用线程等待且不持久 | 超时、重启丢失和线程耗尽 |
| 只看平均RT | 长尾被平均值掩盖 | P99恶化后才发生雪崩 |
| 熔断后无限重试 | OPEN本应快速失败 | 重试风暴和恢复探测被淹没 |
| 扩容时保持“集群总阈值”配置在每实例 | 每台都按总量放行 | 下游流量随实例数成倍放大 |
| 手工Entry不在finally退出 | 统计生命周期不闭合 | 线程数、RT和调用树失真 |
二十七、关联知识点
- Sentinel基础、规则和Starter用法
- Sentinel独立面试题
- Hystrix内部原理与Resilience4j迁移
- Gateway内部链路与生产治理
- 动态配置运行时安全
- 微服务稳定性治理
- 分布式限流
- OpenFeign与完整服务调用链
- Spring Cloud Alibaba全景
真正理解Sentinel的标准不是“会在Dashboard点一条规则”,而是能从一次Entry的生命周期解释统计从哪里来、规则在哪个Slot判断、控制面怎样把版本化规则送到每个实例,以及规则、网络和下游同时异常时怎样保住核心业务并恢复。
