Tomcat并发请求变慢如何优化?在Debian上检查线程池、连接池与CPU负载
Tomcat并发请求变慢时,不要先盲目把 maxThreads 调大。正确的处理顺序是先确认 Debian 主机的 CPU、内存、文件描述符和 JVM 进程状态,再分别检查 Tomcat 工作线程、连接队列、数据库连接池以及共享锁资源。只有明确请求是在“排队等线程”“排队等数据库连接”还是“等待 CPU、磁盘或锁”,调参才有意义。
下面的 Debian Tomcat性能调优策略适用于单个 Tomcat JVM 运行 Web 应用的场景,也适合在测试环境中复现并发变慢问题。操作前需要准备具有读取服务状态和 Tomcat 配置的权限,确认 Java、Tomcat 和 Debian 版本,并准备一个不会修改生产数据的测试接口。修改 server.xml 或连接池配置会触发服务重启,应先备份并安排可接受的中断窗口。
一、先固定测试环境和观察对象
性能测试前,先确定以下条件:
- Debian 版本、Java 版本、Tomcat 版本。
- Tomcat 服务名、JVM 进程号和
CATALINA_BASE路径。 - 测试接口是否涉及数据库、文件读写、远程调用或锁竞争。
- 测试机与被测机是否为同一台服务器。
- 测试期间是否有定时任务、备份、日志切割等额外负载。
- 测试请求是否包含认证、固定参数和相同的数据规模。
Debian 上不要直接假设服务名一定是 tomcat9 或 tomcat10,先查找实际服务:
systemctl list-units --type=service --all | grep -Ei 'tomcat|catalina'
确认服务名后再设置变量。下面以 tomcat9 为示例,实际使用时替换为查询到的名称:
SERVICE=tomcat9
systemctl status "$SERVICE" --no-pager
systemctl show "$SERVICE" -p MainPID -p ExecStart -p User -p LimitNOFILE
java -version
cat /etc/debian_version
获取 Tomcat JVM 进程并确认是否存在多个实例:
PID=$(pgrep -o -f 'org.apache.catalina.startup.Bootstrap')
if [ -z "$PID" ]; then
echo "未找到 Tomcat JVM,请检查服务状态和进程启动参数"
exit 1
fi
echo "Tomcat PID: $PID"
ps -fp "$PID"
tr '\0' ' ' < /proc/"$PID"/cmdline
echo
如果主机上存在多个 Tomcat 进程,不能只使用 pgrep -o 得到的第一个进程。需要结合端口、服务名和 -Dcatalina.base 参数确认目标实例,否则可能把其他应用的 CPU 和线程数据当成当前服务的数据。
二、建立调整前基线
在改变任何线程或连接池参数前,至少记录一轮基线。建议先预热 30 秒,再稳定测试 60 秒,避免 JVM 类加载、连接池首次创建和缓存建立影响结果。

1. 查看 CPU、内存和运行队列
vmstat 1 10
重点观察:
r:等待 CPU 的运行队列。持续高于 CPU 逻辑核数,通常表示 CPU 竞争明显。us:用户态 CPU,常见于业务代码、序列化、压缩或计算。sy:内核态 CPU,可能与网络、文件系统或系统调用有关。wa:I/O 等待。较高时,单纯增加 Tomcat 线程通常会放大等待。si、so:交换分区换入换出。出现持续交换时,应先处理内存压力。free:可用内存,不能只看这一列判断是否缺内存,还要结合交换分区和进程占用。
查看 Tomcat 进程和线程消耗:
top -H -p "$PID"
或者使用 pidstat:
pidstat -p "$PID" -u -r -w 1 10
如果系统尚未安装 pidstat,它通常由 Debian 的 sysstat 软件包提供。生产环境不要在不了解软件源和变更流程的情况下临时安装软件,测试机可以按现有运维规范安装。
2. 查看线程数、连接数和文件描述符
ps -L -p "$PID" -o pid,tid,pcpu,stat,wchan:24,comm
cat /proc/"$PID"/limits | grep -i 'open files'
ss -s
ss -lntp | grep java
ps -L 显示的是 JVM 操作系统线程,不等同于 Tomcat 当前正在处理请求的工作线程。JVM GC 线程、定时任务线程、应用线程和 Tomcat 工作线程都包含在内,因此只能用于观察整体变化。
ss -s 和监听端口信息可以帮助判断连接是否大量堆积,但不能单独证明 Tomcat 工作线程已经耗尽。还需要结合 Tomcat 配置、访问日志和应用指标。
3. 查看 JVM 垃圾回收和线程状态
在 JDK 提供相应工具且执行用户有权限时,可以运行:
jstat -gcutil "$PID" 1000 10
jcmd "$PID" Thread.print > /tmp/tomcat-thread-"$PID".txt
jstat -gcutil 中如果老年代长期接近满值,并伴随 Full GC 或请求延迟尖峰,瓶颈可能在堆内存和垃圾回收,而不是线程池大小。
线程转储中常见状态的含义如下:
| 状态或现象 | 可能含义 | 处理方向 |
|---|---|---|
大量 RUNNABLE,CPU 使用率高 | 业务代码正在计算或频繁系统调用 | 查热点方法、序列化、循环和日志 |
大量 BLOCKED | 线程等待 Java 共享锁 | 查锁对象、同步代码和慢临界区 |
大量 WAITING 或 TIMED_WAITING | 线程在等待连接池、队列或其他资源 | 对照数据库连接池和应用监控 |
| Tomcat 线程数高,但 CPU 低 | 可能在等待数据库、远程服务或文件 I/O | 查连接池、I/O 和外部依赖 |
| Full GC 期间请求整体停顿 | 堆压力或对象分配过快 | 先确认 GC 日志和堆使用,再处理 JVM 参数 |
不要因为线程转储中线程数量多就直接增加 maxThreads。如果线程都在等待同一把锁或同一个数据库连接,继续增加线程只会增加上下文切换和等待队列。
三、理解 Tomcat 的线程、连接和队列关系
一次 HTTP 请求通常会经过以下资源:

- 客户端建立或复用 TCP 连接。
- Tomcat Connector 接收连接并解析请求。
- 请求进入 Tomcat 工作线程或共享 Executor。
- 应用访问数据库、缓存、文件或其他服务。
- 线程返回响应,连接根据 Keep-Alive 设置继续复用或关闭。
因此,“连接数”“工作线程数”“数据库连接数”不是同一个指标。
1. Connector 的关键参数
常见 HTTP Connector 参数包括:
maxThreads:Connector 可使用的最大工作线程数。minSpareThreads:启动或空闲时保留的最小工作线程数。maxConnections:允许保持的连接数量上限,包含正在处理和保持连接的连接。acceptCount:工作线程暂时无法接收新请求时,等待进入操作系统队列的连接数量。connectionTimeout:连接建立或读取请求数据的等待时间。keepAliveTimeout:Keep-Alive 连接等待下一个请求的时间。
acceptCount 不是吞吐能力。它只能在短时间突发流量下提供缓冲,设置过大可能让请求排队更久,最终表现为延迟增加而不是处理能力增加。
maxConnections 也不等于并发处理线程数。大量客户端保持空闲连接时,连接数可能很高,但真正消耗业务线程的请求数并不高。
检查现有配置:
grep -nE 'Connector|Executor' \
"$CATALINA_BASE/conf/server.xml" 2>/dev/null
如果尚未设置 CATALINA_BASE,应从 Tomcat 进程启动参数中读取 -Dcatalina.base,不要凭经验修改 /var/lib/tomcat9 或 /etc/tomcat9 下的文件。
一个仅用于说明参数关系的 Connector 示例:
这个数值不是通用最佳配置。是否适合当前环境,需要根据 CPU 核数、请求耗时、数据库容量和测试结果决定。
2. 使用共享 Executor 时的注意事项
有些 Tomcat 配置会把线程池定义为共享 Executor:
当 Connector 使用 executor 时,实际工作线程由 Executor 控制,Connector 上的部分线程参数不会按照没有 Executor 的方式生效。调整前必须确认当前配置是独立线程池还是共享线程池。
共享 Executor 的优点是多个 Connector 或应用入口可以统一限制线程数,缺点是一个入口的请求可能占满共享线程,影响其他入口。因此,排查时要把不同接口的请求量和响应时间分开观察。
3. 数据库连接池不是越大越快
应用线程在访问数据库时,通常还要从数据库连接池借连接。常见指标包括:
- 当前活动连接数。
- 空闲连接数。
- 最大连接数。
- 等待借连接的线程数。
- 获取连接的平均时间和最大时间。
- 获取连接超时次数。
- 数据库连接创建失败数。
如果应用线程池有 200 个线程,而数据库连接池只有 30 个连接,那么最多只有一部分请求可以同时执行数据库操作,其余线程会等待连接。此时把 Tomcat 的 maxThreads 从 200 调到 400,通常只会增加等待线程。
以 Tomcat JDBC Pool 为例,配置可能类似下面的形式:
这只是 Tomcat JDBC Pool 的示例。DBCP2、HikariCP 和应用自带连接池使用的参数名称并不相同,不能把 maxActive、maxTotal、maximumPoolSize 等参数混用。应先确认应用实际使用的连接池实现,再查看对应配置。
连接池上限需要同时考虑数据库允许的连接数、其他应用占用、单个请求实际使用的连接数和数据库 CPU。增加连接池前,应确认数据库不是已经处于锁等待、I/O 等待或 CPU 饱和状态。
四、设计可重复的并发测试
测试工具可以使用已经纳入运维环境的压测程序。以 wrk 为例,测试接口必须是专用测试接口或只读接口,不要直接对生产写入接口进行高并发测试。
wrk -t4 -c50 -d30s --latency \
http://127.0.0.1:8080/app/read-test
参数含义:
-t4:压测客户端使用 4 个线程。-c50:同时保持约 50 个连接,不代表每秒发送 50 个请求。-d30s:持续 30 秒。--latency:输出延迟分布。
建议采用逐级并发,而不是一次把并发数提高到很大:
| 阶段 | 并发连接 | 目的 |
|---|---|---|
| 预热 | 10~25 | 建立 JVM、缓存和数据库连接 |
| 第一轮 | 25~50 | 获取低压力基线 |
| 第二轮 | 100 | 观察线程和连接池是否开始排队 |
| 第三轮 | 200 或更高 | 确认饱和点和错误拐点 |
| 复测 | 与基线相同 | 判断调整是否真正改善 |
每一轮至少记录以下数据:
- 吞吐量,即每秒完成请求数。
- 平均延迟、P50、P95、P99。
- HTTP 5xx、连接超时和客户端超时。
- Tomcat 工作线程活动数和最大线程数。
- 数据库连接池活动数、等待数和超时数。
- JVM CPU、主机 CPU、运行队列和 I/O 等待。
- GC 次数、GC 停顿和堆使用率。
平均延迟容易掩盖少量极慢请求。并发请求变慢时,P95 和 P99 往往比平均值更能反映排队问题。
压测期间可以在另一个终端持续执行:
vmstat 1
pidstat -p "$PID" -u -r -w 1
top -H -p "$PID"
ss -tanp | grep "pid=$PID,"
如果使用应用监控或 JMX,还应记录 Tomcat Executor 的活动线程、池大小和队列长度,以及连接池的活动连接和等待线程。不同 Tomcat 版本、连接池实现和监控组件暴露的指标名称可能不同,不能仅凭指标名称推断含义。
五、根据指标判断真正的瓶颈
可以用下面的现象组合进行判断:
| 观察结果 | 更可能的瓶颈 | 不建议立即做的事 | 优先检查 |
|---|---|---|---|
| CPU 长时间超过 85%,运行队列接近或超过 CPU 核数 | CPU 或业务计算饱和 | 继续增加 maxThreads | 热点代码、GC、锁竞争、日志 |
| CPU 低,活动工作线程接近上限,P95 随并发上升 | Tomcat 工作线程不足或请求本身较慢 | 只调大 acceptCount | 请求耗时、线程池、外部依赖 |
| Tomcat 线程很多,但数据库池活动数达到上限且等待数上升 | 数据库连接池或数据库本身受限 | 同时扩大 Tomcat 和数据库连接池 | SQL、锁、数据库 CPU、连接获取耗时 |
| TCP 连接数高,但工作线程并不高 | Keep-Alive 或慢客户端占用连接 | 直接扩大工作线程 | maxConnections、Keep-Alive、客户端行为 |
CPU 不高,线程大量 BLOCKED | Java 锁或共享资源竞争 | 增加线程数量 | 线程转储、同步代码、缓存锁 |
wa 高或出现交换 | 磁盘、内存或系统 I/O 压力 | 继续提高并发 | 内存、日志、磁盘和交换分区 |
| 5xx 和连接超时随并发突然增加 | 某个队列或依赖达到上限 | 只延长超时时间 | Tomcat 队列、连接池、下游服务 |
例如,下面是一组用于说明判断方法的示例数据,并非特定环境的实测结果:

| 指标 | 基线示例 |
|---|---|
| 并发连接 | 100 |
| 吞吐量 | 620 req/s |
| P95 延迟 | 1.9 s |
| Tomcat 最大线程 | 200 |
| 活动线程 | 195~200 |
| JVM CPU | 48% |
| 数据库池最大连接 | 80 |
| 数据库活动连接 | 80 |
| 等待数据库连接线程 | 持续增加 |
这组数据中 CPU 还有余量,但 Tomcat 线程几乎全部在工作,数据库连接池已达到上限,说明主要排队点更接近数据库连接池或数据库执行时间。此时把 maxThreads 调到 400,通常只会让更多线程等待数据库连接,P95 可能更高。
另一种示例是 CPU 长时间达到 95%,运行队列高于 CPU 核数,线程转储中大量线程处于 RUNNABLE,数据库池没有等待。此时应该先降低测试并发或优化 CPU 密集型代码、序列化、日志和 GC,而不是继续增加线程。
六、按低风险顺序调整参数
1. 先处理确认的队列瓶颈
如果确认是 Tomcat 工作线程达到上限,并且同时满足以下条件,可以小幅增加线程数:
- CPU 仍有明确余量。
- 没有持续 Full GC。
- 数据库连接池没有明显等待。
- 下游服务没有达到连接或请求上限。
- 线程转储没有大量锁阻塞。
- 内存和文件描述符仍有余量。
可以每次增加 20%~30%,例如从 200 调到 240 或 250,然后使用相同并发条件复测。不要一次从 200 调到 1000,否则无法判断哪个变化导致结果变化。
如果活动线程没有达到上限,增加 maxThreads 没有实际作用。此时应查慢 SQL、远程调用、锁和文件 I/O。
2. 再处理连接池排队
当数据库连接池等待明显时,先确认请求是否都必须访问数据库,以及是否存在连接借出后执行慢查询、事务范围过大或未及时归还连接的问题。
可观察以下结果:
- 连接获取耗时高,但数据库执行时间低:连接池大小或连接归还存在问题。
- 数据库执行时间本身高:扩大连接池可能让数据库更早饱和。
- 活动连接低,但请求仍慢:瓶颈可能不在数据库连接池。
- 连接获取经常超时:需要检查连接泄漏、池上限和应用异常处理。
maxWait 或类似等待超时参数的作用是让请求在资源不可用时尽快失败或降级,不会增加数据库处理能力。过大的等待时间可能把连接池排队转化为大量 Tomcat 线程堆积。
3. 最后处理连接保持和突发队列
maxConnections 主要限制连接数量,acceptCount 主要提供短时排队空间。两者都不是提升业务吞吐量的直接开关。
如果连接数很高、请求处理量不高,可以检查:
- 客户端是否长时间保持空闲连接。
keepAliveTimeout是否过长。- 是否存在慢速上传或慢速读取。
- 文件描述符上限是否接近耗尽。
降低 Keep-Alive 等待时间可能减少空闲连接占用,但也可能增加连接建立次数。必须通过相同客户端和相同请求模型复测,不能仅凭连接数下降就认为性能改善。
七、执行配置修改并验证
确认要修改参数后,先备份实际配置文件。以下操作只适用于已经确认路径和服务名的 Debian Tomcat 环境:
SERVICE=tomcat9
CONFIG="$CATALINA_BASE/conf/server.xml"
BACKUP="$CONFIG.bak.$(date +%Y%m%d-%H%M%S)"
sudo cp -a "$CONFIG" "$BACKUP"
echo "备份文件:$BACKUP"
建议使用 sudoedit 修改,而不是使用复杂的 sed 替换 XML 属性,避免误改多个 Connector 或破坏 XML 结构:
sudoedit "$CONFIG"
修改后先检查 XML 格式。系统安装了 xmllint 时可以执行:
xmllint --noout "$CONFIG"
如果没有 xmllint,至少先查看修改位置,并通过 Tomcat 启动日志验证。重启服务会中断现有请求,操作前应确认影响范围:
sudo systemctl restart "$SERVICE"
sudo systemctl status "$SERVICE" --no-pager
sudo journalctl -u "$SERVICE" -n 80 --no-pager
成功启动后检查:
ss -lntp | grep java
ps -L -p "$PID" -o pid,tid,pcpu,stat,comm
由于重启后 PID 可能变化,应重新获取:
PID=$(pgrep -o -f 'org.apache.catalina.startup.Bootstrap')
验证内容包括:
- Tomcat 服务状态为
active (running)。 - 目标端口正常监听。
- 应用健康检查接口返回正常。
- 日志中没有 XML 解析错误、线程池初始化错误或数据源初始化错误。
- 数据库连接池可以建立连接。
- 低并发请求的响应时间没有明显恶化。
八、常见失败处理
修改了 maxThreads,性能没有变化
优先检查是否使用了共享 Executor。如果 Connector 配置了 executor,应查看 Executor 的 maxThreads。同时确认活动线程是否真的达到上限。如果线程数只使用到几十个,瓶颈可能在数据库、远程接口、锁或客户端连接。
线程数增加后,P95 和超时反而升高
这通常说明线程增加后争抢了 CPU、数据库连接、锁或下游服务。恢复原值,重新对比 CPU、运行队列、数据库池等待数和线程状态。不要把 acceptCount 调大来掩盖问题,因为这可能只是让请求等待更久。
Tomcat 启动失败
立即查看:
sudo journalctl -u "$SERVICE" -n 120 --no-pager
重点检查 XML 标签是否闭合、属性是否重复、Executor 名称是否一致,以及配置项是否被当前 Tomcat 版本支持。若确认是本次改动导致,使用备份恢复:
sudo cp -a "$CONFIG" "$CONFIG.before-rollback.$(date +%Y%m%d-%H%M%S)"
sudo cp -a "$BACKUP" "$CONFIG"
sudo systemctl restart "$SERVICE"
sudo systemctl status "$SERVICE" --no-pager
出现 “Too many open files”
先确认服务级限制,而不是只查看当前登录终端的 ulimit:
systemctl show "$SERVICE" -p LimitNOFILE
cat /proc/"$PID"/limits | grep -i 'open files'
如果文件描述符接近上限,应先检查连接泄漏、空闲连接数量和日志文件句柄。只有确认连接规模合理且应用确实需要更高上限时,才通过 systemd drop-in 调整,并记录原值以便回滚。提高文件描述符上限不能解决数据库连接、CPU 或锁竞争问题。
九、复测、验收和回滚检查项
参数调整后,必须使用与基线一致的测试条件复测:
- 相同的 Debian、Java、Tomcat 和应用版本。
- 相同的测试接口、请求参数和数据规模。
- 相同的预热时间、测试时长和并发梯度。
- 相同的压测客户端线程数和网络位置。
- 测试期间没有额外备份、发布或批处理任务。
验收不能只看吞吐量增加,还应同时检查:
- P95、P99 是否下降或至少没有明显恶化。
- 5xx、连接超时和数据库连接超时是否增加。
- Tomcat 工作线程是否仍有合理余量。
- CPU 是否长期接近满载。
vmstat的运行队列和 I/O 等待是否恶化。- 数据库连接池是否出现持续等待。
- GC 停顿和堆使用率是否正常。
- 文件描述符和内存是否有余量。
- 重启后服务是否能正常恢复,配置是否持久化。
只有当延迟、错误率和资源余量同时达到业务目标时,调整才算有效。如果某项指标改善、另一项指标明显恶化,应回到上一个稳定配置,而不是继续叠加参数。
回滚时保留当前异常配置副本,恢复修改前的 server.xml、Executor 或数据源配置,再重启服务并完成健康检查。若回滚后仍然变慢,应对比线程转储、连接池等待和 CPU 指标,确认是否有测试流量、应用版本或数据库状态变化,而不要反复修改 Tomcat 线程数。