题目:核心线程打满且队列无界,接口 P99 从 50ms 涨到 3s,如何定位并做不停机调优?
难度:high
标签:java/threadpool, java/concurrency
评分:⭐⭐⭐⭐☆(4/5)
当前观测到的核心问题是p99的上升,先通过监控确认具体是某个端口p99暴增还是所有接口都暴增;
查看gc情况是否有大量gc;
如果有skywalking之类链路追踪工具,查看下接口的时间具体消耗在哪里了;
如果是某个或者某几个接口出现p99那么是这个接口中线程调度阻塞了太多的任务导致无法快速完成;
如果是系统中某台服务所有接口都慢,那么应该堆积的队列太大导致大量gc;
不停机调优方面看具体问题,看具体有哪些工具,如果有nacos并且对线程池做了动态配置,可以通过nacos调整线程池配置,增加核心线程数量,选择有界队列进行阻塞队列配置,队列长度按照1000/50核心线程数2,来获取,然后最大线程数配置为核心线程数*2用来进行冗余;核心线程数量需要通过qps来进行评估;
如果线程池配置不是动态参数的,那么就需要先增加机器承担请求量,保证能平稳承载业务后再进行调整
优点:排查顺序专业——先界定范围(单接口 vs 全站)→ 看 GC → 链路追踪定位 → 区分线程池排队与下游依赖;调优有"动态优先、扩缩容兜底"的工程意识;队列公式有量化意图
不足:① 最关键指标漏了:线程池 activeCount、queue.size、任务排队等待时间(getQueue().size() + Micrometer 的 executor.queued),P99 涨到 3s 往往就是排队时间,不是执行时间;② 未提 jstack/arthas thread 看线程状态分布(RUNNABLE 卡在 IO?BLOCKED 锁竞争?);③ 队列公式量纲不对(1000/50 是 QPS?应该是 Little's Law:队列容量 ≈ QPS × 可接受排队时长);④ 未提有界队列 + 拒绝策略(CallerRuns 反压 or 快速失败)与线程池隔离(核心/非核心业务分开池);⑤ 未提下游依赖(DB 连接池、HTTP 超时)才是常见真因;⑥ 无界队列下 maximumPoolSize 永远不生效,这点应作为常识点出
补充:无界队列三大害——max 参数失效、OOM 风险、延迟不可观测;正确姿势:有界队列 + 明确拒绝策略 + 排队时长监控 + 舱壁隔离
核心考点定位:能否用"排队时间 vs 执行时间"切分延迟,并给出可观测、可回滚的调优路径。
知识点拆解:
LinkedBlockingQueue 默认容量 Integer.MAX_VALUE,线程池永远只用到 corePoolSize,maximumPoolSize 形同虚设;任务无限堆积 → 延迟线性上涨 → 最终 OOMP99 总延迟 = 排队时间 + 执行时间 + 下游等待;先用 executor.queued、executor.active、任务提交到开始的耗时定位是哪一段thread -n 10 / jstack 看栈 → SkyWalking/Arthas trace 看下游 RT → GC 日志排除 STWL = λ × W——队列容量 ≈ 目标 QPS × 可接受排队秒数;例如 200 QPS、最多等 0.5s → 100ThreadPoolExecutor.setCorePoolSize/setMaximumPoolSize 可在运行期调整(注意 setMaximumPoolSize 小于 core 会抛异常);Nacos/Apollo 监听 + 参数校验 + 灰度;不能动态时先扩容机器兜底CallerRunsPolicy(反压)或自定义快速失败,禁止 DiscardPolicy 静默丢任务按「底层原理 → 设计权衡 → 工程实践」递进排列。
考察方向:
延伸递进面试题:
SynchronousQueue 或小容量队列CallerRunsPolicy 有什么副作用?【一句话结论】
面试官您好,P99 从 50ms 涨到 3s,第一反应不是加线程,而是先确认这 3 秒里有多少是排队等出来的。
【三点展开】
第一,先做延迟拆解。看线程池的 activeCount、queue.size 和任务从提交到开始执行的耗时:如果排队时间占了 2.9 秒,那就是容量问题;如果排队很短、执行很长,就往下游查——DB 连接池、HTTP 超时、锁竞争。同时用 arthas 的 thread 看线程栈,确认是卡在 IO 还是 BLOCKED。
第二,根因大概率是无界队列。LinkedBlockingQueue 不传容量就是 Integer.MAX_VALUE,线程池永远只用 core,maximumPoolSize 根本不生效,任务无限堆积,延迟线性上涨,最后 OOM。所以第一刀是把队列改成有界,容量按 Little's Law 算:QPS 乘上你能接受的最大排队秒数,比如 200 QPS 最多等 0.5 秒就是 100。
第三,调优要能回滚。core/max 支持运行期 set,接 Nacos 做动态配置并加参数校验和灰度;拒绝策略别用 Discard,选 CallerRuns 形成反压或者自定义快速失败。同时把核心业务和旁路业务分池隔离,避免一个慢接口拖垮全站。参数改不了就先扩容机器兜底,稳定后再调。
【一句边界】
当然,如果下游本来就扛不住,扩线程只会把压力更快地打过去——先限流降级保住下游,再谈线程池参数;调参是最后一步,不是第一步。