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

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

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

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

整体排查思路流程图 📊

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

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

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

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

1. CPU 排查

shell 复制代码
# 查看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. 内存排查

shell 复制代码
# 查看内存使用情况
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 排查

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

常见问题:

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

4. 网络 IO 排查

shell 复制代码
# 查看网络连接情况
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. 线程死锁自动检测工具类

java 复制代码
/**
 * 死锁检测工具类 - 技术亮点:利用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 切面

java 复制代码
/**
 * 接口性能监控切面 - 技术亮点:统一拦截所有接口,自动记录慢请求
 * 支持自定义慢请求阈值,自动输出方法参数和返回值
 */
@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. 内存泄漏检测工具类

java 复制代码
/**
 * 内存泄漏检测工具类 - 技术亮点:定时采集堆内存使用情况,自动生成堆转储
 * 当老年代使用率超过阈值时自动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、网络。

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

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

🧑‍🏫 面试官:

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

🧑💻:

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

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

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

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

🧑‍🏫 面试官:

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

🧑💻:

内存问题我会分两步看:

  • 实时观察 GCjstat -gcutil <pid> 1000 每秒看各区使用率和 GC 次数,如果 Full GC 很频繁,说明堆里有大对象或者泄漏。
  • 定位对象jmap -histo <pid> | 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 命令精确打印方法内部各步骤耗时。
shell 复制代码
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;
    • 同时结合业务日志判断是否发生缓存穿透、击穿或雪崩。

🧑‍💻:

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

按这个路线走完,基本 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,直接跑这个脚本,一步到位打印出元凶线程的堆栈。

shell 复制代码
#!/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 自动报警。

java 复制代码
@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"

相关推荐
Rain的Java大神实战圈1 天前
如何保证MQ消息顺序消费
场景设计题
Rain的Java大神实战圈2 天前
线上MQ消息积压了怎么处理
场景设计题
Rain的Java大神实战圈5 天前
介绍一下Redis的高可用方案
场景设计题
Rain的Java大神实战圈6 天前
如何保证Mysql和Redis双写一致性
场景设计题
Rain的Java大神实战圈7 天前
介绍一下Redis的缓存穿透击穿雪崩
场景设计题
Rain的Java大神实战圈8 天前
对千万级单表进行分页查询
场景设计题
Rain的Java大神实战圈9 天前
MySQL建表时需要注意什么
场景设计题
Rain的Java大神实战圈12 天前
千万级大表如何新增字段
场景设计题
Rain的Java大神实战圈14 天前
日增量500万的表结构如何设计
场景设计题