1. 问题现象与初步判断
上周五凌晨2点37分,我正睡得迷迷糊糊,突然被一阵急促的警报声惊醒。打开手机一看,监控系统显示生产环境某台Web服务器的CPU使用率已经持续半小时保持在98%以上。这种情况如果持续下去,轻则导致服务响应缓慢,重则可能引发雪崩效应拖垮整个集群。
登录服务器后,我立即执行了top命令,发现一个名为java的进程确实占用了187%的CPU(多核累加值)。这让我想起去年处理过的一次类似事故——当时是因为一个递归查询没有设置终止条件,导致无限循环。但这次的堆栈信息显示问题似乎出在日志记录环节。
2. 快速定位问题进程
2.1 使用top命令初步观察
首先通过top -c命令查看整体情况,注意几个关键指标:
bash复制top - 02:45:13 up 127 days, 3:21, 2 users, load average: 15.32, 10.45, 5.67
Tasks: 231 total, 2 running, 229 sleeping, 0 stopped, 0 zombie
%Cpu(s): 98.7 us, 1.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 32779616 total, 1245124 free, 23654812 used, 7879680 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 8561232 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
7793 appuser 20 0 12.345g 4.678g 34876 R 187.3 14.9 63:45.67 java -Xmx8g -Xms8g -jar app.jar
重点关注:
load average三个值分别表示1分钟、5分钟、15分钟的平均负载,超过CPU核心数即表示过载%CPU列显示单个进程的CPU占用率(多核环境下可能超过100%)COMMAND列显示完整启动命令,有助于识别具体服务
2.2 更精细的进程筛选
如果服务器上运行着大量进程,可以使用以下命令快速定位:
bash复制ps -eo pid,user,%cpu,%mem,cmd --sort=-%cpu | head -n 10
这个命令会按CPU使用率降序排列,显示前10个最耗资源的进程。我曾经在某次排查中发现一个被遗忘的测试进程占用了90%的CPU,而正式服务反而被挤占了资源。
3. 深入分析问题线程
3.1 查看线程级CPU占用
找到问题进程后,通过top -H -p <PID>查看其线程情况:
bash复制top - 02:47:22 up 127 days, 3:23, 2 users, load average: 16.01, 11.23, 6.45
Threads: 45 total, 5 running, 40 sleeping, 0 stopped, 0 zombie
%Cpu(s): 98.2 us, 1.8 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 32779616 total, 1123456 free, 23876543 used, 7779617 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 8365123 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
7821 appuser 20 0 12.345g 4.678g 34876 R 92.4 14.9 32:18.67 java
7822 appuser 20 0 12.345g 4.678g 34876 R 91.7 14.9 31:45.23 java
7823 appuser 20 0 12.345g 4.678g 34876 S 0.3 14.9 0:05.12 java
注意观察:
- 两个线程分别占用了约90%的CPU,这很可能是问题根源
- 将线程PID(7821、7822)转换为16进制便于后续分析:
printf "%x\n" 7821得到1e8d
3.2 获取线程堆栈信息
使用jstack获取Java进程的线程快照(非Java程序可用pstack或gdb):
bash复制jstack -l 7793 > /tmp/thread_dump.log
然后在日志中搜索之前转换的16进制线程ID(1e8d):
code复制"http-nio-8080-exec-5" #32 daemon prio=5 os_prio=0 tid=0x00007f8b6826a800 nid=0x1e8d runnable [0x00007f8b5e3f6000]
java.lang.Thread.State: RUNNABLE
at java.io.FileOutputStream.writeBytes(Native Method)
at java.io.FileOutputStream.write(FileOutputStream.java:326)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
at java.io.BufferedOutputStream.write(BufferedOutputStream.java:126)
at ch.qos.logback.core.recovery.ResilientFileOutputStream.write(ResilientFileOutputStream.java:50)
at org.apache.logging.log4j.core.appender.OutputStreamManager.writeToDestination(OutputStreamManager.java:259)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:159)
从堆栈可以看出,两个高CPU线程都在执行日志写入操作。这提示我们可能需要检查日志配置和磁盘IO状况。
4. 系统级瓶颈排查
4.1 检查磁盘IO情况
使用iostat -x 1查看磁盘负载:
bash复制Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 532.00 0.00 428.00 0.00 3248.00 15.18 32.17 75.12 0.00 75.12 2.34 100.00
关键指标解读:
%util达到100%表示磁盘已完全饱和await(75.12ms)远高于正常值(通常应<10ms)wkB/s显示写入速度约3MB/s,对于普通云盘已接近上限
4.2 检查文件系统情况
通过df -h发现日志所在分区已使用95%:
bash复制Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 47G 1.2G 95% /var/log
这解释了为什么日志写入如此缓慢——系统在频繁进行磁盘空间回收操作。我立即清理了部分历史日志文件,暂时缓解了问题。
5. 根因分析与解决方案
5.1 日志配置问题
检查日志框架配置发现两个致命问题:
xml复制<!-- 错误的滚动策略 -->
<RollingFile name="AppLog" fileName="/var/log/app.log"
filePattern="/var/log/app-%d{yyyy-MM-dd}.log">
<PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<!-- 缺失SizeBasedTriggeringPolicy -->
</RollingFile>
<!-- 同步日志会阻塞业务线程 -->
<AsyncLogger name="com.myapp" level="info" includeLocation="true">
<AppenderRef ref="AppLog"/>
</AsyncLogger>
主要问题:
- 只按时间滚动日志,没有设置大小限制,导致单个日志文件过大
- 虽然使用了AsyncLogger,但磁盘IO瓶颈导致缓冲队列快速积压
5.2 临时解决方案
立即采取的应急措施:
- 调整日志级别减少输出量:
bash复制# 动态修改日志级别(Log4j2)
curl -X POST -d 'logger.name=com.myapp&level=WARN' http://localhost:8080/loggers
- 切换日志输出到临时内存文件系统:
bash复制mount -t tmpfs tmpfs /var/log/app -o size=100M
5.3 长期优化方案
后续实施的根治措施:
- 完善日志滚动策略:
xml复制<RollingFile name="AppLog" fileName="/var/log/app.log"
filePattern="/var/log/app-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="100MB"/>
</Policies>
<DefaultRolloverStrategy max="100"/>
</RollingFile>
- 引入日志分级存储:
- 错误日志保留30天
- 信息日志保留7天
- 调试日志仅保留当天
- 增加磁盘监控告警:
bash复制# 添加到crontab
*/5 * * * * df -h | awk '$5 > 90 {print $6 " is almost full!"}' | mail -s "Disk Alert" admin@example.com
6. 预防性监控体系建设
6.1 基础监控项配置
在Prometheus中配置关键指标告警规则:
yaml复制groups:
- name: host-alert
rules:
- alert: HighCPUUsage
expr: 100 - (avg by(instance)(irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage on {{ $labels.instance }}"
description: "CPU usage is {{ $value }}%"
- alert: HighDiskUsage
expr: (node_filesystem_size_bytes{mountpoint="/var/log"} - node_filesystem_avail_bytes{mountpoint="/var/log"}) / node_filesystem_size_bytes{mountpoint="/var/log"} * 100 > 85
for: 15m
labels:
severity: warning
6.2 高级诊断工具集成
部署Arthas等诊断工具实现快速问题定位:
bash复制# 安装Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 常用命令示例
[arthas@12345]$ thread -n 3 # 查看最忙的3个线程
[arthas@12345]$ profiler start # 开始采样
[arthas@12345]$ profiler stop --format html # 生成火焰图
6.3 定期健康检查清单
建立每月例行检查项:
- 日志文件大小及滚动策略验证
- 磁盘空间使用趋势分析
- 监控系统告警规则有效性测试
- 关键进程资源限制检查(cgroup配置)
- 系统最大文件描述符数检查
记得去年双十一前做检查时,发现某台机器的ulimit -n只有1024,及时调整避免了可能的大规模连接拒绝问题。这种预防性检查的价值,往往在关键时刻才能体现出来。
