崩溃日志落盘(INV-CRASH-STDERR-01)
wesgine 使用 dup2 把 fd 2 指向文件,确保 Go runtime.throw 的崩溃栈被完整捕获。
为什么需要 dup2
Go 的 runtime.throw 绕过 os.Stderr 直写 fd 2。
runtime.throw → write(2, stackBytes) → exit(2)
如果 fd 2 指向管道(如 systemd journal),runtime.throw 打印前会 freezetheworld(),转发 goroutine 此后永不被调度——栈留在管道缓冲区里随 exit(2) 一起消失。
症状:崩溃了但没有现场。
teeStderrToFile
func teeStderrToFile(dataDir string) {
logPath := filepath.Join(dataDir, "logs", "wesgine-stderr.log")
f, _ := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
// dup2:把 fd 2 指向文件
syscall.Dup2(int(f.Fd()), 2)
// 从此 runtime.throw 的输出落盘
}
业务日志另行镜像
journal 一个字都收不到,而 unit 里 StandardError=journal 会让运维把空 journal 读成"引擎没启动"。
解决:bootLogger 把 slog 同时写文件与 dup 出来的原始 stderr。
slog → 文件(Debug 级)
slog → journal(Info 级,systemd 有速率限制)
Debug 不灌 journal:systemd 对单元有速率限制且丢弃超额,Debug 灌进去会以"引擎突然安静了"的形状复现同一个症状。
Bootstrap Admin Token
Bootstrap admin token 不走 logger,直接写 os.Stderr(即文件)。
理由:journal 常被转发出机器,admin token 不应出现在远端日志。
每次启动的第一行
version=v1.0.3 commit=abc1234 built=2026-09-01T00:00:00Z
wesgine: boot: hypervisor starting...
wesgine: boot: cell registry loaded (3 cells)
卡在哪一步、跑的是哪个 build,看这几行就够——不需要 strace。
排障起手式
- 看
{data-dir}/logs/wesgine-stderr.log - 不是 journal(panic 栈不在那里)
- 不是 nohup.out(同理)
- 每次启动有版本指纹和阶段里程碑
禁止的替代方案
| 方案 | 问题 |
|---|---|
| 管道 + goroutine 转发 | freezetheworld() 后转发 goroutine 不被调度 |
只用 os.Stderr | runtime.throw 绕过 os.Stderr |
| 只用 journal | journal 常被转发、有速率限制 |
| 不做镜像 | 运维报"无日志输出" |