跳到内容

第 0 卷 / 第 6 章 / 第 4 课

6.4 综合排障实验 ​

前置知识:本章前三页

预计时间:30 分钟

练习环境:Bash、Python 3.9+;Java 小节需要 JDK

完成标志:保存退出码与两个输出流,并写出一份最小排障记录

前三页一直在分析现有故障。本实验提供一个会崩溃的 Python 脚本和一个 Java 类:先保存证据,再做一次只改变一个变量的实验。

所有产物都放进临时目录,不把日志和 class 文件混进项目:

bash
workdir=$(mktemp -d)
cd "$workdir"

热身一:Python 运行时异常 ​

保存为 crash.py:

python
values = [10, 20, 30]
print(values[5])
bash
status=0
python3 crash.py >stdout.log 2>stderr.log || status=$?
printf 'status=%s\n' "$status"
sed -n '1,20p' stdout.log
sed -n '1,20p' stderr.log

记录:

  • 异常类型和消息;
  • 文件名与行号;
  • stdout.log 与 stderr.log 各有什么;
  • 退出码是否为 0。

然后只把 values[5] 改为 values[2],用同一组命令重跑。比较四项证据,不要只看终端颜色。

热身二:Java 编译阶段与运行阶段 ​

先保存一个能编译、运行时失败的 CrashMe.java:

java
public class CrashMe {
    public static void main(String[] args) {
        int[] values = {10, 20, 30};
        System.out.println(values[5]);
    }
}
bash
compile_status=0
javac CrashMe.java >compile.stdout.log 2>compile.stderr.log || compile_status=$?
run_status=not-run
if [ "$compile_status" -eq 0 ]; then
  run_status=0
  java CrashMe >run.stdout.log 2>run.stderr.log || run_status=$?
fi
printf 'compile_status=%s\n' "$compile_status"
printf 'run_status=%s\n' "$run_status"
sed -n '1,20p' compile.stderr.log
if [ "$run_status" != not-run ]; then
  sed -n '1,20p' run.stderr.log
fi

再故意删除一处分号。先移走旧产物,只运行编译器:

bash
rm -- CrashMe.class
compile_status=0
javac CrashMe.java >compile.stdout.log 2>compile.stderr.log || compile_status=$?
printf 'compile_status=%s\n' "$compile_status"
sed -n '1,20p' compile.stderr.log
test ! -e CrashMe.class

比较:编译错误有没有 Java 应用运行时栈?目录中是否还存在可运行的 CrashMe.class?真实脚本也应先检查编译退出码,不能失败后继续运行上一次留下的 class。

挑战:日志、异常与重复记录 ​

使用“日志基础”中的 worker.py:

  1. 运行一次,保存退出码、stderr 和 application.log;
  2. 找出同一个异常出现了几次;
  3. 在调用 process_item() 的外层再捕获并记录一次,观察重复日志;
  4. 删除其中一层记录,只保留拥有足够上下文的一层;
  5. 给日志增加虚构的 request_id=req-7f2a,用 grep -F 找齐同次操作。

验收重点不是“ERROR 越多越好”,而是同一失败有一条完整、可关联、无秘密的记录。

排障案例一:NoClassDefFoundError ​

Java 服务启动时报告:

text
java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/JsonFactory

这个错误发生在 JVM 需要类定义的运行阶段。调查顺序:

问题检查方法
依赖是否解析进来mvn dependency:tree
scope 是否适合运行检查 provided、test 和排除项
启动命令用了什么 classpath查看脚本、IDE run configuration 或容器入口
运行时制品中能否找到该类jar tf target/myapp.jar;普通 JAR 的依赖要另查外部 JAR,Spring Boot 嵌套依赖要继续查看 BOOT-INF/lib 中的 JAR
是否明确构建可执行或 fat JAR检查 Spring Boot、Shade 或其他插件配置
类是否曾初始化失败向前找 ExceptionInInitializerError 和最初异常

两个常见误区:

  • mvn package 默认打出的普通 JAR 通常不包含依赖类;仅仅从 compile 换到 package 不保证运行时类齐全。
  • dependency:tree 证明 Maven 解析了依赖,不证明你的启动命令把它放进了运行时 classpath。

先保存完整异常链和启动命令,再改 scope 或打包插件。不要一看到缺类就随机添加多个版本。

排障案例二:找不到日志文件 ​

配置写的是相对路径 ./logs/app.log,本地能看到,容器里却找不到。

相对路径相对于进程工作目录,不相对于源码目录。先查部署方式:

bash
# 普通 Linux 进程,PID 替换成实际值
readlink -f /proc/PID/cwd

# systemd 服务
systemctl show myapp.service -p WorkingDirectory
journalctl -u myapp.service --since '30 minutes ago'

# Kubernetes
kubectl logs pod/myapp --since=30m

容器应用可能根本不写文件,而是把日志交给标准输出采集。确认配置后再找路径,不要从文件系统根目录盲目 find,那既慢又可能越过权限边界。

排障案例三:看似相同的导入错误 ​

错误先查什么
Python ModuleNotFoundError当前解释器、虚拟环境、包名与导入名、安装位置
Java ClassNotFoundException主动按名称加载类时的 classpath
Java NoClassDefFoundError编译时可见但运行时不可得、依赖缺失或初始化失败

这些错误不能统一归结为“依赖没装”。先确认实际运行的是哪个解释器或 JVM,以及启动命令使用了什么路径。

最小排障记录 ​

完成任一案例后填写:

text
现象:
首次发生时间与时区:
影响范围:
版本与环境:
完整复现命令:
预期结果:
实际结果和退出码:
相关日志 / request_id / trace_id:
当前假设:
支持该假设的证据:
可能推翻它的下一项实验:

验收 ​

  • [ ] 能分别保存 stdout、stderr 和退出码;
  • [ ] 能说出编译失败与运行时异常的差别;
  • [ ] 能从 Java 或 Python 栈中指出类型、消息和相关帧;
  • [ ] 能说明普通 Maven JAR 不自动包含运行依赖;
  • [ ] 能用请求 ID 查询一组日志,且日志不含真实秘密;
  • [ ] 能交付含环境、命令、预期和实际结果的最小复现。

清理前保留需要的排障记录,确认临时目录后删除:

bash
cd
ls -ld -- "$workdir"
rm -r -- "$workdir"

保存好复现命令、两路输出和退出码,再检查修复前后的区别。下一章把程序放进 Docker 容器,继续追问运行环境由什么决定、容器又隔离了哪些资源。

→ 下一步:Docker 基础

Built with VitePress | Software Systems Atlas