首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >批量查询任务高峰期全挂:线程池、信号量与队列背压的治理记录

批量查询任务高峰期全挂:线程池、信号量与队列背压的治理记录

原创
作者头像
用户9660458
发布于 2026-10-08 10:42:42
发布于 2026-10-08 10:42:42
220
举报

批量查询任务高峰期全挂:线程池、信号量与队列背压的治理记录

上午十点,运营那边固定启动一批物流单号批量查询,平时一两分钟跑完。那天十点零二分,监控先报警"任务队列积压超过 5000",又过了半分钟,批量查询接口开始大面积超时,连正常的前台单笔查询都跟着卡了。

我第一反应是加机器,但看了线程池的监控数据后放弃了——问题不是机器不够,是并发治理的阀门全开错了。

一、先看线程池是不是"假扩容"

批量查询走的是一个固定线程池,配置是这样的:

代码语言:txt
复制
ThreadPoolExecutor(
  corePoolSize = 8,
  maximumPoolSize = 32,
  keepAliveTime = 60s,
  workQueue = new LinkedBlockingQueue<>(10000)
)

这个配置有个经典的坑:LinkedBlockingQueue 无界(或极大)时,线程数永远不会从 8 涨到 32。线程池的扩容逻辑是"队列满了才新建线程",队列能装 10000 个任务,线程就一直停在 8 个。高峰期 8 个线程每人要处理几千个查询,单个查询再慢一点,队列就全线积压。

踩坑提醒:很多人以为设了 maximumPoolSize = 32 就是 32 个线程跑,实际上队列没满前线程池根本不会扩容。要看线程池真实并发数,监控 getActiveCount(),别只看配置。

二、队列背压:有界队列 + 拒绝策略

修复第一步是把无界队列改成有界,让压力在入口就显现出来,而不是积压在内存里:

代码语言:txt
复制
workQueue = new ArrayBlockingQueue<>(512)

队列从 10000 压到 512 后,高峰期线程池会真的扩容到 32 个线程;队列再满,任务被拒绝,由拒绝策略决定怎么办。我们用的是 CallerRunsPolicy——被拒绝的任务由提交它的线程自己跑,这样既不丢任务,又天然给上游减速。

代码语言:txt
复制
ThreadPoolExecutor pool = new ThreadPoolExecutor(
  8, 32, 60L, TimeUnit.SECONDS,
  new ArrayBlockingQueue<>(512),
  new ThreadPoolExecutor.CallerRunsPolicy()
);

踩坑提醒:CallerRunsPolicy 有个隐蔽副作用——如果提交任务的线程本身就是批量任务主线程,被拒绝的任务会在主线程里串行执行,批量任务耗时会被拉长。我们在批量场景里给主线程也加了监控,发现执行耗时从 40 秒涨到 90 秒,排查后确认是这个原因,于是改成 AbortPolicy + 显式降级(把多余任务写进本地重试表,下一轮再跑),而不是在主线程硬扛。

三、信号量控制对上游 API 的并发,而不是依赖线程数

批量查询的核心瓶颈不在我们自己的线程,而在上游物流渠道 API 的限流:对方网关对单 appkey 的并发上限是 20。线程池 32 个线程同时打过去,一半请求直接 429。

正确做法是用信号量把"对外并发"和"内部线程数"解耦:

代码语言:txt
复制
Semaphore upstreamLimit = new Semaphore(18); // 留 2 个余量给重试
for (Task task : batchTasks) {
  upstreamLimit.acquire(); // 拿不到就阻塞等待
  pool.submit(() -> {
    try {
      callUpstream(task);
    } finally {
      upstreamLimit.release();
    }
  });
}

信号量 18 保证对上游的并发永远不超过 18,线程池 32 只是内部并行度,两者不再互相打架。改完后 429 全部消失,批量查询耗时从 2 分半回到 1 分 20 秒。

踩坑提醒:acquire() 要放在提交任务前而不是任务内,否则高峰期 32 个线程全卡在 acquire 上,线程池被占满,其他任务也没法执行。

四、5 个我实际踩过的坑

  • 坑1:LinkedBlockingQueue 无界队列,线程池永不扩容,8 线程扛全部流量
  • 坑2:CallerRunsPolicy 让批量主线程串行执行被拒任务,耗时翻倍
  • 坑3:信号量 acquire 放在任务内,高峰期线程池被 acquire 阻塞占满
  • 坑4:没监控 getActiveCount(),一直以为 32 线程在跑,实际只有 8
  • 坑5:上游并发上限当成线程数直接用,429 大量出现

批量查询平台是我们基于固乔快递批量查询助手的接口做的改造,我负责并发治理这部分,把无界队列、信号量和拒绝策略理顺后,十点高峰那波批量查询没有再全挂过。

复盘要点

  • 线程池真实并发看 getActiveCount(),配置的 maximumPoolSize 只是上限,不是常态
  • 有界队列 + 显式拒绝策略让压力在入口显形,比无界队列"假装能扛"安全得多
  • 对外部 API 的并发要用信号量单独控制,和内部线程数解耦,429 问题才能根治

以上是个人实践记录,各平台具体功能以官方实时信息为准。

留个开放问题:批量任务场景下,你们对 CallerRunsPolicy 和"重试表 + 下一轮补偿"这两种拒绝策略是怎么权衡的?

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 批量查询任务高峰期全挂:线程池、信号量与队列背压的治理记录
    • 一、先看线程池是不是"假扩容"
    • 二、队列背压:有界队列 + 拒绝策略
    • 三、信号量控制对上游 API 的并发,而不是依赖线程数
    • 四、5 个我实际踩过的坑
    • 复盘要点
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档