发布时间:2026/8/26 17:45:15
接口只慢了500ms,为什么200个Tomcat线程还是被打满? 接口只慢了500ms为什么200个Tomcat线程还是被打满CPU只有35%堆内存也很稳定。但接口突然开始批量超时Nginx不断出现504。监控里最刺眼的一条曲线是Tomcat busy threads 200我们配置的最大工作线程正好也是200。第一反应通常是线程不够那就从200改成500。但这次故障最反直觉的地方是业务代码没有死循环也没有执行几十秒。只是一个下游接口的P95耗时从100ms涨到了600ms。看起来只多了500ms却足以让200个Tomcat线程全部被占满。真正原因可以用一个很简单的估算看出来并发占用 ≈ 请求速率 × 请求耗时在400 QPS下400 × 0.1秒 40个并发请求 400 × 0.6秒 240个并发请求接口只慢了500ms需要同时处理的请求却从约40个增加到了约240个。Tomcat只有200个工作线程线程池当然会被吃满。说明文中的流量、耗时、接口和线程数量均为简化后的脱敏示例。计算公式用于快速估算最终容量仍应以压测、真实耗时分布和业务峰值为准。线上开放Actuator前请做好鉴权和网络隔离。一、故障现场机器不忙接口却像卡死了事故发生前服务运行得很平稳请求量约400 QPS 接口P95约100ms Tomcat busy35到55 CPU30%左右 错误率接近0某次下游服务发布后接口P95逐渐升高10:05 P95 180msbusy 75 10:08 P95 320msbusy 128 10:11 P95 610msbusy 200 10:12 Nginx开始出现504但同一时间CPU没有跑满 GC暂停没有明显增加 Java堆没有持续上涨 数据库慢SQL数量基本正常如果只看CPU和堆内存会觉得这台机器还有大量余量。实际上请求处理能力已经耗尽了。线程不是只有“消耗CPU”时才算被占用等待数据库、Redis、HTTP响应和文件I/O时它同样不能处理下一个同步请求。二、为什么500ms能把200个线程吃光对于同步Servlet请求一个请求在完成之前通常会持续占用一个Tomcat请求处理线程。可以用下面的关系做第一轮估算平均并发数 ≈ 吞吐量 × 平均响应时间假设流量稳定在400 QPS。正常时400次/秒 × 0.1秒 40大约需要40个并发执行位置。下游变慢后400次/秒 × 0.6秒 240理论并发占用已经超过200。如果还有重试、突发流量和长尾请求实际压力会更高。这里还要注意两个问题。1. P95不是平均值上面的公式严格说使用的是平均吞吐和平均停留时间。生产环境做容量预估时可以用峰值请求率和较保守的耗时做粗略上界但不能把一次乘法当成最终压测结论。2. 平均耗时会掩盖长尾即使平均耗时只有200ms只要一部分请求等待3秒、5秒仍然可能长期占住大量线程。所以真正需要一起看的是QPS P50、P95、P99 Tomcat busy/max 错误率和超时数 下游调用耗时三、先分清maxThreads、maxConnections和acceptCount这三个参数经常被混在一起。Spring Boot常见配置是server:tomcat:threads:max:200min-spare:10max-connections:8192accept-count:100具体默认值应以当前Spring Boot和Tomcat版本为准不要从旧博客复制参数。1. threads.maxserver.tomcat.threads.max决定Tomcat最多创建多少个请求处理工作线程。在传统平台线程模型下它近似决定同时执行多少个同步请求。Tomcat官方文档中的默认值是200。如果配置了共享ExecutorConnector自己的线程参数可能被忽略如果启用了虚拟线程Spring Boot当前文档也明确说明该属性不会产生同样作用。2. max-connections它限制Tomcat同时接受和处理的连接数量。连接数不等于正在执行的请求数。HTTP Keep-Alive连接可以存在但此刻没有占用一个请求处理线程执行Controller。3. accept-count当连接处理能力继续达到上限时操作系统还能保留多少等待建立处理的连接和Connector的积压队列有关。队列也满以后新连接可能被拒绝或超时。整个拥塞过程可以简化为请求进入 → 工作线程逐渐用满 → 已接收连接等待线程 → 连接数达到maxConnections → 操作系统backlog继续排队 → 队列也满 → 连接拒绝或客户端超时因此accept-count不是用来提高业务吞吐量的。它只能让更多请求在门口等一会儿。四、先把Tomcat线程指标打开如果使用Spring Boot Actuator可以先确认Tomcat的MBean Registry是否启用server:tomcat:mbeanregistry:enabled:true并只在受保护的管理网络暴露必要端点management:endpoints:web:exposure:include:health,metrics先列出当前版本实际提供的指标curl-sShttp://127.0.0.1:8080/actuator/metrics\|jq-r.names[]\|grep^tomcat\.常见指标包括tomcat.threads.current tomcat.threads.busy tomcat.threads.config.max tomcat.connections.current不同Spring Boot、Tomcat和Micrometer版本的具体名称可能不同应以/actuator/metrics返回结果为准。查看忙线程curl-sS\http://127.0.0.1:8080/actuator/metrics/tomcat.threads.busy查看最大线程数curl-sS\http://127.0.0.1:8080/actuator/metrics/tomcat.threads.config.max这次事故里两条曲线形成了明显平台busy 200 max 200一旦busy/max长时间接近100%就不是一次普通抖动了。如果你的团队还只监控CPU和JVM堆可以把这一节转给负责监控的同事。没有Web线程池指标很多接口拥塞只能等用户报错以后才知道。五、连续抓三份线程栈不要只看一张快照指标只能证明线程池满了不能告诉你线程在等什么。先找到进程PID$(pgrep-forder-service.jar|head-n1)连续抓三份线程栈foriin123;dojcmd$PIDThread.print-l\/tmp/thread-$PID-$(date%H%M%S).txtsleep10doneOracle文档将Thread.print标记为中等影响实际影响与线程数量有关。线程很多或机器已经濒临失控时不要无限频率执行。先统计Tomcat请求线程grep-h^http-nio-.*-exec-/tmp/thread-$PID-*.txt\|sed-Es/^([^]).*/\1/\|sort-u|wc-l再搜索重复调用栈grep-n-A25^http-nio-.*-exec-\/tmp/thread-$PID-*.txt\|grep-ESocketDispatcher|HttpClient|RestTemplate|Feign|Hikari|PreparedStatement不要只按线程状态判断。网络读取线程在不同JDK、客户端和调用路径中可能表现为RUNNABLE、WAITING或其他状态。真正有价值的是大量Tomcat线程 在连续三份快照中 反复停留在同一类调用栈这次三份线程栈里超过160个Tomcat线程都停在同一个下游HTTP读取路径http-nio-8080-exec-173 at sun.nio.ch.SocketDispatcher.read0(...) at org.apache.hc.core5.http.impl.io.SessionInputBufferImpl... at org.springframework.web.client.RestTemplate... at com.example.order.RiskClient.check(...)根因范围已经从“整个Spring Boot服务”缩小到了一个风险校验接口。六、把线程栈和接口耗时对上时间线只看线程栈还不够。接下来把这些数据放到同一条时间线上Tomcat busy线程 入口接口QPS和P95/P99 下游HTTP耗时 连接超时、读取超时 重试次数 Nginx upstream耗时最终发现风险服务P95从80ms涨到580ms 订单接口P95从100ms涨到610ms 入口QPS仍维持在380到420 Tomcat busy从50左右上涨到200 HTTP客户端读取超时配置为3秒 失败请求还会自动重试一次请求链路实际变成风险服务变慢 → 订单请求线程同步等待 → 同时占用的Tomcat线程增加 → busy达到200 → 新请求开始等待工作线程 → 排队进一步抬高端到端耗时 → 客户端重试带来更多请求 → 服务进入正反馈式拥塞真正危险的不是那500ms本身。而是耗时增加 × 持续流量 × 同步等待 × 自动重试它们一起把并发占用放大了。七、为什么CPU只有35%因为大部分线程没有在计算。它们正在等待下游HTTP响应 数据库连接 SQL锁 Redis返回 文件或网络I/O等待中的线程仍然占据Tomcat工作线程名额但不会持续消耗一个CPU核心。所以完全可能同时看到CPU不高 Tomcat busy满 接口大量超时这也是为什么“机器还有一半CPU”不能证明服务还有一半吞吐余量。对于同步Web服务容量至少要同时看CPU 工作线程 连接池 下游并发 请求耗时任何一个先达到瓶颈整条链路都会变慢。八、为什么把200改成500通常不是第一修复假设下游最多只能稳定处理80个并发请求。把Tomcat线程从200改成500之后会有更多请求同时压向下游 数据库和HTTP连接池竞争更严重 线程栈与调度开销增加 失败前排队时间更长 故障扩散范围更大短期看busy不再等于max。但下游可能更快被打垮。这和把连接池从30扩大到60很像如果根因没有消失扩容只是允许更多请求一起等待。什么时候可以调整最大线程数至少先确认CPU和内存有余量 下游能够承受更高并发 连接池容量匹配 超时和重试已经受控 压测验证吞吐确实提升 故障时不会形成超长队列线程数是容量参数不是急救按钮。九、真正的修复分四层1. 给下游调用设置完整超时至少区分连接超时 读取或响应超时 整次调用超时超时必须小于上游允许的总响应时间并为重试、降级和网络传输留下预算。不要让Tomcat线程无限期等待一个已经失去意义的响应。2. 限制单个下游的并发为风险服务设置独立的并发上限或Bulkhead。例如整个Tomcat有200个线程也不应该允许其中190个同时卡在同一个下游。并发隔离可以把故障控制在局部风险服务最多占用40个并发 超过上限快速失败或走降级 其余线程仍能处理查询、健康检查和其他接口3. 谨慎重试重试只适合短暂、可恢复并且幂等的失败。当下游已经变慢时无条件立即重试会制造额外流量。需要明确哪些错误允许重试 最多重试几次 退避和抖动时间 是否有整体调用截止时间 是否会产生业务重复4. 准备降级和缓存对于非核心校验、展示信息或可接受短暂旧数据的场景可以考虑本地或分布式缓存 返回默认值 异步补偿 排队处理 直接拒绝并提示稍后重试降级不是把异常吞掉而是在依赖失效时保住核心链路。十、修复后怎么验证修复不能只看“接口又能访问了”。需要重新压测同一个故障场景。1. 注入下游延迟在测试环境把下游耗时从100ms逐步提高到200ms 400ms 600ms 1000ms观察入口服务是否出现无界排队。2. 观察线程池是否能够回落重点看tomcat.threads.busy tomcat.threads.config.max 入口P95、P99 下游并发数 超时和拒绝数量下游恢复后busy应该能够快速回落而不是长时间贴着max。3. 验证降级不会拖死其他接口故障期间同时请求风险校验相关接口 普通查询接口 健康检查接口 核心写入接口确认单个下游故障不会占满整个Web线程池。4. 验证重试流量统计入口请求数 下游实际调用数 重试次数 最终成功率如果入口400 QPS下游却被调用700次重试策略本身可能正在放大事故。十一、Tomcat线程池打满排查清单遇到“CPU不高但接口批量超时”可以按这个顺序1. 确认QPS、P50、P95和P99是否同步变化 2. 查看Tomcat busy、current和max线程 3. 区分maxThreads、maxConnections和acceptCount 4. 连续抓三份线程栈不只看单次快照 5. 找出大量请求线程共同停留的调用路径 6. 对齐数据库、Redis、HTTP和文件I/O耗时 7. 检查连接超时、读取超时和总调用超时 8. 检查重试是否放大下游流量 9. 检查单个依赖是否有并发隔离 10. 不要把调大maxThreads当成第一修复 11. 用峰值流量与耗时估算并发需求 12. 在故障注入和压测中验证降级效果真正需要回答的不是200个线程够不够而是这200个线程为什么长期回不来写在最后接口从100ms变成600ms看起来只慢了500ms。但在400 QPS下并发占用可能从约40增长到约240。这就是同步系统最容易被忽略的放大效应耗时增加一点 × 持续流量 大量线程同时被占用所以线程池被打满时先别急着把200改成500。先看这些线程到底在计算还是在替某个下游等待。本文归入「生产环境保命清单」系列。你遇到过CPU不高、Tomcat线程却全部繁忙的情况吗最后卡在数据库、Redis、HTTP下游还是文件I/O欢迎把最终证据留在留言区。系列导航上一篇《Java只配了-Xmx4g进程为什么吃掉8GB》下一篇预告《一条SQL只执行100ms为什么连接池还是排满了》关注公众号云技纵横回复关键词保命获取「生产环境保命清单」全部文章。也可以把脱敏后的Tomcat指标、接口QPS、耗时分位数和三份线程栈发来我会尽量帮你判断下一步应该查哪里。请务必隐藏密码、Token、IP、域名和客户数据。参考资料Apache TomcatHTTP Connector配置Spring BootCommon Application PropertiesSpring BootActuator与Tomcat指标Oraclejcmd命令

相关新闻

2026/8/26 17:45:15

设备树之GPIO

1、示例全景图┌────────────────────────────────────────────── │ 物理芯片上有 16 个引脚:PA0 ~ PA15 │ │ Pin Controller 决定&#xff1a…

2026/8/26 17:45:15

OPENAI GPT PLUS 日常开发你够用吗

你的 AI 算力够用吗?开发者日常消耗量级自测: 别凭感觉买 Token,看看你属于哪个段位? 轻度玩家(≈ 1个 Plus 账号的量):日常写写代码、查查 Bug,偶尔让 AI 润色一下文档&#xff0…

2026/8/26 18:35:39

Linux 性能排查实战

在日常运维中,"机器很卡"是最常见也最模糊的问题描述。卡顿可能来自 CPU、内存、磁盘 IO、网络中的任何一个环节,也可能是特定场景下的局部问题。本文从系统整体卡顿、打字延迟、内存泄漏三个层面,给出一套可执行的排查方法论。 目…

2026/8/26 18:35:39

Codex auth.json 是什么:位置、风险与安全排错

引言:被误解的 auth.json 在 OpenAI Codex 的日常使用中,auth.json 文件常常被开发者从网上搜索、复制粘贴,当作一个普通的“配置文件模板”来使用。这是一个极其危险且普遍存在的误解。本文将深入解析 auth.json 的本质、正确的管理方式以及…

2026/8/26 18:35:39

嵌入式串口调试神器 JCom 详解|自定义协议组包+实时曲线解析

1. 前言 在嵌入式、单片机日常开发中,串口调试是最基础也是最高频的操作。 目前开发者常用的 SSCOM、串口调试助手等工具,仅支持基础的 HEX/ASCII 收发、定时发送功能。 一旦遇到自定义二进制协议、自动CRC校验、传感器ADC数据可视化、丢包检测等场景&am…

2026/8/26 18:35:39

Dynadot全球域名行业全景观察(2026.06)

关于Dynadot Dynadot是通过ICANN认证的域名注册商,自2002年成立以来,服务于全球108个国家和地区的客户,为数以万计的客户提供简洁,优惠,安全的域名注册以及管理服务。 Dynadot平台操作教程索引(包括域名邮…

2026/8/26 18:35:39

程序员兼职的新机会,正从那几百张Excel里冒出

销售表、库存表、回款表、排班表,文件名后面依次跟着「最新版」「最终版」和「敲定版」。每张表单独看都能用,数据一旦需要互相对上,办公室里就开始复制、粘贴、核对,再在群里问一句谁改过。很多公司真正缺的工具,可能…

2026/8/26 9:13:28

[光学原理与应用-521]:对光的错误理解与纠偏

首先光是一种能量的载体和形态,宏观上观察到的光是由无数个微观的光量子组成的,每个光子在产生的瞬间,其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前,在微观层面,每个光量子的运动轨迹是以波函数所展现…

2026/8/25 11:48:27

SIP通话转接原理与REFER方法实战解析

1. 通话转接不是“挂断再拨号”,而是SIP会话的动态重定向你有没有遇到过这样的场景:客服坐席A正在和客户通电话,突然需要把这通对话无缝转给专家坐席B,客户完全感知不到中间的断连——既没听到忙音,也没被要求重新拨号…

2026/8/25 16:56:43

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

1. 为什么选择Kolla-ansible来部署单节点OpenStack?如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法,那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

2026/8/26 0:04:32

Python random 模块常用函数详解:从入门到实战

目录 1. 引言2. 准备工作3. 基础随机函数4. 序列相关函数5. 随机种子与复现6. 实战案例7. 注意事项8. 常见问题与排查9. 总结 1. 引言 摘要: 本文系统介绍 Python 标准库 random 模块中最常用的随机数生成函数。内容涵盖基础随机函数(random()、unifor…

2026/8/26 1:19:35

JSON总结

JSON概念 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,主要用于跟服务器进行交换数据。它基于ECMAScript的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C、C、C#、Java、JavaScr…

2026/8/26 1:19:35

保存连接sse 是什么原理,为什么不会一直请求

“保持连接”用的是 SSE(Server-Sent Events),本质是一个没有马上结束的 HTTP 请求。 过程是: 拷贝机发送一次请求: GET /api/code-sync/events服务器返回: Content-Type: text/event-stream但不关闭响应&…

2026/8/24 13:42:17

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/24 18:13:48

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/25 1:08:14

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…