Java故障排查

Java 应用从运维视角排查启动、CPU、内存、GC、线程、连接、日志和 Kubernetes 故障。

Java 故障排查

本文档面向运维和值班场景,适用于 Spring Boot、Tomcat、Dubbo 等 Java 服务。原则:先保留现场,再定位根因;先摘流量或扩容,再考虑重启。

排查总流程

确认影响范围
  -> 看服务状态和日志
  -> 看系统资源
  -> 按症状采集 JVM 现场
  -> 检查下游依赖
  -> 临时止血
  -> 复盘和长期修复

先记录这些信息:

  • 故障开始时间、影响接口、影响实例。
  • 最近是否发布、变更配置、扩容、切流或迁移。
  • 是单实例异常、单节点异常,还是整组服务异常。
  • 是否有 OOM、重启、探针失败、下游超时、连接池耗尽。

单机排查步骤

1. 服务是否存活

jps -lv
ps -ef | grep java
ss -lntp | grep <port>

如果进程不存在,先看启动日志和系统日志:

tail -n 200 logs/app.log
journalctl -u <service> -n 200 --no-pager
dmesg -T | tail -n 100

2. 日志是否有明确异常

grep -iE 'exception|error|caused by|timeout|refused|oom|deadlock' logs/app.log | tail -n 100
tail -f logs/app.log

重点关注最后一个 Caused by,常见关键词:

关键词方向
OutOfMemoryError堆、Metaspace、直接内存
RejectedExecutionException线程池满或队列满
SocketTimeoutException下游慢或网络异常
Connection refused下游未监听、Service 异常
Too many open files文件句柄耗尽
Communications link failure数据库连接异常

3. 系统资源是否异常

top
free -h
df -h
df -i
iostat -x 1
ss -s

判断方向:

  • CPU 高:进入 CPU 排查。
  • 内存高或 OOM:进入内存排查。
  • 磁盘满:先清理日志或扩容,再查日志暴涨原因。
  • IO 高:关注日志写入、GC、数据库慢盘。
  • 连接异常:检查端口、DNS、下游连接数。

JVM 现场采集

建议先创建采集目录:

export PID=<pid>
export TS=$(date +%F-%H%M%S)
mkdir -p /tmp/java-${PID}-${TS}
cd /tmp/java-${PID}-${TS}

通用采集:

ps -fp ${PID} > process.txt
cat /proc/${PID}/limits > limits.txt
cat /proc/${PID}/status > status.txt
jcmd ${PID} VM.command_line > vm-command-line.txt
jcmd ${PID} VM.flags > vm-flags.txt
jcmd ${PID} GC.heap_info > gc-heap-info.txt
jstat -gcutil ${PID} 1000 10 > jstat-gcutil.txt

连续采集线程栈:

for i in 1 2 3; do
  jstack -l ${PID} > jstack-${i}.txt
  sleep 5
done

需要堆转储时:

jcmd ${PID} GC.heap_dump /tmp/heap-${PID}-${TS}.hprof

堆转储前确认磁盘空间,文件通常很大。

典型故障

CPU 高

top -H -p ${PID}
printf '%x\n' <tid>
jstack -l ${PID} > jstack.txt
grep -n "nid=0x<hex_tid>" jstack.txt

判断:

  • 线程在业务代码:排查循环、正则、序列化、加解密、热点逻辑。
  • 线程在 GC:转入 GC 排查。
  • 多线程都在下游调用:排查数据库、Redis、HTTP 依赖。

内存和 OOM

jstat -gcutil ${PID} 1000 10
jcmd ${PID} GC.heap_info
cat /proc/${PID}/status | egrep 'VmRSS|VmSize|Threads'
dmesg -T | grep -i 'killed process'

判断:

  • Old 区长期高位并频繁 Full GC:堆不足或泄漏。
  • Metaspace OOM:类加载过多或动态生成类异常。
  • Direct buffer memory:直接内存或 Netty 类组件异常。
  • 系统 OOM Kill:进程总内存超过系统或容器限制。

GC 频繁

jstat -gcutil ${PID} 1000
jcmd ${PID} VM.flags | tr ' ' '\n' | grep -E 'GC|Heap|RAM|Metaspace'
jcmd ${PID} VM.command_line | tr ' ' '\n' | grep -E 'Xlog|Xloggc|PrintGC'

关注:

  • FGC 是否持续增加。
  • GC 暂停是否和接口超时重合。
  • 容器内存限制是否和 -XmxMaxRAMPercentage 匹配。

线程阻塞或死锁

jstack -l ${PID} > jstack-lock.txt
grep -nE 'BLOCKED|WAITING|TIMED_WAITING|Found one Java-level deadlock' jstack-lock.txt

大量线程卡在数据库、Redis、HTTP 客户端或日志写入时,继续检查下游连接、网络和连接池配置。

连接池或下游异常

ss -antp | grep ${PID} | awk '{print $1}' | sort | uniq -c
ss -antp | grep ':3306' | wc -l
ss -antp | grep ':6379' | wc -l
curl -v http://<downstream-host>:<port>/<health-path>

数据库侧:

SHOW PROCESSLIST;
SHOW ENGINE INNODB STATUS\G

判断:

  • CLOSE-WAIT 多:应用未正确关闭连接。
  • SYN-SENT 多:网络或下游端口不可达。
  • 连接池日志出现 HikariPoolDruid 超时:看连接泄漏、慢 SQL、下游容量。

Kubernetes 集群内排查

1. 找到异常范围

export NS=<namespace>
export APP=<app-label>

kubectl get deploy,sts,pod,svc,endpoints -n ${NS} -o wide
kubectl get pod -n ${NS} -l app=${APP} -o wide

看是否集中在同一版本、同一 Node、同一可用区,关注 READYRESTARTSAGE

2. 看 Pod 状态和事件

export POD=<pod-name>

kubectl describe pod ${POD} -n ${NS}
kubectl get event -n ${NS} --sort-by=.lastTimestamp | tail -n 50

重点看:

  • Last State 是否为 OOMKilled
  • 是否有 Readiness probe failedLiveness probe failed
  • 是否发生镜像拉取、挂载、调度失败或驱逐。
  • requestslimits 是否过小。

3. 看日志

kubectl logs ${POD} -n ${NS} --tail=300
kubectl logs ${POD} -n ${NS} --previous --tail=300
kubectl logs ${POD} -n ${NS} -c <container> --tail=300

--previous 对排查崩溃重启非常关键。

4. 进入容器采集 JVM

kubectl exec -it ${POD} -n ${NS} -- sh
pgrep -laf java
export PID=<pid>
jstack -l ${PID} > /tmp/jstack.txt
jcmd ${PID} VM.flags > /tmp/vm-flags.txt
jstat -gcutil ${PID} 1000 10 > /tmp/jstat-gcutil.txt

拷贝文件:

kubectl cp ${NS}/${POD}:/tmp/jstack.txt ./jstack.txt
kubectl cp ${NS}/${POD}:/tmp/vm-flags.txt ./vm-flags.txt

如果镜像没有 JDK 工具,使用调试容器或运维工具镜像辅助排查。

5. 查资源、探针和网络

kubectl top pod ${POD} -n ${NS}
kubectl top node
kubectl get pod ${POD} -n ${NS} -o jsonpath='{.spec.containers[*].resources}'
kubectl get pod ${POD} -n ${NS} -o yaml | grep -A20 -E 'startupProbe|livenessProbe|readinessProbe'
kubectl get svc,endpoints -n ${NS} -o wide

容器内看 cgroup:

cat /sys/fs/cgroup/memory.max 2>/dev/null
cat /sys/fs/cgroup/memory.current 2>/dev/null
cat /sys/fs/cgroup/cpu.max 2>/dev/null

临时网络探测:

kubectl run netshoot-${TS} -n ${NS} --rm -it --image=nicolaka/netshoot -- sh
curl -v http://<service>.<namespace>.svc.cluster.local:<port>/<path>
dig <service>.<namespace>.svc.cluster.local
nc -vz <service> <port>

应急处理

按影响从小到大处理:

  • 摘除异常实例流量。
  • 扩容同版本实例。
  • 限流或降级高风险入口。
  • 回滚最近发布。
  • 采集现场后重启单实例。
  • 临时调大资源限制或连接池限制。

Kubernetes 常用操作:

kubectl scale deploy <deploy> -n ${NS} --replicas=<n>
kubectl rollout restart deploy <deploy> -n ${NS}
kubectl rollout status deploy <deploy> -n ${NS}
kubectl rollout history deploy <deploy> -n ${NS}
kubectl rollout undo deploy <deploy> -n ${NS}

重启或回滚前至少保留:

  • 最近应用日志和 --previous 日志。
  • 线程栈 3 份。
  • JVM 参数和 GC 状态。
  • Pod 事件和 describe 输出。
  • 必要时 Heap Dump。

命令速查

场景命令
Java 进程jps -lv、`ps -ef
端口监听`ss -lntp
线程 CPUtop -H -p <pid>
线程栈jstack -l <pid>
GC 观察jstat -gcutil <pid> 1000
JVM 参数jcmd <pid> VM.flags
堆信息jcmd <pid> GC.heap_info
堆转储jcmd <pid> GC.heap_dump /tmp/heap.hprof
文件句柄`ls /proc/<pid>/fd
Pod 状态kubectl describe pod <pod> -n <ns>
Pod 日志kubectl logs <pod> -n <ns> --previous

案例:K8s 中 Java 服务反复重启

现象:

  • 订单服务 10 分钟内多次重启。
  • 用户偶发 502,监控显示 Pod RESTARTS 持续增加。
  • 应用日志只有启动日志,没有明显业务异常。

排查:

export NS=prod
export POD=order-api-6f8c9f9c8b-abcde

kubectl describe pod ${POD} -n ${NS}
kubectl logs ${POD} -n ${NS} --previous --tail=200
kubectl top pod ${POD} -n ${NS}

关键信息:

Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Limits:
  memory: 1024Mi

继续确认 JVM 参数:

kubectl exec -it ${POD} -n ${NS} -- sh
pgrep -laf java
jcmd <pid> VM.flags | tr ' ' '\n' | grep -E 'Xmx|MaxRAM|Metaspace'

发现应用配置 -Xmx1024m,容器内存 limit 也是 1024Mi,没有给 Metaspace、线程栈、直接内存和 JVM Native 内存预留空间。

处置:

kubectl scale deploy order-api -n ${NS} --replicas=6
kubectl set resources deploy order-api -n ${NS} --limits=memory=1536Mi
kubectl rollout status deploy order-api -n ${NS}

后续修复:

  • -Xmx 调整为容器内存的 60% 到 70%。
  • 补充 JVM 内存、Pod OOMKilled、重启次数告警。
  • 发布前检查 resources.limits.memory 与 JVM 参数是否匹配。
  • 保留 --previous 日志和 Pod 事件作为复盘证据。

复盘清单

  • 根因:代码、配置、容量、下游、网络、发布、数据。
  • 影响:接口、实例、用户范围、持续时间。
  • 证据:日志、线程栈、GC、监控、K8s 事件。
  • 处置:摘流量、扩容、回滚、重启、参数调整。
  • 改进:监控告警、容量水位、探针配置、限流降级、发布策略。