一次基础设施崩溃如果只发生在万分之一的请求、千分之一的机器、某个看似随机的时间窗口里,传统调试手段很容易失灵。OpenAI 工程师这次处理的不是单个 core dump,而是一批 core dump:像做流行病学调查一样,把崩溃样本聚类、比对、追踪共同特征,最终同时定位到硬件故障和一个存在了 18 年的软件 bug。
单个 core dump 只能讲故事,成批 core dump 才能给证据
core dump 的价值很直接:进程崩溃时,操作系统把进程内存、寄存器、线程栈等状态保存下来。拿到一个 core 文件,你可以用 gdb 看到崩溃点、调用栈、局部变量,复盘“这个进程死前发生了什么”。
问题在于,稀有基础设施崩溃通常不是“看一眼栈就懂”的类型。单个样本可能误导你:
- 栈顶函数只是受害者,不是根因。
- 内存破坏可能早在几十秒前发生。
- 硬件异常和软件 bug 的症状可能长得很像。
- 重启、调度、负载、机器型号会制造噪声。
这次案例的关键不是某个神奇调试命令,而是规模化:把大量 core dump 当作数据集处理。工程师不是问“这个 core 为什么崩”,而是问“这些 core 之间有什么共同模式”。这也是“core dump epidemiology”这个说法最有意思的地方:崩溃像病例,机器、二进制版本、信号、栈指纹、寄存器状态、硬件信息都是流行病学变量。
稀有崩溃要先分群,再谈根因
在基础设施环境里,最怕把不同问题混在一起分析。一个硬件故障和一个软件 bug 可能都表现为 SIGSEGV,但处理路径完全不同。大规模 core dump 分析的第一步通常是给崩溃建立“指纹”。
一个实用指纹可以包含:
- 崩溃信号,例如
SIGSEGV、SIGABRT、SIGILL。 - 可执行文件和构建 ID。
- 线程崩溃栈的前 N 帧函数名。
- 指令指针附近的符号。
- 机器、CPU、内核版本、实例类型等环境信息。
指纹不是为了完美描述崩溃,而是为了把样本分桶。分桶之后,你才能看到某一类崩溃是否集中在某些主机、某个版本、某个调用路径,或者某个硬件批次上。
在来源案例里,工程师最终发现了两类问题:一类来自硬件故障,另一类来自长期潜伏的软件 bug。这个结果本身很重要:如果只盯着软件栈,硬件问题可能被误判成野指针;如果只怀疑硬件,18 年的软件缺陷又会继续潜伏。
可以这样实践:给 core dump 批量生成栈指纹
下面是一个可改造的小脚本,用 gdb 批量读取 core dump,提取崩溃线程调用栈,并按栈指纹聚类。它不是来源系统的实现,只是一个可以在团队里落地的最小版本。
运行前需要准备:
- Linux 环境。
- 已安装
gdb。 - 有对应的可执行文件和 core dump。
- 二进制最好保留符号,或者能通过 debuginfo 找到符号。
目录示例:
mkdir -p cores
# 假设你的 core 文件在 cores/ 下:
# cores/core.1234
# cores/core.5678
保存为 cluster_cores.py:
#!/usr/bin/env python3
import argparse
import hashlib
import json
import re
import subprocess
from collections import defaultdict
from pathlib import Path
FRAME_RE = re.compile(r"^#\d+\s+(?:0x[0-9a-fA-F]+\s+in\s+)?([^\s(]+)")
def gdb_backtrace(binary: Path, core: Path) -> str:
cmd = [
"gdb",
"--batch",
"-q",
str(binary),
str(core),
"-ex",
"set pagination off",
"-ex",
"thread apply all bt 8",
]
result = subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return result.stdout
def stack_signature(gdb_output: str, max_frames: int = 6) -> tuple[str, list[str]]:
frames = []
in_first_thread = False
for line in gdb_output.splitlines():
if line.startswith("Thread "):
if in_first_thread and frames:
break
in_first_thread = True
continue
if not in_first_thread:
continue
match = FRAME_RE.match(line.strip())
if match:
frames.append(match.group(1))
if len(frames) >= max_frames:
break
if not frames:
frames = ["<no-symbolized-frames>"]
raw = "|".join(frames)
digest = hashlib.sha256(raw.encode()).hexdigest()[:12]
return digest, frames
def main() -> None:
parser = argparse.ArgumentParser(description="Cluster core dumps by stack signature")
parser.add_argument("--binary", required=True, type=Path, help="Path to crashed executable")
parser.add_argument("--cores", required=True, type=Path, help="Directory containing core files")
args = parser.parse_args()
clusters = defaultdict(list)
frame_examples = {}
for core in sorted(args.cores.glob("core*")):
output = gdb_backtrace(args.binary, core)
signature, frames = stack_signature(output)
clusters[signature].append(str(core))
frame_examples.setdefault(signature, frames)
report = []
for signature, files in sorted(clusters.items(), key=lambda item: len(item[1]), reverse=True):
report.append(
{
"signature": signature,
"count": len(files),
"example_frames": frame_examples[signature],
"cores": files[:10],
}
)
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()
运行:
chmod +x cluster_cores.py
./cluster_cores.py --binary /path/to/your/service --cores ./cores
输出类似这样:
[
{
"signature": "9d8a9d9b9b70",
"count": 17,
"example_frames": [
"crash_here",
"handle_request",
"worker_loop",
"start_thread"
],
"cores": [
"cores/core.1234",
"cores/core.5678"
]
}
]
下一步可以把机器元数据拼进去。例如 core 文件名里带 host,或者旁边有一份 metadata.jsonl:
{"core":"cores/core.1234","host":"worker-a17","kernel":"6.6.12","cpu":"model-x","build_id":"2025.01.18.4"}
{"core":"cores/core.5678","host":"worker-a17","kernel":"6.6.12","cpu":"model-x","build_id":"2025.01.18.4"}
当某个指纹集中出现在同一台机器、同一类 CPU、同一批内存条、同一内核版本上,调查方向就会改变。你不再只是读 C++ 代码,还要看硬件日志、MCE 记录、内核告警和部署拓扑。
把硬件也放进嫌疑名单
工程师排查稀有崩溃时很容易默认“软件有错”。这通常是对的,但不是永远对。来源案例明确提到,分析同时揭示了硬件故障和长期软件 bug。这提醒我们:在大规模基础设施里,硬件不是抽象层下面的稳定地基,而是会以低概率、难复现、看似软件异常的方式冒出来。
可以这样把硬件信号纳入排查流程:
# 查看内核日志中的机器检查异常、内存错误、总线错误等线索
sudo dmesg --ctime | grep -Ei 'mce|machine check|edac|ecc|memory error|bus error|hardware error'
# 如果系统使用 systemd journal
sudo journalctl -k --since '24 hours ago' | grep -Ei 'mce|edac|ecc|hardware error'
# 记录 CPU 和内核信息,便于和 core 指纹关联
uname -a
lscpu
这些命令不能直接证明根因,但能帮你避免把所有异常都塞进“未知软件 bug”这个桶里。真正可靠的判断通常来自交叉证据:core 指纹、主机集中度、硬件日志、重启记录、负载曲线、版本变更历史。
18 年老 bug 的教训:时间不会自动修复低频缺陷
一个 bug 能存在 18 年,通常不是因为所有人都粗心,而是因为触发条件苛刻、症状不稳定、复现成本高,或者每次出现都被当成孤例处理。稀有 bug 最需要的是积累证据的机制。
对工程团队来说,可以从几个动作开始:
- 保留崩溃现场:开启 core dump,保存构建 ID 和符号信息。
- 给崩溃自动分桶:至少按信号、二进制版本、栈指纹聚类。
- 关联环境元数据:主机、内核、CPU、镜像、部署批次都要能查。
- 不急着合并不同症状:先分群,再分析根因。
- 同时考虑软硬件:尤其是崩溃集中在少数机器或特定硬件批次时。
风险也要说清楚:core dump 可能包含敏感数据,例如请求内容、密钥片段、用户数据或模型中间状态。采集前要设置访问控制、保留周期、加密和脱敏策略。不要为了调试便利,把 core dump 变成另一个数据泄漏入口。
落地建议
如果你的服务已经跑在生产环境,先别追求一步到位的平台。更实际的路线是:为关键进程启用 core dump;确保每个发布产物能找到符号;写一个小的批处理脚本生成栈指纹;把结果和主机元数据放进一张可查询的表。
当崩溃样本从“零散文件”变成“可统计的数据集”,排查方式会发生变化。你不再靠直觉追一个幽灵般的栈,而是在问更工程化的问题:这一类崩溃有多少?从什么时候开始?集中在哪些机器?和哪个版本、哪个硬件、哪个部署批次相关?这正是稀有基础设施故障最需要的视角。