可观测性与故障排查(Observability & Troubleshooting)

架构师最值钱的时刻就是线上出问题的时候。本模块覆盖CPU飙高、内存泄漏、慢SQL、全链路追踪、OOM、网络故障等核心排障场景,每道题都是真实的生产案例,考察候选人在高压环境下的系统化排障能力。能不能在凌晨3点被叫醒后30分钟内定位问题,是区分”PPT架构师”和”真架构师”的关键。


难度标记

  • 🔵 高级(Senior):8-10年经验应该能答好
  • 🔴 专家(Expert):需要深入的实战经验和思考
  • ⚫ 大师(Master):开放性设计题,考察架构哲学和权衡能力

一、CPU问题排查(1-10题)

1. 🔵 线上Java服务CPU飙到100%,请描述你的完整排查流程。

答:CPU飙高是最常见的线上问题,需要系统化的排查流程。

标准排查流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Step 1:确认是哪个进程占用CPU
top -c
# 找到CPU占用最高的Java进程PID,假设是12345

# Step 2:找到该进程中CPU占用最高的线程
top -Hp 12345
# 找到CPU最高的线程TID,假设是12378

# Step 3:将线程ID转为16进制
printf "%x\n" 12378
# 输出:305a

# Step 4:用jstack导出线程堆栈
jstack 12345 | grep -A 30 "0x305a"
# 找到对应线程的堆栈信息

# Step 5:分析堆栈
# 常见原因:
# - 死循环:堆栈显示在某个循环中
# - 正则表达式回溯:java.util.regex
# - 频繁GC:GC线程占用CPU
# - 加密/序列化:大量计算操作

常见CPU飙高原因及对应堆栈特征:

  1. 死循环/无限递归:
1
2
3
4
"thread-1" #15 prio=5 RUNNABLE
at com.example.Service.process(Service.java:42)
at com.example.Service.process(Service.java:45) // 递归
...
  1. 频繁Full GC:
1
2
3
4
5
# 确认是否GC导致
jstat -gcutil 12345 1000
# 如果FGC频繁增长,Old区接近100%,说明是GC问题
# 进一步用jmap分析堆内存
jmap -histo:live 12345 | head -20
  1. 正则表达式灾难性回溯:
1
2
3
4
5
"thread-1" RUNNABLE
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
// 大量regex相关堆栈
  1. 线程上下文切换过多:
1
2
3
4
5
6
# 检查上下文切换
vmstat 1
# 如果cs(context switch)值很高(>10万/秒)
pidstat -w -p 12345 1
# 查看该进程的上下文切换详情
# 原因通常是线程数过多或锁竞争激烈

2. 🔴 线上服务CPU使用率不高(30%),但接口响应很慢,可能是什么原因?如何排查?

答:CPU不高但响应慢,说明线程大部分时间不在执行计算,而是在等待。

排查思路:

1
2
3
4
5
6
7
# Step 1:查看线程状态分布
jstack 12345 | grep "java.lang.Thread.State" | sort | uniq -c
# 如果大量线程处于WAITING/BLOCKED/TIMED_WAITING,说明在等待

# Step 2:分析等待原因
jstack 12345 > thread_dump.txt
# 搜索BLOCKED和WAITING的线程

常见原因:

  1. 锁竞争(BLOCKED):
1
2
3
4
5
6
7
8
"http-nio-8080-exec-1" BLOCKED
waiting to lock <0x00000007162a0e80> (a java.lang.Object)
at com.example.Service.syncMethod(Service.java:30)

"http-nio-8080-exec-2" RUNNABLE
locked <0x00000007162a0e80> (a java.lang.Object)
at com.example.Service.syncMethod(Service.java:35)
// 大量线程等待同一把锁
  1. 数据库连接池耗尽(WAITING):
1
2
3
4
5
6
7
8
"http-nio-8080-exec-1" WAITING
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
// 等待获取数据库连接

排查:
- 检查连接池配置(maximumPoolSize)
- 检查是否有慢SQL占用连接
- 检查是否有连接泄漏(获取后未归还)
  1. 外部服务调用慢(TIMED_WAITING):
1
2
3
4
5
6
7
8
9
"http-nio-8080-exec-1" TIMED_WAITING
at java.net.SocketInputStream.socketRead0(Native Method)
at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(...)
// 等待外部HTTP响应

排查:
- 检查下游服务的响应时间
- 检查网络延迟(ping/traceroute)
- 检查是否设置了合理的超时时间
  1. IO等待:
1
2
3
4
5
6
7
# 检查IO等待
iostat -x 1
# 如果await很高(>50ms),说明磁盘IO慢
# %iowait高说明CPU在等待IO

# 找到IO高的进程
iotop -p 12345

3. 🔴 如何区分CPU使用率高是由用户态代码还是内核态代码导致的?不同情况的排查方向有什么不同?

答:区分用户态和内核态CPU占用对定位问题方向至关重要。

查看方法:

1
2
3
4
5
6
7
8
9
10
# top命令中的CPU列
top
# us(user):用户态CPU占用
# sy(system):内核态CPU占用
# wa(iowait):IO等待
# si(softirq):软中断

# 更精确的查看
mpstat -P ALL 1
# 每个CPU核心的us/sy/wa/si分布

用户态CPU高(us高):

1
2
3
4
5
6
7
8
9
10
11
原因:应用代码本身的计算密集
排查方向:
- jstack看线程堆栈,找到热点代码
- 使用async-profiler做CPU火焰图
./profiler.sh -d 30 -f cpu.html 12345
- 常见原因:
- 死循环、复杂计算
- JSON/XML序列化大对象
- 正则表达式回溯
- 加密解密操作
- GC(GC线程也是用户态)

内核态CPU高(sy高):

1
2
3
4
5
6
7
8
9
10
11
12
13
原因:大量系统调用或内核操作
排查方向:
- strace跟踪系统调用
strace -cp 12345 # 统计系统调用分布
strace -T -p 12345 # 查看每个系统调用的耗时
- perf分析内核热点
perf top -p 12345
- 常见原因:
- 大量线程创建/销毁(clone/futex系统调用多)
- 频繁的内存分配/释放(mmap/munmap多)
- 网络IO密集(sendto/recvfrom多)
- 锁竞争(futex系统调用多)
- 文件描述符操作频繁(open/close/read/write多)

4. 🔴 什么是CPU火焰图(Flame Graph)?如何生成和解读?请描述一次用火焰图定位性能问题的经历。

答:火焰图是Brendan Gregg发明的性能分析可视化工具,能直观展示CPU时间花在哪些函数调用上。

生成方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 方法1:async-profiler(推荐,Java专用)
# 下载:https://github.com/async-profiler/async-profiler
./profiler.sh -d 30 -f flame.html -o flamegraph 12345
# -d 30:采样30秒
# -f:输出文件
# 12345:Java进程PID

# 方法2:perf + FlameGraph(通用,支持所有语言)
perf record -g -p 12345 -- sleep 30
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > flame.svg

# 方法3:Arthas(阿里开源的Java诊断工具)
profiler start
# 等待30秒
profiler stop --format html --file /tmp/flame.html

解读方法:

1
2
3
4
5
6
7
8
9
火焰图的结构:
- X轴:函数调用的宽度代表CPU占用比例(越宽占用越多)
- Y轴:调用栈深度(从下到上是调用链)
- 颜色:无特殊含义,只是为了区分不同函数

关注点:
1. 最宽的"平顶":CPU时间最多的函数
2. 宽且深的调用链:可能是递归或深层调用
3. 意外出现的宽函数:如GC、序列化、加密

实战案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
问题:某服务CPU从20%突然飙到80%

火焰图分析:
- 发现com.fasterxml.jackson.databind.ser.std.MapSerializer
占了60%的CPU
- 追踪调用链:是一个接口返回了一个巨大的Map(10万个Key)
- 每次请求都在序列化这个大Map

根因:
- 上游服务变更,返回数据量从100条变成10万条
- 下游服务没有做分页,全量序列化

修复:
- 增加分页参数,限制单次返回数据量
- 增加响应体大小监控告警

5. 🔵 Java应用频繁Full GC导致CPU飙高,如何排查和解决?

答:Full GC是Java应用CPU飙高的最常见原因之一。

排查流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Step 1:确认GC情况
jstat -gcutil 12345 1000
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 0.00 100 99.8 95.2 92.1 1523 12.3 89 45.6 57.9
# O(Old区)接近100%,FGC频繁 → 确认是Full GC问题

# Step 2:查看GC日志(如果开启了)
# JDK 8: -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# JDK 11+: -Xlog:gc*:file=gc.log:time,uptime,level,tags

# Step 3:导出堆内存快照
jmap -dump:live,format=b,file=heap.hprof 12345
# 注意:jmap会触发Full GC,线上慎用
# 替代方案:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/

# Step 4:用MAT(Memory Analyzer Tool)分析堆快照
# 重点看:
# - Leak Suspects Report:自动分析可能的内存泄漏
# - Dominator Tree:按对象大小排序
# - Histogram:按类统计对象数量和大小

常见原因及解决方案:

  1. 内存泄漏:
1
2
3
4
5
6
7
8
9
10
现象:Old区持续增长,Full GC后回收不了多少
原因:
- 静态集合不断添加元素(如static Map)
- 未关闭的资源(Connection、Stream)
- 监听器/回调未注销
- ThreadLocal未清理

解决:
- MAT分析找到泄漏对象
- 修复代码,确保资源正确释放
  1. 大对象直接进入Old区:
1
2
3
4
5
6
7
8
9
现象:Young GC正常,但Old区快速填满
原因:
- 大数组、大字符串直接分配到Old区
- -XX:PretenureSizeThreshold设置过小

解决:
- 检查是否有不合理的大对象分配
- 调整PretenureSizeThreshold
- 优化代码,避免创建大对象
  1. 堆内存设置不合理:
1
2
3
4
5
6
7
现象:频繁GC但每次都能回收大量空间
原因:堆太小,正常对象就把堆填满了

解决:
- 增大堆内存(-Xmx)
- 调整Young/Old比例(-XX:NewRatio)
- 考虑升级GC算法(G1 → ZGC)

6. 🔴 线上出现CPU毛刺(偶尔飙高然后恢复),如何排查这种间歇性问题?

答:间歇性CPU毛刺比持续性高CPU更难排查,因为问题发生时可能来不及抓取现场。

排查策略:

  1. 持续监控 + 自动抓取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 方案1:cron定时抓取线程堆栈
# 每10秒抓一次,保留最近1小时的数据
*/10 * * * * jstack $(pgrep -f myapp) > /tmp/jstack_$(date +%s).txt

# 方案2:CPU超过阈值时自动抓取
#!/bin/bash
while true; do
cpu=$(top -bn1 -p 12345 | tail -1 | awk '{print $9}')
if (( $(echo "$cpu > 80" | bc -l) )); then
timestamp=$(date +%Y%m%d_%H%M%S)
jstack 12345 > /tmp/jstack_${timestamp}.txt
jmap -histo 12345 > /tmp/jmap_${timestamp}.txt
echo "CPU spike detected: ${cpu}% at ${timestamp}"
fi
sleep 5
done

# 方案3:使用async-profiler持续采样
./profiler.sh -d 3600 -f profile.jfr 12345
# 持续采样1小时,事后分析
  1. 常见毛刺原因:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a. GC停顿:
- CMS的Remark阶段或G1的Mixed GC
- 查看GC日志中的停顿时间
- 解决:调优GC参数或升级到ZGC

b. JIT编译:
- 热点代码触发JIT编译,编译期间CPU飙高
- 查看:-XX:+PrintCompilation
- 解决:预热(Warm-up)或AOT编译

c. 定时任务:
- 某个定时任务周期性执行重计算
- 查看:crontab和应用内的@Scheduled
- 解决:优化定时任务或错峰执行

d. 缓存过期风暴:
- 大量缓存同时过期,触发重建
- 解决:缓存过期时间加随机偏移

7. 🔵 如何使用Arthas进行线上Java应用的CPU问题诊断?

答:Arthas是阿里开源的Java诊断工具,无需重启应用即可诊断问题。

常用CPU诊断命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 启动Arthas
java -jar arthas-boot.jar
# 选择要诊断的Java进程

# 1. dashboard:总览
dashboard
# 显示线程、内存、GC等实时信息
# 重点看:CPU占用最高的线程、GC次数和耗时

# 2. thread:线程分析
thread -n 5 # CPU占用最高的5个线程
thread -b # 找出阻塞其他线程的线程(死锁检测)
thread --state BLOCKED # 查看所有BLOCKED状态的线程

# 3. profiler:火焰图
profiler start # 开始采样
profiler status # 查看采样状态
profiler stop --format html --file /tmp/flame.html # 停止并生成火焰图

# 4. trace:方法调用链路耗时
trace com.example.OrderService createOrder
# 输出每个子方法的调用耗时,快速定位慢方法

# 5. watch:观察方法参数和返回值
watch com.example.OrderService createOrder '{params, returnObj, throwExp}' -x 3
# 查看方法的入参、返回值、异常

# 6. monitor:方法调用统计
monitor com.example.OrderService createOrder -c 10
# 每10秒统计一次:调用次数、成功率、平均耗时

实战技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
场景:某接口偶尔超时

# 用trace找到慢的子方法
trace com.example.OrderService createOrder '#cost > 1000'
# 只显示耗时>1秒的调用

# 发现是queryOrder方法慢
trace com.example.OrderMapper queryOrder '#cost > 500'
# 继续下钻,发现是SQL执行慢

# 用watch查看慢SQL的参数
watch com.example.OrderMapper queryOrder '{params}' '#cost > 500' -x 3
# 发现某个参数导致全表扫描

8. 🔴 容器化环境(K8s Pod)中的CPU问题排查和物理机有什么不同?需要注意什么?

答:容器环境的CPU排查有几个关键差异。

差异1:CPU限制(CGroup)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
问题:容器设置了CPU limit,但top显示的CPU使用率是宿主机视角

# 容器内看到的CPU核数可能是宿主机的核数,不是limit的核数
# 例如:limit=2核,但容器内nproc显示64核

# 正确查看容器CPU限制
cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us # CPU配额(微秒)
cat /sys/fs/cgroup/cpu/cpu.cfs_period_us # CPU周期(微秒)
# quota/period = 可用CPU核数
# 例如:200000/100000 = 2核

# JVM需要感知容器CPU限制
# JDK 8u191+/JDK 10+自动识别
# 旧版本需要手动设置:-XX:ActiveProcessorCount=2

差异2:CPU Throttling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
问题:容器CPU使用率看起来不高,但响应慢

原因:CPU Throttling(节流)
- 容器在一个周期内用完了CPU配额,被CGroup暂停
- 表现为间歇性延迟

检查:
cat /sys/fs/cgroup/cpu/cpu.stat
# nr_throttled: 被节流的次数
# throttled_time: 被节流的总时间(纳秒)

# Kubernetes中查看
kubectl top pod <pod-name>
# 如果CPU使用率接近limit,很可能在被throttle

解决:
- 增加CPU limit
- 优化代码减少CPU使用
- 设置合理的request和limit比例(建议limit = 2×request)

差异3:工具可用性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
容器镜像通常是精简的,缺少诊断工具

解决方案:
1. 使用kubectl exec进入容器后安装工具
kubectl exec -it <pod> -- bash
apt-get update && apt-get install -y procps

2. 使用临时调试容器(Ephemeral Container)
kubectl debug -it <pod> --image=busybox --target=<container>

3. 使用nsenter从宿主机进入容器的命名空间
nsenter -t <pid> -m -u -i -n -p -- jstack <java-pid>

4. 预置诊断工具到基础镜像
FROM openjdk:17-slim
RUN apt-get update && apt-get install -y procps net-tools

9. 🔴 如何排查Java应用的线程死锁?

答:死锁是线程互相等待对方持有的锁,导致所有相关线程永久阻塞。

排查方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 方法1:jstack自动检测
jstack 12345
# jstack会在输出末尾自动报告检测到的死锁:
# Found one Java-level deadlock:
# =============================
# "thread-1":
# waiting to lock monitor 0x00007f8b3c003f08 (object 0x00000007162a0e80)
# which is held by "thread-2"
# "thread-2":
# waiting to lock monitor 0x00007f8b3c004068 (object 0x00000007162a1e90)
# which is held by "thread-1"

# 方法2:Arthas
thread -b # 直接找出阻塞源头

# 方法3:JMX(代码中检测)
ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = tmx.findDeadlockedThreads();
if (deadlockedThreads != null) {
ThreadInfo[] infos = tmx.getThreadInfo(deadlockedThreads, true, true);
// 记录死锁信息并告警
}

常见死锁模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 模式1:锁顺序不一致
// 线程1:lock(A) → lock(B)
// 线程2:lock(B) → lock(A)
// 解决:所有线程按相同顺序获取锁

// 模式2:数据库死锁
// 事务1:UPDATE account SET balance=900 WHERE id=1; -- 锁住id=1
// UPDATE account SET balance=1100 WHERE id=2; -- 等待id=2
// 事务2:UPDATE account SET balance=900 WHERE id=2; -- 锁住id=2
// UPDATE account SET balance=1100 WHERE id=1; -- 等待id=1
// 解决:按ID排序后再操作

// 模式3:线程池死锁
// 任务A在线程池中执行,提交任务B到同一线程池并等待结果
// 如果线程池满了,任务B无法执行,任务A永远等待
// 解决:使用不同的线程池,或使用CompletableFuture异步

10. 🔵 什么是Java的安全点(Safepoint)?它如何影响应用性能?如何排查Safepoint导致的延迟?

答:Safepoint是JVM中所有线程都暂停的点,GC、偏向锁撤销等操作需要在Safepoint执行。

Safepoint的影响:

1
2
3
4
5
6
7
8
9
10
11
问题:即使GC本身很快(如Young GC只需5ms),
但等待所有线程到达Safepoint可能需要很长时间

场景:
- 某个线程在执行一个大循环(如遍历大数组)
- JVM需要等这个线程到达Safepoint(循环回边或方法返回)
- 如果循环体内没有Safepoint,其他所有线程都要等待

JDK 8的问题:
- int类型的循环计数器,JIT编译后可能不插入Safepoint
- 导致长时间无法到达Safepoint(称为"Safepoint bias")

排查方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 开启Safepoint日志
# JDK 8:
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1

# JDK 11+:
-Xlog:safepoint=info

# 日志示例:
# Total time for which application threads were stopped: 0.0234567 seconds
# Stopping threads took: 0.0200000 seconds ← 等待线程到达Safepoint的时间
# 如果"Stopping threads"时间远大于GC时间,说明Safepoint等待是瓶颈

解决方案:

1
2
3
4
1. 避免大循环中使用int计数器(用long代替)
2. 在大循环中手动插入Safepoint:Thread.yield()
3. JDK 17+:-XX:+UseCountedLoopSafepoints(默认开启)
4. 使用ZGC/Shenandoah:大幅减少需要Safepoint的场景

二、内存问题排查(11-20题)

11. 🔵 Java应用发生OOM(OutOfMemoryError),有哪几种类型?分别是什么原因?

答:Java的OOM不只是堆内存溢出,有多种类型,排查方向完全不同。

OOM类型:

  1. java.lang.OutOfMemoryError: Java heap space
1
2
3
4
5
6
7
8
原因:堆内存不足
- 内存泄漏:对象持续创建但无法被GC回收
- 大对象:一次性加载大量数据到内存(如全表查询)
- 堆设置过小

排查:
- jmap -dump:live,format=b,file=heap.hprof <pid>
- MAT分析Dominator Tree和Leak Suspects
  1. java.lang.OutOfMemoryError: Metaspace
1
2
3
4
5
6
7
8
9
原因:元空间(类元数据)不足
- 动态生成大量类(如CGLIB代理、Groovy脚本、JSP)
- 类加载器泄漏(热部署场景常见)
- Metaspace设置过小

排查:
- jstat -gcutil查看M(Metaspace)使用率
- jcmd <pid> VM.classloader_stats
- -XX:MaxMetaspaceSize=512m 设置上限
  1. java.lang.OutOfMemoryError: GC overhead limit exceeded
1
2
3
4
原因:GC花费了98%以上的时间,但只回收了不到2%的堆内存
- 本质上还是堆内存不足,但JVM提前报错避免无意义的GC

排查:同heap space
  1. java.lang.OutOfMemoryError: Direct buffer memory
1
2
3
4
5
6
7
8
原因:直接内存(堆外内存)不足
- NIO的ByteBuffer.allocateDirect()
- Netty的PooledByteBufAllocator
- 直接内存不受-Xmx限制,受-XX:MaxDirectMemorySize限制

排查:
- jcmd <pid> VM.native_memory summary
- Netty的内存泄漏检测:-Dio.netty.leakDetection.level=PARANOID
  1. java.lang.OutOfMemoryError: unable to create new native thread
1
2
3
4
5
6
7
8
9
原因:无法创建新的操作系统线程
- 线程数达到操作系统限制
- 每个线程默认占用1MB栈空间(-Xss)
- ulimit -u限制了用户最大进程数

排查:
- 查看线程数:jstack <pid> | grep "java.lang.Thread.State" | wc -l
- 查看系统限制:ulimit -u, cat /proc/sys/kernel/threads-max
- 检查是否有线程泄漏(线程池未正确关闭)
  1. java.lang.OutOfMemoryError: Requested array size exceeds VM limit
1
2
原因:尝试创建超大数组(接近Integer.MAX_VALUE)
- 通常是代码Bug,如错误的数组大小计算

12. 🔴 如何排查Java应用的内存泄漏?请描述完整的排查流程。

答:内存泄漏排查是一个系统化的过程,需要结合多种工具。

排查流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Step 1:确认是否存在内存泄漏
- 监控堆内存使用趋势(Grafana/Prometheus)
- 如果Full GC后Old区使用量持续上升 → 确认泄漏

jstat -gcutil <pid> 5000
# 观察每次Full GC后O(Old区)的使用率
# 如果每次GC后O都比上次高 → 泄漏

Step 2:获取堆快照
# 方法1:jmap(会触发Full GC,线上慎用)
jmap -dump:live,format=b,file=heap1.hprof <pid>
# 等待一段时间后再dump一次
jmap -dump:live,format=b,file=heap2.hprof <pid>

# 方法2:OOM时自动dump(推荐,提前配置)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heap.hprof

Step 3:MAT分析
1. 打开heap.hprof
2. 查看Leak Suspects Report(自动分析)
3. 查看Dominator Tree(按Retained Heap排序)
- Retained Heap:对象被GC后能释放的总内存
- 找到Retained Heap最大的对象
4. 右键 → Path to GC Roots → exclude weak/soft references
- 找到泄漏对象的引用链
- 确定是谁持有了不该持有的引用

Step 4:对比两次dump
- MAT支持对比两次dump的Histogram
- 找到数量增长最多的类
- 这些类很可能就是泄漏的对象

常见内存泄漏模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 静态集合
private static final Map<String, Object> cache = new HashMap<>();
public void process(String key, Object value) {
cache.put(key, value); // 只增不减
}
// 修复:使用WeakHashMap或设置淘汰策略

// 2. 未关闭的资源
public void query() {
Connection conn = dataSource.getConnection();
// 如果中间抛异常,conn不会被关闭
ResultSet rs = conn.prepareStatement(sql).executeQuery();
// ...
}
// 修复:try-with-resources

// 3. 监听器未注销
eventBus.register(this); // 注册了但从未unregister
// 修复:在对象销毁时注销监听器

// 4. ThreadLocal未清理
private static ThreadLocal<List<Object>> threadLocal = new ThreadLocal<>();
public void process() {
threadLocal.set(new ArrayList<>());
// 线程池中的线程不会销毁,ThreadLocal的值一直存在
}
// 修复:finally中调用threadLocal.remove()

13. 🔴 堆外内存泄漏如何排查?和堆内存泄漏的排查有什么不同?

答:堆外内存泄漏更难排查,因为常规的jmap/MAT无法看到堆外内存。

堆外内存的来源:

1
2
3
4
5
6
1. DirectByteBuffer:NIO直接内存
2. JNI:本地方法分配的内存
3. 线程栈:每个线程的栈空间(-Xss)
4. Metaspace:类元数据
5. Code Cache:JIT编译后的代码
6. 第三方native库:如Netty的PooledByteBufAllocator

排查方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 1. Native Memory Tracking(NMT)
# 启动时添加参数:-XX:NativeMemoryTracking=detail
# 查看内存分布:
jcmd <pid> VM.native_memory summary
# 输出示例:
# Total: reserved=4GB, committed=2.5GB
# - Java Heap: reserved=2GB, committed=1.8GB
# - Thread: reserved=500MB, committed=500MB (500 threads × 1MB)
# - Internal: reserved=100MB, committed=80MB
# - Direct: reserved=256MB, committed=200MB ← 直接内存

# 对比两个时间点的差异:
jcmd <pid> VM.native_memory baseline # 设置基线
# 等待一段时间
jcmd <pid> VM.native_memory summary.diff # 查看差异

# 2. pmap查看进程内存映射
pmap -x <pid> | sort -k3 -n -r | head -20
# 找到占用最大的内存区域

# 3. Netty内存泄漏检测
# 启动参数:-Dio.netty.leakDetection.level=PARANOID
# Netty会在GC时检测ByteBuf是否被正确释放
# 泄漏时输出详细的分配堆栈

# 4. jemalloc/tcmalloc内存分析
# 使用jemalloc替代默认的malloc,开启profiling
LD_PRELOAD=/usr/lib/libjemalloc.so MALLOC_CONF=prof:true java -jar app.jar
# 生成内存分配profile,分析native内存分配热点

14. 🔵 什么是Java的内存模型(JMM)相关的线上问题?如何排查可见性和有序性Bug?

答:JMM相关的Bug是最难排查的线上问题之一,因为它们通常是间歇性的、不可复现的。

常见JMM问题:

  1. 可见性问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 经典案例:标志位不可见
private boolean running = true; // 缺少volatile

public void stop() {
running = false; // 线程A修改
}

public void run() {
while (running) { // 线程B可能永远看不到修改
// JIT可能将running缓存到寄存器
doWork();
}
}
// 修复:volatile boolean running = true;
  1. 指令重排序问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 经典案例:双重检查锁定(DCL)
private static Singleton instance; // 缺少volatile

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
// 这行代码实际上是3步:
// 1. 分配内存
// 2. 初始化对象
// 3. 将引用指向内存
// 2和3可能重排序,其他线程可能看到未初始化的对象
}
}
}
return instance;
}
// 修复:volatile static Singleton instance;

排查方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 代码审查:
- 检查多线程共享变量是否有volatile或synchronized保护
- 检查是否有DCL模式缺少volatile
- 检查long/double类型的非原子读写

2. 工具检测:
- FindBugs/SpotBugs:静态分析,检测并发Bug
- ThreadSanitizer(C/C++):运行时检测数据竞争
- jcstress:Java并发压力测试框架(OpenJDK出品)

3. 压测复现:
- 增加并发线程数
- 在关键位置插入Thread.yield()增加调度随机性
- 使用jcstress编写并发测试用例

15. 🔴 Linux的OOM Killer是什么?它如何选择要杀死的进程?如何保护关键进程不被OOM Killer杀死?

答:OOM Killer是Linux内核的内存保护机制,当系统物理内存耗尽时,内核会选择一个进程杀死以释放内存。

OOM Killer的选择算法:

1
2
3
4
5
6
7
8
内核为每个进程计算一个oom_score(/proc/<pid>/oom_score):
- 基础分数:进程使用的内存越多,分数越高
- 调整因子:oom_score_adj(-1000 ~ 1000)
- -1000:永远不会被OOM Killer杀死
- 0:默认值
- 1000:优先被杀死

OOM Killer选择oom_score最高的进程杀死

保护关键进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 方法1:调整oom_score_adj
echo -1000 > /proc/<pid>/oom_score_adj
# 或在systemd service中配置
[Service]
OOMScoreAdjust=-1000

# 方法2:关闭overcommit
echo 2 > /proc/sys/vm/overcommit_memory
# 0:默认,启发式overcommit
# 1:总是允许overcommit
# 2:禁止overcommit,分配内存时严格检查

# 方法3:设置cgroup内存限制
# 容器场景下,OOM Killer在cgroup级别工作
# Pod被OOM Kill时,kubectl describe pod会显示:
# Last State: Terminated
# Reason: OOMKilled

排查OOM Kill:

1
2
3
4
5
6
7
8
# 查看系统日志
dmesg | grep -i "oom\|killed"
# 输出示例:
# Out of memory: Kill process 12345 (java) score 800 or sacrifice child
# Killed process 12345 (java) total-vm:8388608kB, anon-rss:4194304kB

# 查看历史OOM事件
journalctl -k | grep -i oom

16. 🔴 如何监控和排查Java应用的堆外内存(Off-Heap)使用情况?Netty的内存池是如何管理的?

答:堆外内存是Java应用内存管理中容易被忽视的部分。

Netty内存池架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
PooledByteBufAllocator
├── PoolArena(线程绑定,减少竞争)
│ ├── PoolChunk(16MB,向OS申请的内存块)
│ │ ├── PoolSubpage(用于小内存分配,<8KB)
│ │ └── 伙伴算法(用于大内存分配)
│ └── PoolChunk...
└── PoolArena...

内存分配策略:
- Tiny(<512B):从PoolSubpage分配
- Small(512B~8KB):从PoolSubpage分配
- Normal(8KB~16MB):从PoolChunk用伙伴算法分配
- Huge(>16MB):直接分配,不池化

监控方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. Netty内置指标
PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
PooledByteBufAllocatorMetric metric = allocator.metric();

// 已使用的直接内存
long usedDirectMemory = metric.usedDirectMemory();
// 已使用的堆内存
long usedHeapMemory = metric.usedHeapMemory();
// 每个Arena的详细信息
for (PoolArenaMetric arena : metric.directArenas()) {
System.out.println("Active allocations: " + arena.numActiveAllocations());
System.out.println("Active bytes: " + arena.numActiveBytes());
}

// 2. 暴露为Prometheus指标
Gauge.builder("netty.direct.memory.used", allocator,
a -> a.metric().usedDirectMemory())
.register(meterRegistry);

常见堆外内存泄漏场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. ByteBuf未释放
ByteBuf buf = allocator.buffer(1024);
// 使用后忘记release
// 修复:buf.release() 或使用ReferenceCountUtil.release(buf)

// 2. Pipeline中的Handler未正确传递ByteBuf
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
// 处理后既没有传递给下一个Handler,也没有release
// 修复:ctx.fireChannelRead(msg) 或 buf.release()
}

// 3. 开启泄漏检测
// -Dio.netty.leakDetection.level=PARANOID
// 级别:DISABLED < SIMPLE < ADVANCED < PARANOID

17. 🔵 什么是内存碎片?在Java和Linux层面分别如何处理?

答:内存碎片是指可用内存被分割成大量不连续的小块,导致无法分配大块连续内存。

Java层面:

1
2
3
4
5
6
7
8
9
10
11
JVM堆内存碎片:
- CMS收集器:标记-清除算法,不压缩,容易产生碎片
- 表现:堆空间充足但无法分配大对象 → Full GC
- 解决:-XX:+UseCMSCompactAtFullCollection(Full GC时压缩)
- 或升级到G1/ZGC(基于Region,碎片问题小)

- G1收集器:基于Region,通过Mixed GC回收碎片化严重的Region
- 碎片问题比CMS小很多
- 但Region内部仍可能有碎片

- ZGC/Shenandoah:并发压缩,几乎无碎片问题

Linux层面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看内存碎片情况
cat /proc/buddyinfo
# Node 0, zone Normal 1024 512 256 128 64 32 16 8 4 2 1
# 数字表示各大小的空闲块数量
# 如果大块(右侧)很少,说明碎片严重

# 手动触发内存压缩
echo 1 > /proc/sys/vm/compact_memory

# 查看透明大页(THP)状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# THP可以减少碎片影响,但可能导致延迟抖动
# 对于延迟敏感的应用(如Redis),建议关闭THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled

18. 🔴 如何设计一个Java应用的内存监控告警方案?需要监控哪些指标?

答:完善的内存监控是预防OOM的关键。

监控指标体系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
一、JVM堆内存:
- 堆总使用率(used/max):>80%告警
- Old区使用率:>85%告警
- Full GC频率:>1次/分钟告警
- Full GC后Old区回收率:<10%告警(可能泄漏)
- Young GC耗时P99:>100ms告警

二、JVM非堆内存:
- Metaspace使用率:>90%告警
- Direct Memory使用量:接近MaxDirectMemorySize告警
- 线程数:>1000告警
- 线程栈总内存:线程数 × Xss

三、操作系统内存:
- 物理内存使用率:>90%告警
- Swap使用量:>0告警(Java应用不应该使用Swap)
- OOM Kill事件:任何OOM Kill立即告警

四、容器内存(K8s):
- 容器内存使用率(相对于limit):>85%告警
- 容器OOM Kill次数
- 内存Request vs 实际使用(用于优化资源配置)

Prometheus + Grafana实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# JVM指标(通过Micrometer暴露)
- jvm_memory_used_bytes{area="heap"}
- jvm_memory_used_bytes{area="nonheap"}
- jvm_gc_pause_seconds_count{cause="Allocation Failure"}
- jvm_gc_pause_seconds_sum
- jvm_threads_live_threads
- jvm_buffer_memory_used_bytes{id="direct"}

# 告警规则示例
groups:
- name: jvm-memory
rules:
- alert: HeapMemoryHigh
expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85
for: 5m
labels:
severity: warning
- alert: FrequentFullGC
expr: rate(jvm_gc_pause_seconds_count{action="end of major GC"}[5m]) > 0.2
for: 5m
labels:
severity: critical

19. 🔴 Java应用使用了大量内存但jmap显示堆使用率不高,可能是什么原因?

答:这是一个经典的”内存去哪了”问题,需要从堆外内存角度排查。

可能的原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1. 直接内存(Direct Memory):
- NIO的ByteBuffer.allocateDirect()
- Netty的堆外内存池
- 检查:jcmd <pid> VM.native_memory summary

2. 线程栈:
- 每个线程默认1MB栈空间
- 1000个线程 = 1GB
- 检查:jstack <pid> | grep "java.lang.Thread.State" | wc -l

3. Metaspace:
- 大量动态生成的类
- 检查:jstat -gcutil <pid>,看M列

4. Code Cache:
- JIT编译后的代码缓存
- 默认240MB(JDK 8+)
- 检查:jcmd <pid> Compiler.codecache

5. JNI/Native库:
- 第三方native库分配的内存
- 如RocksDB、LevelDB、OpenSSL
- 检查:pmap -x <pid>

6. 内存映射文件(mmap):
- MappedByteBuffer
- 如Kafka的日志文件、RocketMQ的CommitLog
- 检查:pmap -x <pid> | grep map

7. GC自身的开销:
- G1的Remember Set、Card Table
- ZGC的染色指针需要额外内存

完整排查命令:

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 查看进程总内存
ps -p <pid> -o rss,vsz
# RSS:实际物理内存使用
# VSZ:虚拟内存使用

# 2. NMT详细分析
jcmd <pid> VM.native_memory detail
# 列出每个类别的内存使用

# 3. 对比堆内存和总内存
# 总内存 - 堆内存 = 堆外内存
# 如果堆外内存占比很大,逐一排查上述原因

20. ⚫ 如何设计一个能自动检测和处理内存泄漏的系统?

答:自动化内存泄漏检测是大规模微服务架构的刚需。

系统设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
架构:
Agent(每个JVM实例)→ 数据收集 → 分析引擎 → 告警/自愈

1. Agent层:
- 定期采集JVM内存指标(每10秒)
- Full GC后自动记录Old区使用量
- 内存超过阈值时自动dump堆快照(限制频率)

2. 分析引擎:
- 趋势分析:连续N次Full GC后Old区使用量递增 → 疑似泄漏
- 对比分析:同一服务的不同实例,内存差异过大 → 疑似泄漏
- 版本对比:新版本上线后内存增长速率变化 → 可能引入泄漏

3. 自动处理:
Level 1:告警通知(钉钉/Slack)
Level 2:自动dump堆快照并上传到分析平台
Level 3:自动重启泄漏实例(K8s Pod重建)
Level 4:自动回滚到上一个版本(如果是新版本引入的)

4. 分析平台:
- 自动分析堆快照,生成泄漏报告
- 对比不同时间点的快照,找出增长最快的对象
- 展示泄漏对象的引用链

三、慢SQL与数据库问题排查(21-30题)

21. 🔵 线上出现慢SQL告警,请描述你的完整排查和优化流程。

答:慢SQL是最常见的性能问题之一,需要系统化的排查流程。

排查流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- Step 1:找到慢SQL
-- 方法1:慢查询日志
-- my.cnf配置:
-- slow_query_log = 1
-- long_query_time = 1 -- 超过1秒记录
-- slow_query_log_file = /var/log/mysql/slow.log

-- 方法2:实时查看
SHOW PROCESSLIST;
-- 找到Time很大的查询

-- 方法3:performance_schema
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

-- Step 2:分析执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 1;
-- 重点关注:
-- type:ALL(全表扫描) → 需要优化
-- key:NULL → 没有使用索引
-- rows:扫描行数过多
-- Extra:Using filesort, Using temporary → 需要优化

-- Step 3:查看索引使用情况
SHOW INDEX FROM orders;
-- 检查是否有合适的索引

-- Step 4:优化
-- 添加索引、改写SQL、调整表结构等

常见慢SQL原因及优化:

  1. 缺少索引或索引失效:
1
2
3
4
5
6
7
8
9
10
11
-- 索引失效的常见情况:
WHERE YEAR(create_time) = 2024 -- 函数导致索引失效
-- 优化:WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'

WHERE name LIKE '%张%' -- 左模糊导致索引失效
-- 优化:使用全文索引或ES

WHERE status != 1 -- 不等于导致索引失效(取决于数据分布)

WHERE user_id = 123 OR status = 1 -- OR可能导致索引失效
-- 优化:UNION ALL拆分
  1. 大表JOIN:
1
2
3
4
-- 两个大表JOIN,没有合适的索引
SELECT * FROM orders o JOIN order_items oi ON o.id = oi.order_id
WHERE o.create_time > '2024-01-01';
-- 优化:确保JOIN字段有索引,先过滤再JOIN
  1. 深分页:
1
2
3
4
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- 需要扫描100万+20行
-- 优化:游标分页
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;

22. 🔴 MySQL的锁等待和死锁如何排查?如何预防?

答:锁问题是数据库性能的隐形杀手。

排查锁等待:

1
2
3
4
5
6
7
8
9
10
11
-- 查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 或MySQL 8.0+
SELECT * FROM performance_schema.data_lock_waits;

-- 查看当前持有锁的事务
SELECT * FROM information_schema.INNODB_TRX;
-- 重点关注:trx_state='LOCK WAIT'的事务

-- 查看锁详情
SELECT * FROM performance_schema.data_locks;

排查死锁:

1
2
3
4
5
6
7
8
9
-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS\G
-- 在LATEST DETECTED DEADLOCK部分:
-- 显示两个事务各自持有和等待的锁
-- 以及InnoDB选择回滚的事务

-- 开启死锁日志
SET GLOBAL innodb_print_all_deadlocks = ON;
-- 所有死锁信息会记录到error log

死锁预防:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 按固定顺序访问表和行
-- 反模式:事务1先更新A再更新B,事务2先更新B再更新A
-- 正确:所有事务都按ID排序后更新

2. 缩短事务时间
-- 事务中不要包含RPC调用、文件IO等耗时操作
-- 尽快提交或回滚

3. 使用合理的隔离级别
-- RC(Read Committed)比RR(Repeatable Read)产生的锁更少
-- RR下的间隙锁是死锁的常见原因

4. 添加合理的索引
-- 没有索引时,UPDATE/DELETE会锁全表
-- 有索引时,只锁匹配的行

23. 🔴 如何排查MySQL的连接数暴涨问题?

答:连接数暴涨通常意味着应用层出了问题。

排查流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- Step 1:查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';

-- Step 2:查看连接来源分布
SELECT user, host, db, command, time, state, info
FROM information_schema.processlist
ORDER BY time DESC;

-- 按来源IP统计
SELECT SUBSTRING_INDEX(host, ':', 1) AS client_ip, COUNT(*) AS conn_count
FROM information_schema.processlist
GROUP BY client_ip ORDER BY conn_count DESC;

-- Step 3:查看连接状态分布
SELECT command, COUNT(*) FROM information_schema.processlist GROUP BY command;
-- 如果大量Sleep → 连接未正确归还连接池
-- 如果大量Query → 慢SQL导致连接堆积

常见原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. 慢SQL导致连接堆积:
- 一个慢SQL占用连接10秒
- QPS=100 → 同时需要1000个连接
- 解决:优化慢SQL

2. 连接池配置不当:
- 连接池maxSize过大
- 10个服务实例 × 每个50连接 = 500连接
- 解决:合理设置连接池大小
- 推荐公式:connections = (core_count * 2) + effective_spindle_count

3. 连接泄漏:
- 获取连接后未归还(异常路径未关闭)
- HikariCP配置:leakDetectionThreshold=60000(60秒未归还告警)

4. 突发流量:
- 大促、爬虫等导致请求暴增
- 解决:限流 + 连接池排队

24. 🔴 数据库主从延迟如何排查和处理?

答:主从延迟是读写分离架构中最常见的问题。

排查方法:

1
2
3
4
5
6
7
8
9
10
11
-- 在从库执行
SHOW SLAVE STATUS\G
-- 关键字段:
-- Seconds_Behind_Master:延迟秒数(不完全准确)
-- Relay_Log_Space:中继日志大小
-- Slave_SQL_Running_State:SQL线程状态

-- 更准确的延迟监控:pt-heartbeat
-- 主库定期写入心跳时间戳,从库读取并计算差值
pt-heartbeat --database=test --update --create-table --daemonize
pt-heartbeat --database=test --monitor

常见延迟原因及处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1. 从库单线程回放(MySQL 5.6之前):
- 主库并行写入,从库单线程回放
- 解决:升级到MySQL 5.7+,开启并行复制
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = 8

2. 大事务:
- 主库一个大事务执行5分钟
- 从库也需要5分钟回放
- 解决:拆分大事务

3. 从库负载过高:
- 从库承担大量读请求,CPU/IO打满
- 回放线程得不到资源
- 解决:增加从库数量,分散读压力

4. 网络延迟:
- 主从之间网络带宽不足或延迟高
- 解决:检查网络,考虑半同步复制

业务层处理延迟:
- 写后读走主库(通过Hint或中间件路由)
- 关键业务强制走主库
- 非关键业务容忍延迟(如评论列表)

25. 🔵 如何排查MySQL的磁盘IO问题?

答:磁盘IO是数据库性能的关键瓶颈。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Step 1:确认IO瓶颈
iostat -x 1
# 关键指标:
# %util:磁盘利用率,>80%说明IO繁忙
# await:IO平均等待时间,>10ms需要关注
# r/s, w/s:每秒读写次数
# rkB/s, wkB/s:每秒读写数据量

# Step 2:找到IO高的进程
iotop -o
# 显示正在进行IO的进程

# Step 3:MySQL层面分析
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 查看InnoDB IO状态
SHOW ENGINE INNODB STATUS\G
-- FILE I/O部分:pending reads/writes

-- 查看Buffer Pool命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- 命中率 = 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)
-- 命中率<99%说明Buffer Pool太小

-- 查看IO密集的SQL
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_ROWS_EXAMINED DESC LIMIT 10;
-- 扫描行数多的SQL通常IO也多

优化方向:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 增大Buffer Pool:
innodb_buffer_pool_size = 物理内存的60-80%

2. 优化SQL减少IO:
- 添加索引减少全表扫描
- 避免SELECT *
- 使用覆盖索引

3. 使用SSD:
- 随机IO性能提升100倍+

4. 调整刷盘策略:
innodb_flush_log_at_trx_commit = 2 -- 每秒刷盘(牺牲少量持久性换性能)
sync_binlog = 100 -- 每100个事务刷一次binlog

26. 🔴 线上数据库突然变慢,但SQL和数据量都没变化,可能是什么原因?

答:这种”什么都没变但突然变慢”的场景,通常是环境因素导致。

排查清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1. 统计信息过期:
- MySQL优化器依赖统计信息选择执行计划
- 统计信息过期可能导致优化器选错索引
- 检查:SHOW INDEX FROM table_name; -- Cardinality是否准确
- 修复:ANALYZE TABLE table_name;

2. Buffer Pool被冲刷:
- 一个大查询(如全表扫描)把热数据从Buffer Pool中挤出
- 后续查询都需要从磁盘读取
- 检查:Buffer Pool命中率突然下降
- 预防:innodb_old_blocks_time = 1000(防止全表扫描污染Buffer Pool)

3. 锁竞争加剧:
- 某个长事务持有锁,导致其他事务等待
- 检查:SHOW ENGINE INNODB STATUS中的TRANSACTIONS部分
- 修复:找到并终止长事务

4. 磁盘IO抖动:
- 同一物理机上的其他服务抢占IO
- 磁盘坏道导致IO延迟增加
- 检查:iostat -x 1

5. 网络问题:
- 应用到数据库的网络延迟增加
- 检查:ping/traceroute

6. 操作系统层面:
- Swap被使用(内存不足)
- 透明大页(THP)导致延迟抖动
- 检查:free -h, cat /sys/kernel/mm/transparent_hugepage/enabled

7. MySQL内部维护操作:
- InnoDB的Purge线程清理undo log
- Change Buffer合并
- 自适应哈希索引重建

27. 🔴 如何设计一个完善的SQL审核和慢SQL治理体系?

答:SQL治理需要从开发到运行的全生命周期管理。

全生命周期治理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
一、开发阶段 - SQL审核:
工具:Yearning、Archery、SQLAdvisor

审核规则:
- 禁止SELECT *
- 禁止不带WHERE的UPDATE/DELETE
- 必须有合适的索引
- JOIN不超过3张表
- 子查询改写为JOIN
- 禁止在索引列上使用函数
- 分页必须有合理的LIMIT

流程:
开发提交SQL → 自动审核 → DBA人工审核 → 执行

二、测试阶段 - 性能验证:
- 使用生产数据量级的测试环境
- 对关键SQL做EXPLAIN分析
- 压测验证SQL在高并发下的表现

三、运行阶段 - 慢SQL监控:
- 慢查询日志 + 定期分析(pt-query-digest)
- APM系统实时监控SQL耗时(SkyWalking/Pinpoint)
- 告警规则:
- 单条SQL > 1秒
- 同一SQL 5分钟内出现10次以上慢查询
- 全表扫描次数突增

四、优化阶段 - 持续治理:
- 每周慢SQL Top10报告
- 按影响面排序(慢SQL × 调用频率 = 总影响时间)
- 优先优化影响面最大的SQL
- 建立SQL优化知识库,避免重复问题

28. 🔵 什么是MySQL的查询缓存(Query Cache)?为什么MySQL 8.0移除了它?

答:Query Cache是MySQL缓存SELECT结果的机制,但在高并发场景下反而成为性能瓶颈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
工作原理:
- 对SELECT语句做哈希,缓存结果集
- 相同的SQL直接返回缓存结果
- 表数据有任何修改 → 该表所有缓存失效

为什么被移除:
1. 全局互斥锁:
- 查询缓存使用一把全局锁
- 高并发下锁竞争严重
- 即使缓存命中,获取锁的开销也很大

2. 失效粒度太粗:
- 表级失效,任何写操作都清空该表所有缓存
- 写多读少的场景,缓存命中率极低
- 频繁的缓存失效和重建反而浪费CPU

3. 内存管理问题:
- 缓存碎片化
- 大结果集占用大量内存

替代方案:
- 应用层缓存(Redis/Memcached)
- ProxySQL的查询缓存(更智能的缓存策略)
- InnoDB Buffer Pool(数据页级别的缓存,更高效)

29. 🔴 分库分表后的SQL排查有什么特殊之处?

答:分库分表增加了SQL排查的复杂度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
特殊挑战:

1. SQL路由问题:
- SQL被路由到了错误的分片
- 或者没有带分片键,导致全分片扫描

排查:
- 检查中间件(ShardingSphere/MyCat)的路由日志
- 确认SQL是否包含分片键
- EXPLAIN在中间件层面查看路由结果

2. 跨分片查询:
- ORDER BY + LIMIT在多个分片上执行后需要归并排序
- 深分页问题被放大(每个分片都要扫描大量数据)

排查:
- 检查中间件的归并日志
- 查看每个分片的执行时间

3. 分片数据倾斜:
- 某个分片数据量远大于其他分片
- 该分片的查询性能差

排查:
- 统计各分片的数据量和查询耗时
- 检查分片键的分布是否均匀

4. 全局表的一致性:
- 广播表(如配置表)在所有分片都有副本
- 更新时需要保证所有分片一致

排查:
- 对比各分片的广播表数据
- 检查是否有更新失败的分片

30. ⚫ 如何设计一个数据库性能自动诊断系统?

答:自动诊断系统是DBA效率提升的关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
系统架构:

数据采集层:
├── 慢查询日志采集(Filebeat → Kafka)
├── Performance Schema指标采集(Prometheus)
├── 系统指标采集(node_exporter)
└── 中间件指标采集(ShardingSphere metrics)

分析引擎:
├── 实时分析:
│ ├── 慢SQL自动EXPLAIN
│ ├── 索引推荐(基于查询模式分析)
│ ├── 锁等待检测
│ └── 连接数异常检测

├── 离线分析:
│ ├── SQL指纹聚合(相似SQL归类)
│ ├── 索引使用率分析(未使用的索引建议删除)
│ ├── 表空间增长趋势预测
│ └── 分片均衡度分析

└── 智能诊断:
├── 根因分析:CPU高→锁等待→慢SQL→缺少索引
├── 优化建议:自动生成索引建议、SQL改写建议
└── 容量预警:基于增长趋势预测磁盘/连接数不足

展示层:
├── 实时大盘(Grafana)
├── 诊断报告(每日/每周)
├── 告警通知(钉钉/Slack/PagerDuty)
└── 一键优化(自动执行低风险优化,如ANALYZE TABLE)

四、全链路追踪与可观测性(31-40题)

31. 🔵 请解释可观测性的三大支柱:Metrics、Logging、Tracing,它们之间的关系是什么?

答:可观测性三大支柱各有侧重,互相补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Metrics(指标):
- 回答"发生了什么"
- 数值型时间序列数据
- 适合告警和趋势分析
- 示例:QPS、延迟P99、错误率、CPU使用率
- 工具:Prometheus、InfluxDB、Datadog

Logging(日志):
- 回答"为什么发生"
- 离散的事件记录
- 适合排查具体问题
- 示例:错误堆栈、请求参数、业务日志
- 工具:ELK(Elasticsearch+Logstash+Kibana)、Loki

Tracing(追踪):
- 回答"在哪里发生"
- 请求在分布式系统中的完整调用链
- 适合定位跨服务的性能瓶颈
- 示例:一个请求经过API Gateway→OrderService→StockService→DB的完整链路
- 工具:Jaeger、Zipkin、SkyWalking

三者的关系:
Metrics发现问题 → Tracing定位到具体服务和接口 → Logging查看详细原因

示例:
1. Metrics告警:订单服务P99延迟从50ms飙到2s
2. Tracing分析:发现是库存服务的deductStock接口慢(1.8s)
3. Logging查看:库存服务日志显示SQL执行超时,连接池等待
4. 进一步排查:数据库慢查询日志找到具体的慢SQL

32. 🔴 如何设计一个分布式追踪系统?核心数据模型是什么?

答:分布式追踪的核心是在请求的整个生命周期中传递上下文。

核心数据模型(OpenTelemetry标准):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Trace(追踪):
- 一个完整请求的调用链
- 由一个全局唯一的TraceID标识

Span(跨度):
- 调用链中的一个操作单元
- 包含:SpanID、ParentSpanID、操作名、开始时间、结束时间、标签、日志
- Span之间通过ParentSpanID形成树形结构

示例:
TraceID: abc123
├── Span A: API Gateway (SpanID: 1, Parent: null, 0-100ms)
│ ├── Span B: OrderService.createOrder (SpanID: 2, Parent: 1, 5-95ms)
│ │ ├── Span C: StockService.deduct (SpanID: 3, Parent: 2, 10-60ms)
│ │ │ └── Span D: MySQL.query (SpanID: 4, Parent: 3, 15-55ms)
│ │ └── Span E: PayService.pay (SpanID: 5, Parent: 2, 65-90ms)

上下文传播:

1
2
3
4
5
6
7
8
9
10
HTTP传播(W3C Trace Context标准):
traceparent: 00-abc123-span1-01
tracestate: vendor=value

gRPC传播:
通过Metadata传递TraceID和SpanID

MQ传播:
将TraceID写入消息Header
消费者从Header中提取TraceID,创建新Span并关联

架构设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
应用层(SDK/Agent):
- 自动埋点:拦截HTTP/RPC/DB调用,自动创建Span
- 手动埋点:业务关键节点手动创建Span
- 采样策略:头部采样(决定是否追踪整个请求)

数据传输层:
- Agent → Collector(批量发送,减少网络开销)
- 协议:OTLP(OpenTelemetry Protocol)
- 缓冲:本地队列,防止Collector不可用时丢数据

存储层:
- 热数据:Elasticsearch/ClickHouse(最近7天,支持快速查询)
- 冷数据:对象存储(历史数据,低成本)
- 索引:TraceID、ServiceName、OperationName、Duration、Tags

查询层:
- 按TraceID查询完整调用链
- 按服务/接口查询延迟分布
- 拓扑图:自动生成服务依赖关系

33. 🔴 分布式追踪的采样策略有哪些?如何在数据完整性和性能开销之间取得平衡?

答:全量采集在高QPS场景下不现实,采样是必须的。

采样策略:

  1. 头部采样(Head-based Sampling):
1
2
3
4
5
在请求入口决定是否采样
- 固定比例:每100个请求采样1个(1%)
- 速率限制:每秒最多采样100个请求
- 优点:简单,开销小
- 缺点:可能错过异常请求(异常请求也可能不被采样)
  1. 尾部采样(Tail-based Sampling):
1
2
3
4
5
6
7
8
9
10
11
12
在请求完成后,根据结果决定是否保留
- 错误请求:100%保留
- 慢请求(>1s):100%保留
- 正常请求:1%采样

实现:
- Collector收集完整Trace后做决策
- 需要在Collector中缓存一段时间的Span数据
- OpenTelemetry Collector支持tail_sampling processor

优点:不会错过异常请求
缺点:Collector需要更多内存和CPU
  1. 自适应采样:
1
2
3
4
5
6
7
8
根据系统负载动态调整采样率
- 系统空闲时:采样率高(如10%)
- 系统繁忙时:采样率低(如0.1%)
- 保证在任何负载下都有足够的采样数据

实现:
- 基于当前QPS和Collector处理能力动态计算
- Jaeger的Adaptive Sampling
  1. 关键路径采样:
1
2
3
4
对不同的接口/服务使用不同的采样率
- 核心交易链路:10%采样
- 内部管理接口:0.1%采样
- 健康检查接口:不采样

34. 🔴 SkyWalking和Jaeger的架构有什么区别?你会如何选择?

答:两者都是主流的分布式追踪系统,但设计理念不同。

维度 SkyWalking Jaeger
语言 Java Go
埋点方式 Java Agent自动埋点(无侵入) SDK手动埋点 + 自动埋点
协议 自定义协议 + OTLP OpenTracing/OTLP
存储 ES/H2/MySQL/BanyanDB ES/Cassandra/Kafka
指标 内置Metrics分析(服务/实例/端点) 主要是Tracing,Metrics较弱
拓扑图 自动生成服务拓扑 需要额外配置
告警 内置告警引擎 需要外部告警系统
性能分析 支持代码级性能分析(Profile) 不支持
社区 Apache顶级项目,国内社区活跃 CNCF毕业项目,国际社区活跃

选择建议:

1
2
3
4
5
6
7
8
9
10
11
选SkyWalking:
- Java技术栈为主
- 需要开箱即用的全套可观测性方案
- 需要无侵入的自动埋点
- 国内团队,中文文档和社区支持好

选Jaeger:
- 多语言技术栈
- 已有Prometheus+Grafana的监控体系
- 需要与OpenTelemetry深度集成
- 偏好轻量级、可组合的方案

35. 🔴 如何实现跨线程、跨异步调用的Trace上下文传播?

答:异步场景下的上下文传播是分布式追踪的难点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 问题:异步调用时,TraceContext丢失
CompletableFuture.supplyAsync(() -> {
// 这里是新线程,没有TraceContext
return orderService.query(orderId);
});

// 解决方案1:手动传播
Span currentSpan = tracer.activeSpan();
CompletableFuture.supplyAsync(() -> {
try (Scope scope = tracer.activateSpan(currentSpan)) {
return orderService.query(orderId);
}
});

// 解决方案2:包装线程池(推荐)
// SkyWalking的方案:
ExecutorService tracedExecutor = new TracedExecutorService(
Executors.newFixedThreadPool(10), tracer);

// OpenTelemetry的方案:
ExecutorService wrappedExecutor = Context.taskWrapping(
Executors.newFixedThreadPool(10));

// 解决方案3:Java Agent自动增强
// SkyWalking Agent自动增强以下类:
// - Runnable/Callable
// - CompletableFuture
// - ForkJoinPool
// - @Async注解的方法
// 无需修改业务代码

// 解决方案4:MQ场景
// 生产者:将TraceContext写入消息Header
producer.send(new ProducerRecord<>(topic, key, value,
Collections.singletonList(
new RecordHeader("trace-id", traceId.getBytes()))));

// 消费者:从Header中恢复TraceContext
String traceId = new String(record.headers().lastHeader("trace-id").value());
Span span = tracer.buildSpan("consume").asChildOf(extractContext(traceId)).start();

36. 🔵 什么是OpenTelemetry?它和OpenTracing、OpenCensus的关系是什么?

答:OpenTelemetry(OTel)是可观测性领域的统一标准。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
历史:
- OpenTracing:分布式追踪的API标准(CNCF项目)
- OpenCensus:Google开源的Metrics+Tracing库
- 两者功能重叠,社区分裂
- 2019年合并为OpenTelemetry

OpenTelemetry的定位:
- 统一的可观测性数据采集标准
- 覆盖Traces、Metrics、Logs三大支柱
- 提供SDK、API、Collector、协议(OTLP)
- 不提供存储和展示(交给Jaeger、Prometheus、Grafana等)

架构:
Application(SDK/Auto-instrumentation)
→ OTLP协议
→ OpenTelemetry Collector
→ Exporter → Jaeger/Prometheus/Elasticsearch/...

优势:
- 厂商中立,避免锁定
- 一套SDK采集所有信号(Traces+Metrics+Logs)
- 丰富的自动埋点库(HTTP、gRPC、JDBC、Redis等)
- Collector支持数据处理(过滤、采样、转换、路由)

37. 🔴 如何利用可观测性数据实现自动根因分析(Root Cause Analysis)?

答:自动根因分析是可观测性的高级应用,能大幅缩短故障定位时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
方法论:

1. 异常检测(发现问题):
- 基于统计的异常检测:3-sigma、MAD
- 基于机器学习:时间序列异常检测(如Prophet、LSTM)
- 多维度联合检测:同时监控延迟、错误率、流量

2. 关联分析(缩小范围):
- 时间关联:在异常时间窗口内,哪些指标同时异常
- 拓扑关联:异常服务的上下游是否也异常
- 变更关联:异常时间点附近是否有发布、配置变更

3. 根因定位(精确定位):
- 调用链分析:从异常Trace中找到耗时最长的Span
- 对比分析:异常请求vs正常请求的Trace差异
- 下钻分析:从服务→接口→SQL→索引逐层下钻

实际案例:
告警:订单服务P99延迟从50ms飙到5s
自动分析:
1. 调用链分析 → 库存服务deductStock耗时4.8s
2. 库存服务指标 → 数据库连接池等待时间飙高
3. 数据库指标 → 活跃连接数打满,大量锁等待
4. 慢SQL分析 → 发现一个全表扫描的UPDATE
5. 变更关联 → 30分钟前有一次库存服务发布
6. 根因:新版本引入了一个缺少WHERE条件的UPDATE语句

38. 🔵 如何设计日志系统的架构?ELK和Loki各有什么优缺点?

答:日志系统是可观测性的基础设施。

日志架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
应用层:
- 结构化日志(JSON格式)
- 包含TraceID、SpanID(与追踪系统关联)
- 日志级别合理使用(ERROR/WARN/INFO/DEBUG)

采集层:
- Filebeat/Fluentd/Vector:从文件采集
- 直接推送:应用直接发送到Kafka

传输层:
- Kafka:缓冲和削峰
- 保证日志不丢失

处理层:
- Logstash/Flink:日志解析、过滤、富化
- 提取关键字段、脱敏、添加标签

存储层:
- 热数据:Elasticsearch/ClickHouse(最近7天)
- 温数据:降低副本数(7-30天)
- 冷数据:对象存储(>30天)

查询层:
- Kibana/Grafana:可视化查询
- 支持全文搜索、字段过滤、时间范围

ELK vs Loki:

维度 ELK Loki
索引方式 全文索引(倒排索引) 只索引标签,不索引日志内容
存储成本 高(索引占用大量存储) 低(压缩存储,无全文索引)
查询能力 强(支持复杂的全文搜索) 弱(只能按标签过滤+grep)
资源消耗 高(ES需要大量内存和CPU) 低(轻量级)
运维复杂度 高(ES集群运维复杂) 低(单二进制文件)
生态 成熟,插件丰富 Grafana生态,与Prometheus配合好

选择建议:

1
2
3
4
5
6
7
8
9
10
选ELK:
- 需要复杂的全文搜索
- 日志分析是核心需求
- 有专门的运维团队

选Loki:
- 已有Grafana+Prometheus体系
- 成本敏感
- 日志主要用于排障,不需要复杂分析
- 小团队,运维能力有限

39. 🔴 如何设计一个高效的告警系统?如何避免告警风暴和告警疲劳?

答:告警系统的核心挑战不是发告警,而是发”有用的”告警。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
告警分层:

P0(致命):核心业务不可用
- 支付成功率<95%
- 订单服务全部不可用
- 数据库主库宕机
→ 电话 + 短信 + 钉钉,5分钟内响应

P1(严重):核心业务降级
- 接口P99>2s
- 错误率>5%
- 从库延迟>30s
→ 短信 + 钉钉,15分钟内响应

P2(警告):非核心功能异常
- CPU>80%持续5分钟
- 磁盘使用率>85%
- 非核心服务错误率升高
→ 钉钉,工作时间处理

P3(通知):需要关注但不紧急
- 证书即将过期
- 容量预警
→ 邮件/工单

避免告警风暴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. 告警聚合:
- 同一服务的多个实例同时告警 → 聚合为一条
- 同一根因导致的级联告警 → 只发根因告警
- 时间窗口内的重复告警 → 合并

2. 告警抑制:
- P0告警触发后,抑制该服务的P1/P2告警
- 已知维护窗口期间抑制告警

3. 告警收敛:
- 持续告警不重复发送
- 首次告警 → 5分钟后未恢复再发 → 30分钟后再发
- 恢复后发送恢复通知

4. 动态阈值:
- 不用固定阈值,用基于历史数据的动态基线
- 工作日和周末的基线不同
- 大促期间自动调整阈值

40. ⚫ 请设计一个完整的可观测性平台架构,支持万级微服务实例。

答:万级实例的可观测性平台需要考虑数据量、性能和成本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
数据量估算(万级实例):
- Metrics:10,000实例 × 500指标 × 15秒采集 = 33万数据点/秒
- Traces:假设总QPS=100万,1%采样 = 1万Trace/秒
- Logs:10,000实例 × 100条/秒 = 100万条/秒

架构设计:

┌─────────────────────────────────────────────┐
│ 应用层 │
│ OTel SDK + Agent(自动埋点) │
└──────────────┬──────────────────────────────┘
│ OTLP
┌──────────────▼──────────────────────────────┐
│ OTel Collector集群 │
│ ├── 采样(Tail-based Sampling) │
│ ├── 过滤(去除健康检查等无用数据) │
│ ├── 富化(添加K8s元数据) │
│ └── 路由(按类型分发到不同后端) │
└──────┬──────────┬──────────┬────────────────┘
│ │ │
┌──────▼───┐ ┌───▼────┐ ┌──▼──────────┐
│Prometheus│ │ Tempo │ │ Loki │
│(Metrics) │ │(Traces)│ │ (Logs) │
│ + Thanos │ │ │ │ │
└──────┬───┘ └───┬────┘ └──┬──────────┘
│ │ │
┌──────▼─────────▼─────────▼──────────────────┐
│ Grafana │
│ ├── 统一查询(Metrics+Traces+Logs关联) │
│ ├── 告警引擎 │
│ ├── 服务拓扑图 │
│ └── SLO Dashboard │
└─────────────────────────────────────────────┘

关键设计决策:
1. Thanos:Prometheus的长期存储和全局查询方案
2. Tempo:Grafana的追踪后端,支持对象存储,成本低
3. Loki:只索引标签,存储成本是ES的1/10
4. 统一用Grafana查询,Metrics→Traces→Logs一键跳转

五、网络与中间件故障排查(41-50题)

41. 🔵 线上服务出现大量Connection Timeout和Read Timeout,如何区分和排查?

答:Connection Timeout和Read Timeout是两种完全不同的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Connection Timeout(连接超时):
- TCP三次握手未在超时时间内完成
- 原因:
a. 目标服务不可达(IP/端口错误、防火墙拦截)
b. 目标服务连接队列满(backlog满了)
c. 网络丢包严重
d. 目标服务进程挂了但端口还在监听(如K8s Pod正在终止)

排查:
telnet target_host target_port # 测试连通性
ss -lnt | grep <port> # 查看目标端口监听状态和backlog
netstat -s | grep -i "listen" # 查看SYN队列溢出
tcpdump -i eth0 host target_host and port target_port # 抓包分析

Read Timeout(读超时):
- TCP连接已建立,但在超时时间内未收到响应数据
- 原因:
a. 目标服务处理慢(慢SQL、锁等待、下游调用慢)
b. 目标服务GC停顿
c. 网络延迟突增
d. 响应数据量大,传输时间长

排查:
# 在目标服务上查看请求处理时间
# 检查目标服务的CPU、内存、GC状态
# 抓包查看响应时间
tcpdump -i eth0 -A host target_host and port target_port

42. 🔴 如何排查微服务之间的网络抖动问题?

答:网络抖动表现为延迟间歇性升高,是最难排查的问题之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Step 1:确认是网络问题还是服务问题
# 在调用方和被调用方同时记录时间戳
# 调用方记录:发送时间、收到响应时间
# 被调用方记录:收到请求时间、发送响应时间
# 网络耗时 = (调用方总耗时) - (被调用方处理耗时)

# Step 2:网络层面排查
# 持续ping检测丢包和延迟
ping -i 0.1 target_host # 每100ms ping一次
mtr target_host # 追踪每一跳的延迟和丢包

# 检查网络接口错误
ifconfig eth0 # 查看errors、dropped、overruns
ethtool -S eth0 # 查看详细网络统计

# 检查TCP重传
netstat -s | grep -i retrans
# 重传率高说明网络质量差

# Step 3:常见原因
1. 网络带宽打满:
iftop -i eth0 # 实时查看带宽使用
# 某个大数据传输任务占满带宽

2. TCP队列溢出:
ss -lnt # 查看Recv-Q和Send-Q
# Recv-Q > 0 说明应用处理不过来
# Send-Q > 0 说明网络发送缓慢

3. 容器网络问题(K8s):
# 检查CNI插件(Calico/Flannel)状态
# 检查iptables规则数量(规则过多影响性能)
iptables -L -n | wc -l
# 检查conntrack表是否满
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

4. DNS解析慢:
# 微服务间调用如果用域名,DNS解析可能成为瓶颈
dig target_service.namespace.svc.cluster.local
# 检查CoreDNS的延迟和错误率

43. 🔴 Redis突然变慢,如何排查?

答:Redis变慢的排查需要从多个维度入手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Step 1:确认慢的程度
redis-cli --latency # 测量Redis延迟
redis-cli --latency-history # 延迟历史

# Step 2:检查慢查询日志
redis-cli SLOWLOG GET 10
# 查看最近10条慢查询
# 常见慢命令:KEYS *、HGETALL(大Hash)、SORT、大Key的DEL

# Step 3:检查大Key
redis-cli --bigkeys
# 扫描所有大Key
# 大Key的操作(读写、删除、过期)都可能导致阻塞

# Step 4:检查内存
redis-cli INFO memory
# used_memory_rss / used_memory > 1.5 说明内存碎片严重
# 碎片整理:CONFIG SET activedefrag yes

# Step 5:检查持久化
redis-cli INFO persistence
# rdb_last_bgsave_status:最近一次RDB是否成功
# aof_rewrite_in_progress:是否正在AOF重写
# RDB/AOF操作会fork子进程,大内存实例fork很慢

常见原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. 大Key操作:
- HGETALL一个100万field的Hash
- DEL一个包含100万元素的Set
- 解决:拆分大Key,使用HSCAN/SSCAN分批操作
- Redis 4.0+:UNLINK异步删除

2. 持久化阻塞:
- RDB fork子进程时,如果内存大(>10GB),fork耗时可能>1秒
- AOF重写同理
- 解决:控制实例内存大小(<10GB),使用SSD

3. 内存交换(Swap):
- Redis使用了Swap,性能急剧下降
- 检查:cat /proc/<redis-pid>/smaps | grep Swap
- 解决:确保物理内存充足,禁用Swap

4. 网络问题:
- 客户端和Redis之间网络延迟
- 使用Pipeline减少网络往返

5. CPU绑定:
- Redis是单线程,如果CPU被其他进程抢占
- 解决:taskset绑定CPU核心

44. 🔴 Kafka消费延迟(Consumer Lag)突然增大,如何排查和处理?

答:Consumer Lag是Kafka最常见的运维问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Step 1:查看Lag
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group my_consumer_group
# 输出每个分区的当前offset、最新offset、lag

# Step 2:确认是生产加速还是消费变慢
# 查看生产速率
kafka-run-class.sh kafka.tools.GetOffsetShell \
--broker-list localhost:9092 --topic my_topic --time -1
# 对比两个时间点的offset差值 = 生产速率

# 查看消费速率
# 对比两个时间点的consumer offset差值 = 消费速率

常见原因及处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1. 消费者处理慢:
- 消费逻辑中有慢操作(DB写入、RPC调用)
- 排查:在消费逻辑中添加耗时日志
- 解决:优化消费逻辑、异步处理、批量操作

2. 消费者数量不足:
- 分区数 > 消费者数,部分消费者处理多个分区
- 解决:增加消费者实例(不超过分区数)

3. 消费者Rebalance频繁:
- 消费者频繁加入/退出导致Rebalance
- Rebalance期间所有消费者暂停消费
- 排查:消费者日志中搜索"rebalance"
- 解决:
- 增大session.timeout.ms(默认10s→30s)
- 增大max.poll.interval.ms(默认5min→10min)
- 减小max.poll.records避免单次处理时间过长

4. 生产端突发流量:
- 上游突然产生大量消息
- 解决:临时增加消费者、增加分区数

5. 消费者GC停顿:
- GC导致消费者心跳超时,触发Rebalance
- 解决:优化GC参数,增大session.timeout.ms

45. 🔵 如何排查微服务的级联故障(雪崩)?

答:级联故障是分布式系统中最危险的故障模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
典型雪崩过程:
1. 数据库慢查询 → 连接池耗尽
2. 服务A调用数据库超时 → 线程池堆积
3. 服务B调用服务A超时 → 服务B的线程池也堆积
4. 服务C调用服务B超时 → 继续传播
5. 整个调用链全部不可用

排查方法:
1. 从告警时间线倒推:
- 哪个服务最先告警?
- 告警的传播路径是什么?
- 最先告警的服务就是根因所在

2. 调用链分析:
- 从异常Trace中找到最深层的慢Span
- 通常是数据库、缓存、或外部服务

3. 指标关联:
- 线程池使用率、连接池使用率
- 错误率、超时率
- 找到最先异常的指标

预防措施:
1. 超时设置:所有外部调用必须设置超时
2. 熔断器:Hystrix/Sentinel/Resilience4j
3. 限流:保护服务不被过载
4. 隔离:线程池隔离、信号量隔离
5. 降级:非核心功能自动降级

46. 🔴 线上出现TCP连接泄漏(CLOSE_WAIT堆积),如何排查和处理?

答:CLOSE_WAIT堆积是常见的连接泄漏问题,表示本端没有正确关闭连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Step 1:确认CLOSE_WAIT数量
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
# 或
netstat -ant | awk '{print $6}' | sort | uniq -c | sort -rn
# 如果CLOSE_WAIT数量持续增长 → 连接泄漏

# Step 2:找到CLOSE_WAIT连接的对端
ss -antp state close-wait
# 查看是哪个进程、连接到哪个地址

# Step 3:分析原因
# CLOSE_WAIT表示:对端已经发送FIN(关闭连接),但本端没有调用close()
# 即:本端代码没有正确关闭连接

常见原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. HTTP连接未关闭
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
InputStream is = conn.getInputStream();
// 读取数据后没有关闭连接
// 修复:try-with-resources + conn.disconnect()

// 2. 数据库连接未归还
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
// 异常路径没有关闭连接
// 修复:try-with-resources

// 3. Redis连接未归还
Jedis jedis = jedisPool.getResource();
jedis.get("key");
// 没有调用jedis.close()归还连接池
// 修复:try-with-resources

// 4. 异步回调中未关闭连接
httpClient.execute(request, new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse response) {
// 处理响应但没有关闭
EntityUtils.consume(response.getEntity()); // 必须消费Entity
}
@Override
public void failed(Exception ex) {
// 异常路径也要关闭
}
});

处理方案:

1
2
3
4
5
6
7
1. 代码修复:确保所有连接在finally/try-with-resources中关闭
2. 连接池泄漏检测:
- HikariCP:leakDetectionThreshold=60000
- Jedis:设置maxWaitMillis,超时报错而非无限等待
3. 内核参数调优(临时缓解):
# 减少CLOSE_WAIT的存活时间(不推荐,治标不治本)
echo 60 > /proc/sys/net/ipv4/tcp_keepalive_time

47. 🔴 如何排查DNS解析导致的性能问题?

答:DNS问题在微服务架构中很常见但容易被忽视。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 排查DNS解析时间
dig @dns-server target-service +stats
# 查看Query time

# Java应用的DNS缓存问题
# JVM默认缓存DNS解析结果:
# - 成功解析:永久缓存(networkaddress.cache.ttl=-1)
# - 失败解析:缓存10秒(networkaddress.cache.negative.ttl=10)
# 这在K8s环境中是个大问题:Pod IP变化后,JVM还在用旧IP

# 修复:
# 方法1:JVM参数
-Dsun.net.inetaddr.ttl=30 # DNS缓存30秒

# 方法2:java.security配置
networkaddress.cache.ttl=30
networkaddress.cache.negative.ttl=10

# K8s中的DNS问题:
# CoreDNS性能不足导致解析慢
# 检查CoreDNS的指标:
kubectl top pod -n kube-system -l k8s-app=kube-dns
# 如果CoreDNS CPU高,考虑:
# 1. 增加CoreDNS副本数
# 2. 使用NodeLocal DNSCache
# 3. 应用层DNS缓存

48. 🔵 服务发布后出现问题,如何快速回滚?回滚策略有哪些?

答:快速回滚能力是生产环境的生命线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
回滚策略:

1. 代码回滚:
- 回滚到上一个版本的代码/镜像
- K8s:kubectl rollout undo deployment/my-app
- 最常用,最可靠

2. 配置回滚:
- 如果是配置变更导致的问题
- 配置中心(如Apollo/Nacos)支持版本回滚
- 回滚后实时生效,无需重启

3. 数据库回滚:
- 如果执行了DDL/DML变更
- 需要提前准备回滚SQL
- 大表DDL回滚可能很慢,需要评估

4. 流量回滚:
- 灰度发布场景,将流量切回旧版本
- Istio:调整VirtualService的权重
- Nginx:调整upstream权重

回滚决策流程:
1. 发现问题(监控告警/用户反馈)
2. 快速评估影响范围
3. 如果影响核心业务 → 立即回滚,事后分析
4. 如果影响非核心功能 → 评估是否可以热修复
5. 回滚后验证服务恢复正常
6. 事后复盘(Postmortem)

关键原则:
- 回滚优先于修复(先止血再治病)
- 回滚操作必须提前演练
- 每次发布都要有回滚方案
- 数据库变更必须可回滚

49. 🔴 如何排查Java应用的类加载问题(ClassNotFoundException、NoClassDefFoundError、ClassCastException)?

答:类加载问题在复杂的Java应用中很常见,尤其是使用了多个ClassLoader的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
三种异常的区别:

ClassNotFoundException:
- 运行时动态加载类失败(Class.forName()、ClassLoader.loadClass())
- 类路径中确实没有这个类
- 排查:检查依赖是否正确引入

NoClassDefFoundError:
- 编译时存在,运行时找不到
- 通常是依赖冲突导致(Maven/Gradle引入了错误版本)
- 或者类的静态初始化块抛异常,导致类加载失败

ClassCastException:
- 同一个类被不同的ClassLoader加载了两次
- 两个ClassLoader加载的同名类是不同的Class对象
- 常见于OSGi、Tomcat多应用、热部署场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 排查工具:

# 1. 查看类从哪个jar加载
# Arthas:
sc -d com.example.MyClass
# 输出:classLoaderHash、codeSource(jar路径)

# 2. 查看类加载器层次
classloader -t # Arthas命令,显示ClassLoader树

# 3. Maven依赖冲突排查
mvn dependency:tree -Dincludes=com.google.guava
# 查看guava被哪些依赖引入了哪些版本

# 4. JVM参数查看类加载过程
-verbose:class # 打印所有类加载信息
# 或
-Xlog:class+load=info # JDK 11+

50. ⚫ 请描述一次你经历过的最复杂的线上故障排查过程。

答:这是一个开放性问题,考察候选人的实战经验和系统化思维。

优秀回答的要素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1. 故障现象描述:
- 什么时间发现的
- 影响范围多大
- 核心指标变化

2. 排查过程:
- 第一反应是什么(先止血还是先排查)
- 排查的思路和步骤
- 使用了哪些工具
- 走了哪些弯路
- 最终如何定位到根因

3. 根因分析:
- 技术根因是什么
- 为什么之前没有发现
- 是否有多个因素叠加

4. 修复方案:
- 短期修复(止血)
- 长期修复(根治)
- 如何验证修复有效

5. 复盘改进:
- 监控告警是否及时
- 排查工具是否完善
- 流程是否需要改进
- 如何防止类似问题再次发生

示例框架:
"某天凌晨2点收到告警,订单服务成功率从99.99%降到85%...
首先检查了最近的变更记录,发现2小时前有一次配置变更...
通过调用链追踪发现是库存服务超时...
进一步排查发现是数据库连接池泄漏...
根因是新引入的一个ORM框架在异常路径没有正确关闭连接...
临时修复:重启服务恢复连接池...
长期修复:修复代码 + 添加连接池泄漏检测 + 添加连接数监控告警..."

本模块共50题,覆盖CPU排查、内存泄漏、慢SQL分析、全链路追踪、可观测性设计、网络故障、中间件排障等核心主题。每道题都来源于真实的生产场景,能够有效考察架构师在高压环境下的系统化排障能力。