服务器反应慢如何排查
服务器反应慢如何排查
面试官视角:这道题考察什么?
这道题是大厂必考题,考察的不是你背了多少命令,而是你的系统思维能力和问题定位方法论。优秀的回答应该体现出 "先宏观后微观,先整体后局部"的排查思路,而不是上来就说" 看日志 "、" 查 CPU"。
整体排查思路流程图 📊
第一步:快速确认问题范围 ⚡
30 秒黄金判断期,先排除非服务器问题:
- 📶 是不是所有用户都慢?还是只有部分地区 / 网络的用户?
- 🕒 是突然变慢还是逐渐变慢?突然变慢大概率是突发流量、死锁、OOM 前兆
- 🔗 是所有接口都慢?还是只有特定接口?特定接口慢直接查对应业务逻辑
第二步:系统层面排查(四大金刚)💻
1. CPU 排查
# 查看CPU整体使用情况
top -c
# 按CPU使用率排序
top -c -o %CPU
# 查看进程内线程CPU使用情况
top -Hp <pid>
# 将线程ID转为16进制,用于后续查栈
printf "%x\n" <tid>
# 查看Java线程栈
jstack <pid> | grep <16进制tid> -A 20常见问题:
- 死循环 / 无限递归
- 频繁 GC(特别是 Full GC)
- 大量线程上下文切换
2. 内存排查
# 查看内存使用情况
free -h
# 查看进程内存占用
ps aux | sort -k4r | head -10
# 查看JVM内存使用情况
jstat -gc <pid> 1000 10
# 生成堆转储文件
jmap -dump:format=b,file=heapdump.hprof <pid>常见问题:
- 内存泄漏(对象无法被 GC 回收)
- 堆内存设置过小
- 大对象创建过多
3. 磁盘 IO 排查
# 查看磁盘IO使用率
iostat -x 1 10
# 查看哪个进程在读写磁盘
iotop
# 查看磁盘空间使用情况
df -h
# 查看目录大小
du -sh /*常见问题:
- 磁盘空间满(日志文件未清理)
- 大量随机读写(数据库索引未优化)
- 磁盘硬件故障
4. 网络 IO 排查
# 查看网络连接情况
netstat -an | grep <port>
# 查看TCP连接状态统计
ss -s
# 查看网络流量
iftop
# 测试网络延迟
ping <服务器IP>
# 测试端口连通性
telnet <服务器IP> <port>常见问题:
- 网络带宽打满
- 大量 TIME_WAIT/CLOSE_WAIT 连接
- 防火墙 / 安全组限制
第三步:应用层面排查 🧩
1. JVM 层面
- GC 情况:重点关注 Full GC 频率和耗时,频繁 Full GC 是性能杀手
- 线程状态:查看是否有大量 BLOCKED/WAITING 线程,排查死锁
- 堆内存:分析堆转储文件,找出占用内存最多的对象
2. 数据库层面
- 🔍 查看慢查询日志,找出执行时间长的 SQL
- 📊 分析 SQL 执行计划,检查是否有全表扫描
- 🔄 检查数据库连接池配置,是否连接数不足
- 💾 查看数据库锁情况,是否有行锁 / 表锁冲突
3. 中间件层面
- Redis:查看命中率、连接数、慢查询
- MQ:查看消息堆积情况、消费者消费速度
- Nginx:查看请求数、响应时间、错误日志
4. 代码层面
- 循环中调用数据库 / 远程接口
- 同步锁使用不当导致并发度低
- 大对象创建和销毁频繁
- 未使用缓存或缓存失效策略不合理
第四步:核心排查工具代码(技术亮点)🔧
1. 线程死锁自动检测工具类
/**
* 死锁检测工具类 - 技术亮点:利用JMX API实现无侵入式死锁检测
* 可集成到监控系统中,定时检测并告警
*/
@Component
public class DeadlockDetector {
private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
@Scheduled(fixedRate = 60000) // 每分钟检测一次
public void detectDeadlock() {
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads, true, true);
StringBuilder sb = new StringBuilder();
sb.append("⚠️ 检测到死锁!涉及线程数:").append(deadlockedThreads.length).append("\n");
for (ThreadInfo info : threadInfos) {
sb.append("\n线程名称:").append(info.getThreadName())
.append("\n线程ID:").append(info.getThreadId())
.append("\n锁名称:").append(info.getLockName())
.append("\n锁持有者:").append(info.getLockOwnerName())
.append("\n堆栈信息:\n");
for (StackTraceElement ste : info.getStackTrace()) {
sb.append(" at ").append(ste).append("\n");
}
}
// 发送告警邮件/短信/钉钉
log.error(sb.toString());
alertService.sendAlert(sb.toString());
}
}
}2. 接口性能监控 AOP 切面
/**
* 接口性能监控切面 - 技术亮点:统一拦截所有接口,自动记录慢请求
* 支持自定义慢请求阈值,自动输出方法参数和返回值
*/
@Aspect
@Component
@Slf4j
public class PerformanceMonitorAspect {
@Value("${performance.slow-threshold:1000}") // 慢请求阈值,默认1秒
private long slowThreshold;
@Around("execution(* com.example.service..*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
if (costTime > slowThreshold) {
log.warn("⚠️ 慢请求检测:方法={}, 耗时={}ms, 参数={}",
methodName, costTime, Arrays.toString(args));
}
return result;
} catch (Throwable e) {
long endTime = System.currentTimeMillis();
log.error("❌ 方法执行异常:方法={}, 耗时={}ms, 参数={}",
methodName, endTime - startTime, Arrays.toString(args), e);
throw e;
}
}
}3. 内存泄漏检测工具类
/**
* 内存泄漏检测工具类 - 技术亮点:定时采集堆内存使用情况,自动生成堆转储
* 当老年代使用率超过阈值时自动dump堆内存,便于事后分析
*/
@Component
@Slf4j
public class MemoryLeakDetector {
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
@Value("${memory.leak-threshold:80}") // 老年代使用率阈值,默认80%
private double leakThreshold;
@Scheduled(fixedRate = 300000) // 每5分钟检测一次
public void detectMemoryLeak() {
MemoryUsage oldGenUsage = getOldGenMemoryUsage();
if (oldGenUsage != null) {
double usagePercent = (double) oldGenUsage.getUsed() / oldGenUsage.getMax() * 100;
log.info("老年代内存使用情况:{}%", String.format("%.2f", usagePercent));
if (usagePercent > leakThreshold) {
log.warn("⚠️ 老年代内存使用率超过阈值:{}%,即将生成堆转储文件", usagePercent);
try {
String fileName = "heapdump-" + System.currentTimeMillis() + ".hprof";
HotSpotDiagnosticMXBean hotspotMBean = ManagementFactory.newPlatformMXBeanProxy(
ManagementFactory.getPlatformMBeanServer(),
"com.sun.management:type=HotSpotDiagnostic",
HotSpotDiagnosticMXBean.class);
hotspotMBean.dumpHeap(fileName, true);
log.info("堆转储文件已生成:{}", fileName);
// 发送告警
alertService.sendAlert("老年代内存使用率超过" + leakThreshold + "%,已生成堆转储文件");
} catch (Exception e) {
log.error("生成堆转储文件失败", e);
}
}
}
}
private MemoryUsage getOldGenMemoryUsage() {
for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
if (pool.getType() == MemoryType.HEAP && pool.getName().contains("Old Gen")) {
return pool.getUsage();
}
}
return null;
}
}第五步:技术难点与解决方案对照表 🎯
| 技术难点 | 难点描述 | 解决方案 | 技术亮点 |
|---|---|---|---|
| 间歇性性能问题难以复现 | 问题只在特定时间 / 特定流量下出现,无法在测试环境复现 | 1. 部署全链路追踪系统(SkyWalking/Pinpoint) 2. 开启详细日志记录 3. 增加 JVM 和系统指标的采集频率 4. 使用流量录制工具(Tcpdump/Wireshark) | 全链路追踪可以记录每个请求的完整调用链,包括每个方法的耗时和参数 |
| 分布式系统中的性能瓶颈定位 | 请求经过多个服务,难以确定是哪个服务导致的慢 | 1. 使用分布式追踪系统 2. 在每个服务入口和出口添加性能监控 3. 统一日志格式,添加 traceId 4. 压测时逐个服务隔离测试 | 分布式追踪系统可以将一个请求在多个服务中的调用过程串联起来 |
| 内存泄漏的根因分析 | 堆转储文件很大,分析起来耗时费力 | 1. 使用 MAT/JProfiler 等专业工具分析 2. 对比多个时间点的堆转储文件 3. 重点关注大对象和增长最快的对象 4. 检查静态集合类和线程局部变量 | MAT 的 "支配树" 视图可以快速找出占用内存最多的对象 |
| 死锁的检测与预防 | 死锁发生时系统会卡住,但很难定位具体原因 | 1. 使用 Jstack 命令查看线程栈 2. 集成死锁自动检测工具 3. 避免嵌套锁 4. 使用 tryLock () 方法设置超时时间 5. 统一加锁顺序 | 自动死锁检测工具可以在死锁发生时立即告警并记录详细信息 |
| 高并发下的性能调优 | 系统在低并发下正常,高并发下性能急剧下降 | 1. 优化数据库查询和索引 2. 使用缓存减少数据库压力 3. 异步化处理非核心逻辑 4. 调整线程池参数 5. 进行水平扩展 | 异步化可以将同步调用转为异步,提高系统吞吐量 |
常见问题与解决方案对照表 📋
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| CPU 使用率 100%,但业务量不大 | 死循环、无限递归 | 用 jstack 定位到具体线程和代码 |
| 频繁 Full GC,系统卡顿 | 内存泄漏、堆内存过小 | 分析堆转储文件,修复泄漏点;调整 JVM 参数 |
| 接口响应时间忽快忽慢 | 数据库查询未加索引 | 给查询条件添加合适的索引 |
| 系统突然变慢,然后恢复 | 突发流量导致缓存雪崩 | 优化缓存策略,添加熔断降级 |
| 磁盘 IO 使用率 100% | 大量日志写入、数据库全表扫描 | 调整日志级别;优化 SQL 查询 |
面试官加分项 ✨
- 有优先级意识:先排除简单问题,再深入复杂问题
- 有数据支撑:不是凭感觉,而是用命令和工具获取数据
- 有全局观:不仅考虑服务器本身,还考虑网络、数据库、中间件等
- 有经验沉淀:能说出自己曾经遇到过的类似问题和解决过程
- 有预防意识:提到监控告警、性能压测等事前预防措施
- 有工具思维:能自己开发工具辅助排查,而不是只依赖现成工具
回答话术参考(真实面试场景)
" 面试官您好,服务器反应慢我会按照先宏观后微观的思路来排查:
首先,我会先确认问题范围,是个别用户还是所有用户都慢,是突然变慢还是逐渐变慢,是所有接口还是特定接口慢。这一步能帮我快速排除网络、CDN 等非服务器问题。
然后,我会先看系统层面的四大指标:CPU、内存、磁盘 IO 和网络 IO。用 top、free、iostat、netstat 这些命令快速定位是哪个资源出现了瓶颈。
如果系统层面没问题,我再深入应用层面。先看 JVM 的 GC 情况和线程状态,然后查数据库的慢查询和连接池,再看 Redis、MQ 这些中间件的状态,最后定位到具体的代码逻辑。
在实际工作中,我还开发了一些自动化工具来辅助排查,比如死锁自动检测工具、接口性能监控切面和内存泄漏检测工具。这些工具可以在问题发生时自动告警并收集必要的信息,大大提高了排查效率。
最后,解决问题后我会进行复盘,添加相应的监控告警,避免类似问题再次发生。"
真实面试模拟
真实面试模拟
面试官 🧑🏫:
同学你好,今天我们聊一道很常见的线上问题排查题——用户反馈服务器反应突然变慢了,你从哪儿着手? 不用紧张,就当是咱们在一起复盘一个真实故障,你来说说思路。
候选人 🧑💻:
好的面试官,我一般会按 “从外到内、从资源到应用” 的层次来缩小范围,避免上来就盯着代码瞎猜。我先按顺序拆成 6 步跟您说。
🧑🏫 面试官:
第一步你会做什么?
候选人 🧑💻:
第一步肯定是定性 & 定界。先看监控大盘,确认是全站慢还是某个接口慢,QPS 和 RT 有没有突变。然后快速排除前端和网络:
- 浏览器 F12 看接口耗时、静态资源是否过大;
ping、mtr看网络延迟和丢包;- 检查负载均衡、带宽打满没有。
如果明确是后端服务 RT 飙高,我就会登录服务器,进入第二步——系统资源检查。
🧑🏫 面试官:
嗯,登机之后你敲什么命令?
候选人 🧑💻💻:
先看四大件:CPU、内存、磁盘 IO、网络。
top # 看负载、CPU sy/us/wa
free -h # 内存使用
iostat -x # 磁盘 IO 利用率
sar -n DEV # 网络流量这几个一跑,基本就能看出哪块资源紧张了。
🧑🏫 面试官:
那如果看到 CPU 飙高,怎么往下定位?
🧑💻:
我会直接用 Java 进程的诊断连招:
top -Hp <pid>找出 CPU 最高的线程号;printf '%x\n' <线程号>转成十六进制;jstack <pid> | grep <十六进制>就能直接看到是哪段代码在消耗 CPU。
90% 的情况这样就能找到死循环、正则回溯或大对象序列化的问题。
如果现场允许,我更习惯用 Arthas,一个 thread -n 3 直接列出最忙的三个线程,省掉手工转进制的时间。
🧑🏫 面试官:
说得好,内存这块呢?频繁 Full GC 你怎么查?
🧑💻:
内存问题我会分两步看:
- 实时观察 GC:
jstat -gcutil <pid> 1000每秒看各区使用率和 GC 次数,如果 Full GC 很频繁,说明堆里有大对象或者泄漏。 - 定位对象:
jmap -histo <pid> | head -20看哪些对象实例特别多;需要深入就jmap -dump把堆导出来用 MAT 分析,查找 GC Root 引用链。
另外我还会检查是不是因为日志打印过多、swap 分区使用导致磁盘 IO 高拖慢了整体,这一点 iostat 结合 lsof 就能判断。
🧑🏫 面试官:不错,那如果系统资源看起来都还好,就是接口响应慢,你怎么切入?
🧑💻:
那就要进到 JVM / 应用层 深挖了。我一般两个方向同时走:
- 看线程状态:
jstack重点关注BLOCKED、WAITING、TIMED_WAITING的线程,一旦大量线程等在同一个锁上,基本上就是死锁或锁竞争,Arthas 的thread -b能一键找出死锁。 - 分布式链路追踪:用 SkyWalking 或 Jaeger 找出一条慢调用链,定位到具体 span 是哪个方法耗时高,然后再用 Arthas 的
trace命令精确打印方法内部各步骤耗时。
trace com.xxx.service.OrderService getOrderDetail这种模式查出来的,80% 都是慢 SQL、缓存失效或者下游接口超时引起的。
🧑🏫 面试官:你提到了慢 SQL 和缓存,这两类中间件问题你怎么验证?
🧑💻:
会直接查中间件本身的诊断接口:
数据库:
- 开启慢查询日志,拿到 SQL 用
EXPLAIN看执行计划; - 查连接池监控(Druid / HikariPool),如果活跃连接打满,说明有 SQL 阻塞;
SHOW ENGINE INNODB STATUS看锁等待和事务状态。
- 开启慢查询日志,拿到 SQL 用
缓存:
redis-cli slowlog get 10看最近慢命令;redis-cli --bigkeys检查大 key;- 同时结合业务日志判断是否发生缓存穿透、击穿或雪崩。
🧑💻:
我一般会把整个排查路径总结成一个流程,方便和同事对齐思路:
按这个路线走完,基本 90% 的“变慢”问题都能定位出来,最后再用 JMeter 复现对比,验证修复效果。
🧑🏫 面试官:挺好,你这套思路很清晰。再问你一句,平时排查时哪些工具你觉得最顺手?
🧑💻:我整理了一个速查表,遇到问题直接对号入座:
| 现象 | 最可能原因 | 我首选工具/命令 |
|---|---|---|
| CPU 持续高 | 死循环、正则回溯 | top -Hp + jstack,Arthas thread -n |
| 频繁 YGC/FGC | 朝生夕灭对象多、泄漏 | jstat -gcutil,jmap + MAT |
| 大量 BLOCKED 线程 | 死锁或锁竞争 | jstack,Arthas thread -b |
| 单接口 RT 突然变高 | 慢 SQL、缓存失效 | SkyWalking 链路 + Arthas trace |
| Redis 响应慢 | 大 key、慢命令 | slowlog get,--bigkeys |
面试官 🧑🏫:
同学,前面你把端到端的排查流程讲得很清楚。那顺着这个思路,你能不能秀一段你平时自己写的、带技术亮点的排查代码?比如脚本或者诊断工具类的,我们很想看到你解决实际问题的动手能力。
🧑💻 候选人:
好的,我分享两个我一直在用的“压箱底”小工具,一个是 Shell 脚本一键定位 CPU 飙高线程,另一个是用 JDK 自带 API 实现死锁监控,都能直接贴在线上使用。
🔥 亮点代码 1:一键揪出 Java 进程中的“CPU 刺客”
场景:半夜告警 CPU 90%,不用一步步手工敲 top -Hp,直接跑这个脚本,一步到位打印出元凶线程的堆栈。
#!/bin/bash
# 用法:./find_cpu_hog.sh [PID],不传 PID 则自动探测第一个 Java 进程
pid=${1:-$(jps | grep -v Jps | awk '{print $1}' | head -1)}
[ -z "$pid" ] && echo "❌ 找不到 Java 进程" && exit 1
echo "🔍 开始探测 PID=$pid 的 CPU 热点线程..."
# 1. 找到 CPU 占用最高的线程
high_thread=$(top -Hp $pid -bn1 | grep -v "top -H" | sort -k9,9nr | awk 'NR==2{print $1}')
# 2. 转为 16 进制
thread_hex=$(printf '%x\n' $high_thread)
# 3. 拉取栈信息并过滤目标线程
jstack $pid | awk 'BEGIN{RS="\n\n"} /nid=0x'"$thread_hex"'/ {print $0}'技术亮点:
- 用
top -Hp -bn1非交互模式,无人工干预,可接入告警机器人。 jstack结合awk多行记录分隔符RS="\n\n"精确截取一个线程的完整栈,避免手工翻几百行。
🧠 亮点代码 2:纯 Java 实现死锁检测并暴露给监控
场景:接口突然全部 BLOCKED,为了避免每次都手动 jstack,我把死锁检测直接内嵌在应用里,通过 /actuator/health 暴露,让 Prometheus 自动报警。
@Component
public class DeadlockHealthIndicator implements HealthIndicator {
@Override
public Health health() {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = mxBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
StringBuilder detail = new StringBuilder("死锁线程ID: ");
for (long id : deadlockedThreads) {
ThreadInfo info = mxBean.getThreadInfo(id);
detail.append(info.getThreadName()).append("; ");
}
return Health.down().withDetail("deadlock", detail.toString()).build();
}
return Health.up().build();
}
}技术亮点:
- 利用
ThreadMXBean.findDeadlockedThreads()原生 API,零依赖检测synchronized 和 ReentrantLock 死锁。 - 接入 Spring Boot Actuator,结合 Prometheus AlertManager 可以在死锁发生 10 秒内告警,极大缩短 MTTR。
面试官 🧑🏫:
不错,这些代码非常接地气,能看出你深度排查的功底。最后,你能不能总结一下“服务器变慢”这个场景中,你踩过最深的坑(技术难点),以及你是怎么解决的?
🧑💻:
没问题,我整理了 5 个核心难点与解决方案,每次复盘都会拿出来对照。
| 技术难点 🔥 | 典型表现 | 解决方案 ✅ |
|---|---|---|
| 偶发性慢,无法稳定复现 | 用户偶尔卡 3 秒,马上又正常,监控无明显尖刺 | 增强埋点:关键方法用 StopWatch 记录耗时,超过阈值打印完整调用链;开启 JFR (Java Flight Recorder) 低开销持续录制,事后用 JMC 分析。 |
| 分布式调用链中某环节“假正常” | 链路追踪显示 A 服务正常,实际上 A 的线程池排队严重导致延迟 | 在线程池处埋点监控队列长度、等待时间;用 SkyWalking 插件自定义增强,从消费者视角记录“提交到执行”的排队时间。 |
| GC 停顿但堆大小正常 | STW 时间变长,每次 GC 回收内存却不多,对象分配速率高 | 搭配 -XX:+PrintReferenceGC 检查软/弱引用处理耗时;用 JFR 的 Allocation Profiling 找出高频创建对象的代码路径,针对性做对象池化或批处理。 |
| 日志打印成为性能杀手 | 磁盘 IO 不高,但 iostat 显示单次写延迟大,日志文件巨大 | 开启日志异步输出 (AsyncAppender);合理控制日志级别;使用占位符代替字符串拼接;对大日志文件做轮转和压缩。 |
| 缓存“误命中”导致拖垮数据库 | 热点数据过期瞬间,大量请求直落 DB,接口 RT 暴涨 | 实现缓存互斥更新或“永不过期+异步刷新”策略;用 Redisson 的 RLock 控制回源并发;搭建二级缓存(Caffeine+Redis)减少击穿风险。 |
这些坑基本上都是线上血泪换来的,我把它们沉淀成了团队故障手册,新人一来就能照着查 🚑。
面试官 🧑🏫:
非常好!从分层排查 → 硬核脚本 → 内嵌诊断 → 难点沉淀,这条链路完整展示了高级工程师的系统化思维。今天这场面试就到这里,表现很出色 👍。
候选人 🧑💻:
谢谢面试官,这些问题也帮我重新梳理了知识体系,收获很大! 🌟
