线上服务器反应慢如何排查

面试官视角:这道题考察什么?

这道题是大厂必考题,考察的不是你背了多少命令,而是你的系统思维能力问题定位方法论。优秀的回答应该体现出 "先宏观后微观,先整体后局部"的排查思路,而不是上来就说" 看日志 "、" 查 CPU"。

整体排查思路流程图 📊

image

第一步:快速确认问题范围 ⚡

30 秒黄金判断期,先排除非服务器问题:

  • 📶 是不是所有用户都慢?还是只有部分地区 / 网络的用户?
  • 🕒 是突然变慢还是逐渐变慢?突然变慢大概率是突发流量、死锁、OOM 前兆
  • 🔗 是所有接口都慢?还是只有特定接口?特定接口慢直接查对应业务逻辑

第二步:系统层面排查(四大金刚)💻

1. CPU 排查

# 查看CPU整体使用情况
top -c
# 按CPU使用率排序
top -c -o %CPU
# 查看进程内线程CPU使用情况
top -Hp 
# 将线程ID转为16进制,用于后续查栈
printf "%x\n" 
# 查看Java线程栈
jstack  | grep <16进制tid> -A 20

常见问题:

  • 死循环 / 无限递归
  • 频繁 GC(特别是 Full GC)
  • 大量线程上下文切换

2. 内存排查

# 查看内存使用情况
free -h
# 查看进程内存占用
ps aux | sort -k4r | head -10
# 查看JVM内存使用情况
jstat -gc  1000 10
# 生成堆转储文件
jmap -dump:format=b,file=heapdump.hprof 

常见问题:

  • 内存泄漏(对象无法被 GC 回收)
  • 堆内存设置过小
  • 大对象创建过多

3. 磁盘 IO 排查

# 查看磁盘IO使用率
iostat -x 1 10
# 查看哪个进程在读写磁盘
iotop
# 查看磁盘空间使用情况
df -h
# 查看目录大小
du -sh /*

常见问题:

  • 磁盘空间满(日志文件未清理)
  • 大量随机读写(数据库索引未优化)
  • 磁盘硬件故障

4. 网络 IO 排查

# 查看网络连接情况
netstat -an | grep 
# 查看TCP连接状态统计
ss -s
# 查看网络流量
iftop
# 测试网络延迟
ping <服务器IP>
# 测试端口连通性
telnet <服务器IP> 

常见问题:

  • 网络带宽打满
  • 大量 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. 统一日志格式,添加 traceId4. 压测时逐个服务隔离测试 分布式追踪系统可以将一个请求在多个服务中的调用过程串联起来
内存泄漏的根因分析 堆转储文件很大,分析起来耗时费力 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 查询

面试官加分项 ✨

  1. 有优先级意识:先排除简单问题,再深入复杂问题
  2. 有数据支撑:不是凭感觉,而是用命令和工具获取数据
  3. 有全局观:不仅考虑服务器本身,还考虑网络、数据库、中间件等
  4. 有经验沉淀:能说出自己曾经遇到过的类似问题和解决过程
  5. 有预防意识:提到监控告警、性能压测等事前预防措施
  6. 有工具思维:能自己开发工具辅助排查,而不是只依赖现成工具

回答话术参考(真实面试场景)

" 面试官您好,服务器反应慢我会按照先宏观后微观的思路来排查:

首先,我会先确认问题范围,是个别用户还是所有用户都慢,是突然变慢还是逐渐变慢,是所有接口还是特定接口慢。这一步能帮我快速排除网络、CDN 等非服务器问题。

然后,我会先看系统层面的四大指标:CPU、内存、磁盘 IO 和网络 IO。用 top、free、iostat、netstat 这些命令快速定位是哪个资源出现了瓶颈。

如果系统层面没问题,我再深入应用层面。先看 JVM 的 GC 情况和线程状态,然后查数据库的慢查询和连接池,再看 Redis、MQ 这些中间件的状态,最后定位到具体的代码逻辑。

在实际工作中,我还开发了一些自动化工具来辅助排查,比如死锁自动检测工具、接口性能监控切面和内存泄漏检测工具。这些工具可以在问题发生时自动告警并收集必要的信息,大大提高了排查效率。

最后,解决问题后我会进行复盘,添加相应的监控告警,避免类似问题再次发生。"

真实面试模拟

真实面试模拟

面试官 🧑‍🏫:

同学你好,今天我们聊一道很常见的线上问题排查题——用户反馈服务器反应突然变慢了,你从哪儿着手? 不用紧张,就当是咱们在一起复盘一个真实故障,你来说说思路。

候选人 🧑‍💻:

好的面试官,我一般会按 “从外到内、从资源到应用” 的层次来缩小范围,避免上来就盯着代码瞎猜。我先按顺序拆成 6 步跟您说。

🧑‍🏫 面试官:

第一步你会做什么?

候选人 🧑‍💻:

第一步肯定是定性 & 定界。先看监控大盘,确认是全站慢还是某个接口慢,QPS 和 RT 有没有突变。然后快速排除前端和网络:

  • 浏览器 F12 看接口耗时、静态资源是否过大;
  • pingmtr 看网络延迟和丢包;
  • 检查负载均衡、带宽打满没有。

如果明确是后端服务 RT 飙高,我就会登录服务器,进入第二步——系统资源检查

🧑‍🏫 面试官:

嗯,登机之后你敲什么命令?

候选人 🧑‍💻💻:

先看四大件:CPU、内存、磁盘 IO、网络。

top        # 看负载、CPU sy/us/wa
free -h    # 内存使用
iostat -x  # 磁盘 IO 利用率
sar -n DEV # 网络流量

这几个一跑,基本就能看出哪块资源紧张了。

🧑‍🏫 面试官:

那如果看到 CPU 飙高,怎么往下定位?

🧑💻:

我会直接用 Java 进程的诊断连招:

  1. top -Hp 找出 CPU 最高的线程号;
  2. printf '%x\n' <线程号> 转成十六进制;
  3. jstack | grep <十六进制> 就能直接看到是哪段代码在消耗 CPU。

90% 的情况这样就能找到死循环、正则回溯或大对象序列化的问题。

如果现场允许,我更习惯用 Arthas,一个 thread -n 3 直接列出最忙的三个线程,省掉手工转进制的时间。

🧑‍🏫 面试官:

说得好,内存这块呢?频繁 Full GC 你怎么查?

🧑💻:

内存问题我会分两步看:

  • 实时观察 GCjstat -gcutil 1000 每秒看各区使用率和 GC 次数,如果 Full GC 很频繁,说明堆里有大对象或者泄漏。
  • 定位对象jmap -histo | head -20 看哪些对象实例特别多;需要深入就 jmap -dump 把堆导出来用 MAT 分析,查找 GC Root 引用链。

另外我还会检查是不是因为日志打印过多、swap 分区使用导致磁盘 IO 高拖慢了整体,这一点 iostat 结合 lsof 就能判断。

🧑‍🏫 面试官:不错,那如果系统资源看起来都还好,就是接口响应慢,你怎么切入?

🧑💻:

那就要进到 JVM / 应用层 深挖了。我一般两个方向同时走:

  1. 看线程状态jstack 重点关注 BLOCKEDWAITINGTIMED_WAITING 的线程,一旦大量线程等在同一个锁上,基本上就是死锁或锁竞争,Arthas 的 thread -b 能一键找出死锁。
  2. 分布式链路追踪:用 SkyWalking 或 Jaeger 找出一条慢调用链,定位到具体 span 是哪个方法耗时高,然后再用 Arthas 的 trace 命令精确打印方法内部各步骤耗时。
trace com.xxx.service.OrderService getOrderDetail

这种模式查出来的,80% 都是慢 SQL、缓存失效或者下游接口超时引起的。

🧑‍🏫 面试官:你提到了慢 SQL 和缓存,这两类中间件问题你怎么验证?

🧑💻:

会直接查中间件本身的诊断接口:

  • 数据库

    • 开启慢查询日志,拿到 SQL 用 EXPLAIN 看执行计划;
    • 查连接池监控(Druid / HikariPool),如果活跃连接打满,说明有 SQL 阻塞;
    • SHOW ENGINE INNODB STATUS 看锁等待和事务状态。
  • 缓存

    • redis-cli slowlog get 10 看最近慢命令;
    • redis-cli --bigkeys 检查大 key;
    • 同时结合业务日志判断是否发生缓存穿透、击穿或雪崩。

🧑‍💻:

我一般会把整个排查路径总结成一个流程,方便和同事对齐思路:

image

按这个路线走完,基本 90% 的“变慢”问题都能定位出来,最后再用 JMeter 复现对比,验证修复效果。

🧑‍🏫 面试官:挺好,你这套思路很清晰。再问你一句,平时排查时哪些工具你觉得最顺手?

🧑💻:我整理了一个速查表,遇到问题直接对号入座:

现象 最可能原因 我首选工具/命令
CPU 持续高 死循环、正则回溯 top -Hp + jstack,Arthas thread -n
频繁 YGC/FGC 朝生夕灭对象多、泄漏 jstat -gcutiljmap + 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,零依赖检测synchronizedReentrantLock 死锁。
  • 接入 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)减少击穿风险。

这些坑基本上都是线上血泪换来的,我把它们沉淀成了团队故障手册,新人一来就能照着查 🚑。

面试官 🧑‍🏫:

非常好!从分层排查 → 硬核脚本 → 内嵌诊断 → 难点沉淀,这条链路完整展示了高级工程师的系统化思维。今天这场面试就到这里,表现很出色 👍。

候选人 🧑‍💻:

谢谢面试官,这些问题也帮我重新梳理了知识体系,收获很大! 🌟


公众号“Rain的Java大神之路”
个人博客“www.javadashen.com”


原文地址: https://www.cveoy.top/t/topic/qHj4 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录