线上业务时不时报警,服务变卡,数据库连接池被打满,到底是谁的问题?如果你用的是Jetty,先别急着甩锅给网络或者操作系统。学会盯住几个关键指标,再动手分析,你就知道下一步该干什么。这篇文章不搞一堆抽象术语,直接拿命令和脚本说话,哪怕你刚接触Jetty,照着敲也能用起来。
一、先聊聊应用场景:什么时候需要盯着Jetty?
Jetty是Java世界里常见的Web容器,很多内部系统、微服务都跑在它上面。正常情况下它安安静静,但一旦流量涨起来、或者代码里出现资源没释放,问题就会一层一层浮现。常见场景包括:大促前做压测,看Jetty能扛多少并发;线上偶发超时,想确认是不是线程池被占满;服务占用的内存一路飙升,怀疑有泄漏或GC频繁;老板让你出个容量报告,你得拿出真实数据说服他。
这些问题不能靠拍脑袋解决。你需要在问题还没爆发前,就能看到连接数涨到多少了、线程池还剩几个空闲线程、每次请求平均用了多少毫秒、内存曲线是不是持续上涨。这些指标就像汽车仪表盘,不看仪表盘开车,很容易把引擎烧掉。
另外,Jetty本身是Java程序,所以除了Web层的指标,JVM层面的内存和GC数据也必须纳入监控范围。很多Jetty性能问题的根源,其实并不在Jetty的请求处理逻辑里,而是底层JVM在“求生存”。比如老年代涨得飞快,Full GC一次几秒钟,那所有请求都要排队,表现就是响应时间飙升。所以我们的监控指标要分成两块:Jetty自身的运行状态,以及JVM的运行状态。
二、监控指标那么多,到底该看哪些?
2.1 核心指标清单
Jetty的指标可以从外部观察,也可以借助自身统计工具。但不管用什么方式,下面几个维度必须覆盖:
- 连接数:当前打开的连接数、每秒新建连接数。如果连接数接近系统限制,说明要么流量太大,要么连接没有正常关闭。
- 线程池状态:线程池的忙碌线程数、空闲线程数、队列积压数量。线程池被打满,请求就会排队。
- 请求吞吐量:单位时间处理的请求数,一般用QPS(每秒查询数)表示。
- 响应时间:平均响应时间、最大响应时间、99分位响应时间。平均响应可能被长尾掩盖,所以分位数很有参考价值。
- JVM内存:堆内存的使用情况,特别是老年代的增长趋势。
- GC情况:Young GC和Full GC的频率及耗时。GC太频繁会直接造成停顿,影响接口延迟。
这些指标不是孤立的。比如吞吐量下降,同时线程池忙碌线程数一直处于高位,那可能不是压力变小,而是线程在等某个下游资源。再比如GC时间变长,那就得先解决内存问题,否则加线程也白搭。
2.2 从Jetty自己拿指标:StatisticsHandler
Jetty自带了一个统计处理器,叫做StatisticsHandler。开启之后,Jetty会记录请求数量、响应时间、连接数等信息。我们可以通过JMX或者HTTP接口把数据拿出来。为了快速验证,我建议先用HTTP方式,直接在浏览器里看JSON数据,方便直观。
首先需要在Jetty的配置里启用StatisticsHandler。以最常见的内嵌Jetty为例,可以在代码中这样设置,但因为我们这里统一用Shell技术栈演示监控方法,所以配置部分我给出一个概念说明:你在Jetty的jetty.xml中,把DefaultHandler替换成StatisticsHandler,并把它链接到Server上。然后在代码里或者通过JMX把统计信息暴露出来。
在实际运维环境中,更常见的做法是通过JMX建立RMI连接,然后用jconsole查看。但命令行脚本更方便自动化。我们可以通过JDK自带的jcmd或其他工具从进程里获取JMX信息,但需要设置环境变量。为了方便演示,我直接用一个开发时常用的方式:用curl请求一个暴露了JSON指标的端点。你需要提前在应用里添加一个Servlet,它返回StatisticsHandler的数据。虽然这种方式生产环境不一定用,但用来理解指标含义特别合适。
2.3 从JVM拿指标:内存与GC
JVM指标通常用JDK自带的jstat命令查看。这个命令非常安全,不会对运行中的进程造成干扰。先找到Jetty进程的PID,然后用jstat输出GC情况。
下面这个命令能实时看到进程PID为12345的堆内存和GC统计:
# 技术栈:Shell/bash
# 假设12345是你的Jetty进程PID
# 每隔1000毫秒打印一次,一共打印5次
# -gcutil表示输出已用空间占总空间的百分比
jstat -gcutil 12345 1000 5
执行后你会看到类似这样的输出,注释在右边解释了每个字段:
# S0: 幸存区0使用百分比 S1: 幸存区1使用百分比
# E: 伊甸园区使用百分比 O: 老年代使用百分比
# M: 元数据区使用百分比 YGC: Young GC次数
# YGCT: Young GC耗时(秒) FGC: Full GC次数
# FGCT: Full GC耗时(秒) GCT: 全部GC总耗时(秒)
S0 S1 E O M YGC YGCT FGC FGCT GCT
0.00 12.50 68.20 45.30 82.10 1280 2.340 3 0.120 2.460
如果看到老年代O持续接近100%,同时FGC次数快速增加,基本可以断定内存压力很大。有可能是代码里有对象被错误地长期持有,也有可能是缓存设置得太大。
三、动手写一个简单的监控脚本
3.1 准备工作
在开始写脚本之前,你需要确定自己的Jetty是否开启了JMX远程管理。最简单的方式是在启动Jetty时加上几个JVM参数,让JMX端口固定。比如:
# 技术栈:Shell/bash
# 启动Jetty时开启JMX远程访问
# 端口3138,使用本机基本认证,密码文件在/etc/jetty-jmx.password
java -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=3138 \
-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.password.file=/etc/jetty-jmx.password \
-jar start.jar
这里必须注意:生产环境务必开启认证并限制网络访问,否则任何人都能通过JMX控制JVM,非常危险。如果没有条件开JMX,也可以退而求其次使用jstat这类本地工具,但脚本的通用性会下降,因为jstat需要能访问到进程的机器账号。
3.2 获取Jetty核心指标
假设我们已经有一个暴露了统计信息的HTTP接口,地址是http://localhost:8080/stats,返回JSON。下面这段Shell脚本负责抓取数据并解析出几个关键字段:
# 技术栈:Shell/bash
# 抓取Jetty统计接口的数据,并提取连接数和请求数
# 定义接口地址
URL="http://localhost:8080/stats"
# 使用curl静默模式获取响应,-w让响应后输出换行,防止JSON后面跟着其他数据
RESPONSE=$(curl -s "${URL}")
# 使用echo管道连接sed,去掉JSON中的空白字符
# 这里并没有引入jq,是为了让你在基础环境里也能跑
# 但如果你有jq,推荐用jq更安全
CLEAN_RESPONSE=$(echo "${RESPONSE}" | tr -d '[:space:]')
# 提取当前连接数,假设JSON中有"connectionsCurrent":数字
CONNECTIONS=$(echo "${CLEAN_RESPONSE}" | sed -n 's/.*"connectionsCurrent":\([0-9]*\).*/\1/p')
# 提取请求总数,假设JSON中有"requestCountTotal":数字
TOTAL_REQUESTS=$(echo "${CLEAN_RESPONSE}" | sed -n 's/.*"requestCountTotal":\([0-9]*\).*/\1/p')
# 打印结果,方便肉眼观察
echo "当前连接数: ${CONNECTIONS}"
echo "累计请求数: ${TOTAL_REQUESTS}"
这段脚本非常直接,适合临时手工执行。但如果你要长期监控,最好使用jq来解析复杂的嵌套JSON,因为手动正则解析很容易在字段顺序变化时出错。下面是一个更可靠的版本,使用jq,并且同时获取吞吐量和响应时间:
# 技术栈:Shell/bash
# 使用jq解析Jetty统计接口
# 先检查jq是否安装
if ! command -v jq &> /dev/null; then
echo "请先安装jq,比如:apt install jq"
exit 1
fi
URL="http://localhost:8080/stats"
# 使用jq从JSON中提取多个字段
# .命名空间和实际字段名需要根据你的StatisticsHandler的JSON结构调整
curl -s "${URL}" | jq '.stats |
{
connections: .connectionsCurrent,
requests: .requestCountTotal,
avg_time_ms: .meanRequestTime,
max_time_ms: .maxRequestTime,
active_threads: .threadPoolActive,
idle_threads: .threadPoolIdle
}'
执行后你会得到类似下面的输出,注释部分是对应字段的说明:
# 输出示例
{
"connections": 45, # 当前有45个连接
"requests": 10240, # 累计处理了10240个请求
"avg_time_ms": 12.5, # 平均每个请求耗时12.5毫秒
"max_time_ms": 890, # 最大的单次请求耗时890毫秒
"active_threads": 8, # 8个线程正在干活
"idle_threads": 92 # 92个线程空闲
}
看到这个输出,你就能快速判断当前Jetty的健康状态。比如active_threads长期接近最大线程数,同时idle_threads很低,说明线程池可能不够用。如果avg_time_ms突然变大,你就要接着去看JVM的GC情况。
3.3 把JVM监控也加进来
上面的脚本只涉及Jetty高层状态,而JVM的GC才往往是大问题的根源。我们可以写一个综合脚本,同时抓取Jetty指标和JVM GC指标。为了简化,这里直接调用jstat一次性输出GC概况,再结合前台的Jetty统计,给你一个“组合拳”式的监控工具。
# 技术栈:Shell/bash
# 综合监控脚本(需要提前传入Jetty进程PID)
# 用法: ./monitor.sh <PID>
# 检查参数
if [ "$#" -ne 1 ]; then
echo "请指定Jetty的进程ID,例如: ./monitor.sh 12345"
exit 1
fi
PID=$1
# 第一部分:获取JVM GC情况
echo "===== JVM GC 情况 ====="
jstat -gcutil "${PID}" 500 3
# 第二部分:如果Jetty有统计接口,也抓取下来
# 这里假设统计接口地址固定,你可以根据自己的实际端口调整
JETTY_URL="http://localhost:8080/stats"
if curl -s "${JETTY_URL}" &> /dev/null; then
echo ""
echo "===== Jetty 请求耗时 ====="
curl -s "${JETTY_URL}" | jq '{avg_time_ms: .stats.meanRequestTime, max_time_ms: .stats.maxRequestTime}'
else
echo ""
echo "警告: 无法访问 ${JETTY_URL},跳过Jetty指标"
fi
这个脚本非常适合在排查问题的时候跑一下,一两秒钟就能看到全貌。如果JVM的GC输出显示Full GC非常频繁,而Jetty的响应时间又恰好很高,那几乎可以断定GC停顿是元凶。你可以进一步用jmap -histo:live查看哪些对象占用了内存,或者用jstack抓取线程栈,看看GC之后有哪些线程处于阻塞状态。
3.4 给脚本加个简单的阈值报警
监控不能只看实时输出,最好还能在指标异常时自动发出一条警告。下面这段脚本是一个很朴素的报警示例,它定期检查线程活跃数和老年代使用率,超过阈值就打印警报。在生产环境中,你可以把警报改成钉钉机器人或者邮件通知。
# 技术栈:Shell/bash
# 循环监控,并在超过阈值时发出警报
PID=$1
# 定义阈值
THREAD_LIMIT=80 # 活跃线程数超过80就告警
OLD_GEN_LIMIT=90 # 老年代使用率超过90%就告警
# 开始一个死循环,每5秒检查一次
while true; do
# 从health接口获取活跃线程数(示例)
ACTIVE=$(curl -s "http://localhost:8080/stats" | jq '.stats.threadPoolActive')
# 使用jstat获取老年代使用率,取百分比并去掉小数
OLD_GEN=$(jstat -gcutil "${PID}" | tail -1 | awk '{print $4}' | cut -d. -f1)
# 判断是否超过阈值
if [ "${ACTIVE}" -gt "${THREAD_LIMIT}" ]; then
echo "[警告] 活跃线程数异常: ${ACTIVE} > ${THREAD_LIMIT}"
fi
if [ "${OLD_GEN}" -gt "${OLD_GEN_LIMIT}" ]; then
echo "[警告] 老年代使用率异常: ${OLD_GEN}% > ${OLD_GEN_LIMIT}%"
fi
# 睡眠5秒,等待下次检查
sleep 5
done
这段脚本虽然简单,但已经具备了一个监控工具的雏形。你完全可以根据需要扩展,比如加上时间戳、把警告写入日志文件。注意,在生产环境做自动告警前,一定要充分验证阈值设置是否合理,否则会被无意义的告警轰炸到麻木。
四、技术优缺点与注意事项
4.1 这种命令加脚本的方式有什么优缺点?
- 优点:轻量,不需要额外部署复杂监控系统。只要服务器能执行几个命令,就能快速获取核心指标。对临时排查问题特别有效,几行脚本就能拿到数据。不依赖商业软件,学习成本低。
- 缺点:自动化程度不高,无法长期保存历史趋势数据。比如你想看一个月内老年代使用率的变化曲线,光靠jstat和curl很难做到。另外,jstat只适用于本机进程,如果Jetty跑在容器里,你需要能进入容器才能执行。对于请求延迟的分位数分析,也需要更专业的APM工具才能高效完成。
如果你的服务规模很大,或者团队已经有Prometheus和Grafana这类基础设施,那应该用Micrometer把Jetty的指标暴露给Prometheus。但如果你是第一次接触Jetty性能监控,先把这个篇里手动命令记熟,再去上大工具,会容易理解得多。
4.2 注意事项
- JMX安全问题:开启JMX远程端口时,务必设置强密码并限定来源IP。不要裸奔在公网上,否则恶意者可以用JMX调用System.exit()直接让服务挂掉。
- jstat对性能和采样的影响:虽然jstat本身比较轻,但不要让监控脚本过于频繁地调用它。一般以秒为单位没问题,如果毫秒级频率,反而会干扰应用性能。
- 统计接口的权限:如果通过HTTP暴露StatisticsHandler的数据,一定要做访问控制,至少用防火墙限制来源,或者添加认证。不然别人很容易看到你的业务量。
- 阈值设置要基于基线:别把活跃线程数限制定得很死。每台机器的配置和应用特性不同,先观察一段时间“正常”值,再设定阈值。比如一个后台任务型应用,线程池活跃度本来就不高,你设80可能永远不会触发;而一个高并发的网关,活跃线程数常年在200以上,你设80就会导致告警刷屏。
- 容器环境的内存指标要留意:如果Jetty跑在容器里,jstat看到的JVM内存可能不等于容器限制。你要注意JVM是否已经感知到容器所在的CPU和内存限制,否则可能会因无法分配内存而OOM。
五、接下来该怎么做?
当你学会了抓取这些指标,下一步不是立刻去改代码,而是先形成一套分析思路:先看Jetty的线程和连接,确认是不是高并发请求把入口堵住;再看JVM的GC,确认底层内存是否有问题;最后再看响应时间的分布,判断是业务逻辑慢还是资源等待慢。
就比如线上出现超时,你第一件事不是查日志,而是跑一下本篇里的综合监控脚本。如果活跃线程数和连接数都特别大,说明流量确实高。如果老年代使用率涨得很快,同时Full GC频繁,那你该看一下内存快照。如果一切都正常,那可能是外部依赖出问题了。这样一层层排查,效率会高很多。
Jetty的性能监控并不是什么高深魔术,核心就是盯住入口流量、处理线程、内存回收这三个阶段。把这几个指标放到冲刺冲刺,大多数性能问题都会现出原形。当你用熟了这些命令和脚本,再决定要不要引入更完整的监控平台,思路就会非常清晰。
评论
围绕“Jetty的性能监控指标与分析方法”参与讨论