选错拒绝策略会导致背压失效:AbortPolicy甩锅上游引发级联失败,CallerRunsPolicy虽具背压但可能卡住HTTP线程,DiscardPolicy静默丢数据;正确做法是API层用CallerRunsPolicy+熔断限流,内部调用自定义策略+同步重试+告警,且拒绝策略中禁止IO操作。

拒绝策略选错,背压就变成甩锅
AbortPolicy 是默认策略,但直接抛 RejectedExecutionException 并不等于实现了背压——它只是把压力原样甩给上游,调用方没做兜底时会 cascade fail。真正起背压作用的是 CallerRunsPolicy:它让提交线程自己执行任务,天然拖慢生产速度。但要注意,如果调用方是 Tomcat 的 http-nio-8080-exec 线程,就会导致 HTTP 请求线程被占用,响应延迟飙升甚至超时。
常见错误现象:
- 用
DiscardPolicy后监控告警没响,但业务数据对不上 - 在高并发接口里配了
CallerRunsPolicy,结果全链路 P99 延迟翻倍 - 自定义拒绝策略里调用了阻塞日志或远程上报,反而加剧线程阻塞
实操建议:
- 对外暴露的 API 接口层,优先用
CallerRunsPolicy,但必须搭配熔断(如 Sentinel)和请求限流(如 Spring Cloud Gateway 的RequestRateLimiter) - 内部服务间调用,可考虑自定义策略:记录指标 + 降级为同步重试(最多 1 次)+ 触发 Prometheus 告警
- 拒绝策略里禁止任何 IO 操作;日志只打 warn 级别,且加采样(如每 100 次打 1 条)
队列长度不是越大越好,而是要卡住“可观测窗口”
设 ArrayBlockingQueue 容量为 1000,看起来很宽裕,但如果任务平均耗时 200ms、核心线程数只有 4,那队列填满只要不到 5 秒——你根本来不及发现和干预。所谓“可观测窗口”,是指队列从空到满之间,留出足够时间让监控系统捕获、告警、人工介入。
计算公式很简单:queueSize ≈ (maxThroughput × avgTaskTime) × 1.5,其中 maxThroughput 是你期望支撑的峰值 QPS,avgTaskTime 是 P95 耗时(不是平均值)。比如目标 200 QPS、P95 耗时 150ms,则队列长度建议设为 200 × 0.15 × 1.5 = 45,向上取整到 64 或 128。
关键差异点:
- 用
LinkedBlockingQueue不设容量 → 内存持续增长,OOM 前无征兆 - 用
SynchronousQueue→ 队列长度为 0,所有压力立刻转为线程创建,容易触发OutOfMemoryError: unable to create new native thread - Spring Boot 默认的
ThreadPoolTaskExecutor配置里,queueCapacity缺省是Integer.MAX_VALUE,等同于无界队列,务必覆盖
如何让线程池自己“感知”过载并缩容
纯靠拒绝策略和固定队列长度,只能被动响应;真正的自适应背压需要线程池能主动反馈。JDK 原生不支持,但可以通过组合手段实现:用 Micrometer 暴露 ThreadPoolExecutor.getQueue().size() 和 getActiveCount() 指标,再配合 Prometheus 的 rate(thread_pool_queue_size[1m]) > 0.8 告警,驱动配置中心动态调整 corePoolSize 和 maxPoolSize。
不过要注意,线程数不能频繁扩缩——每次 addWorker / interruptIdleWorkers 都有开销。更稳妥的做法是分档位预设:
- 低水位(队列使用率 corePoolSize = 4
- 中水位(30% ~ 70%):提升至
corePoolSize = 8,避免频繁扩容 - 高水位(> 70%):触发降级开关,关闭非核心功能(如日志采样、异步通知)
这个逻辑不能写死在线程池里,而应由外部控制面(如 Spring Cloud Config + Actuator Endpoint)统一调度。
微服务间背压链路断裂的典型信号
单个服务的线程池调优再好,如果上下游没有对齐背压语义,整个链路还是脆的。最常被忽略的是 HTTP 客户端侧:Feign 默认用 JDK HttpURLConnection,连接池大小固定、无拒绝回调;RestTemplate 默认无超时、无熔断。这意味着下游服务已开始拒绝任务,上游却还在疯狂重试,形成雪球效应。
必须检查的 3 个点:
- Feign 客户端是否配置了
feign.client.config.default.connectTimeout和readTimeout(建议 ≤ 2s) - 是否启用了
RetryableException白名单,避免对503 Service Unavailable重试 - 下游返回
Retry-AfterHeader 时,上游是否解析并休眠(很多 SDK 默认忽略)
真正的自适应背压,从来不是某个组件的参数调优,而是从网关、API 层、RPC 客户端、线程池、数据库连接池,一层层把“忙不过来”的信号透传上去。漏掉任意一环,压力就会在某处堆成山。

















