53e7d1e5f2
文档/ (2026-09-16; 老板定调: 这是 agent 底座, 所以必须扎实, 现在越扎实以后开发越简单)
00-索引 文档地图 + 30 秒概念速查 + 三条命令跑起来 + 事实源优先级
01-快速上手 体检 -> 建库 -> 起内核 -> 起驱动 -> 收工, 全带实测输出; 第一次最易踩的四个坑
02-写一个驱动 五分钟最小驱动 / 形态选择 / 能碰哪些表 / 汇报与调用两份模板 / 交付检查表
03-命令手册 两层每条命令 + 日志选项 + 退出码约定 + --json 样例 + 日常十条
04-契约与调用 一次调用的完整生命周期 / 六条仲裁 / 锁与按需拉起 / 排障表
05-日志与排障 三条道怎么读 + "症状->判据->处置"总表 + 断电收尸语义
06-架构与不变量 分层 / 14 条硬不变量 / 主流程表 / 双真相 / 为什么故意不做 / 已知薄弱点
07-模块与接口 逐模块职责与公开接口 + "想改 X -> 动哪几处"连带清单
08-数据模型 8 张表逐字段 (谁写谁读) + events.kind 字典 + 状态机 + 快照 + 排查 SQL
09-扩展指南 六个配方 (加子命令/加字段/加表/加日志来源/加自测/改判定) + 同步清单
10-验收与质量门 四道门 + 五份自测明细 + pyright 严格档 + 26 条已知坑总表 + 发布 checklist
规矩: 不重复设计文档 / 每条命令实测过再写 (含 jq 表达式) / 代码>设计>文档 的事实源优先级 /
改代码必须同步文档 (清单在 09 末尾) / 暂时没做到的事写成"已知边界"不含糊过去
修复: 同锁串行化原来是死的 (实测抓到的真缺陷)
旧行为: db.领调用 只领 pending (waiting 没人再碰) + db.同锁在跑 把 waiting 也算"占着锁"
-> 同一把锁上两条请求互相排队, 双双停在 waiting 谁也不跑 (实测 id 16/17);
而 收权超时 只收 running -> 排队连超时都没有 = 死锁
修法: ① db.领调用 的 SQL 改 state IN ('pending','waiting') -- 每轮把排队的领回来重判, 锁一空就推进
② db.同锁在跑 只认 state='running' (排队的还没拿到锁, 不挡人)
③ 内核.转发调用 waiting 分支补 deadline (排队也立期限); 内核.收权超时 遍历 running + waiting
④ 抽出 内核.期限文本() 统一算 deadline
实测: 两条同锁调用串行跑完 (19.started_at == 18.finished_at); 排队者超时被收权 (events 有记录)
回归: 自测db.py 调用组 +4 条断言 (waiting 不算占着锁 / waiting 会被重新领 / ...);
去掉一条依赖生产库全局计数的脆弱断言
其它: 内核 与 引导器 的 用法() 末尾加文档指引
验收: uvx pyright 0 errors / 0 warnings; 五份自测全过 (进程/内核 58/配置/db/日志 86);
试跑引导器.py PASS 11 / FAIL 0 / 残留无; 残留进程 0
9.0 KiB
9.0 KiB
05 · 日志与排障
出事先看四条:
日志 --内核(内核怎么想的)、内核.out.log(命令输出与崩溃原文)、日志 <驱动>(驱动说了什么)、事件 -n 50(谁在什么时候干了什么)。这份文档先讲三条道怎么读, 再给一张"症状 → 判据 → 处置"的总表。
1. 三条道(一个文件只装一种内容)
| 文件 | 里面是什么 | 从哪看 |
|---|---|---|
内核/logs/内核.log |
只有结构化日志行(内核自己写) | 内核/内核.py 日志 --内核 / UEFI.boot.py 内核 日志 |
内核/logs/内核.out.log |
内核进程的 stdout/stderr 原始流:命令输出(表格/JSON)+ 未捕获的崩溃原文 | 内核 日志 --输出 |
内核/logs/引导器.log |
引导器的动作:开始 / 收工 / 移交 / 内核启停 / 所有 WARN | UEFI.boot.py 日志 |
驱动/<名>/logs/<名>.log |
驱动的 stdout/stderr 原样(内核不解析,每轮启动前插一条分隔头) | 内核/内核.py 日志 <名> |
行格式(内核/引导器那份):
2026-09-16T21:08:35+08:00 INFO [内核] 常驻调度启动 pid=26456
└── 时刻(带时区) └级别 └来源 └内容(不截断)
为什么分三条:以前命令输出跟日志行挤在一个文件里(引导器把内核 stdout 一起重定向进去了),
想按级别筛一条都做不到。现在日志行归内核自己写、stdio 归 .out.log,两边都干净。
档案(修前的脏文件)在 归档/20260916-日志系统重做前/。
2. 级别、门槛、轮转
| 项 | 配置键(环境.efi.json) |
默认 | 行为 |
|---|---|---|---|
| 门槛 | log_level |
INFO |
低于它的不写(DEBUG 不落盘);空串 = 全写 |
| 单份上限 | log_max_mb |
5 |
超过就轮转成 .1;0 = 不轮转 |
| 保留份数 | log_keep |
3 |
留几份历史(.1/.2/.3);0 = 不轮转 |
| 默认行数 | log_lines |
200 |
日志 命令不写 -n 时给几行 |
- 轮转只在拉起进程之前做(运行中改名会让进程继续写老 inode = 日志丢)。内核拉驱动前、 引导器拉内核前各轮转一次;引导器每次跑也轮转自己的。
- 读的时候把历史份一起算:
-n 200拿到的是"跨轮转的最近 200 行"。 - 驱动日志是别人的原始输出,
--级别只能靠字样猜(含ERROR/Traceback/失败→ ERROR, 含WARN/警告/超时/retry→ WARN,其余 INFO)。这一点必须记住:猜的,不是规范。
./.venv/bin/python 内核/内核.py 日志 --内核 --级别 WARN -n 50 # 内核只说警告以上
./.venv/bin/python 内核/内核.py 日志 样板常驻 -g 心跳 -n 20 # 只看含"心跳"的行
./.venv/bin/python 内核/内核.py 日志 --全部 -n 20 # 所有驱动各一段
./.venv/bin/python 内核/内核.py 日志 --json | jq -r '.["消息"]' # 机器读(JSON Lines)
python3 UEFI.boot.py 日志 --级别 WARN -n 30 # 引导器自己的动作
3. 现场状态从哪看(判活一律回 /proc)
./.venv/bin/python 内核/内核.py 状态 # 全部驱动: 状态/PID/运行时长/配置一致性
./.venv/bin/python 内核/内核.py 状态 样板常驻 --json # 单驱动详情: 命令行/内存/CPU/重启次数/契约
python3 UEFI.boot.py 内核 状态 # 内核自己: pid/启动时刻/今日次数/未收尾台账
库里记的 pid 只是账:真正判活用 /proc/<pid>/cmdline 校验(pid 会被系统复用 —— 宁可不杀,
不可误杀)。状态 里若写"现判 X 而库里记的是 Y",以现场为准。
4. 症状 → 判据 → 处置(总表)
环境 / 引导器
| 症状 | 判据 | 处置 |
|---|---|---|
体检不过: venv 不健康 |
环境 --json 的 3 项详情(pyvenv.cfg 里的版本/路径与实际不符) |
python3 UEFI.boot.py 环境 重建 |
包缺失 |
第 4 项列出缺哪个(必需项缺才阻塞) | python3 UEFI.boot.py 包 安装 |
| PG 那项 WARN | 第 6 项 pg_ok=false(引导器自己的命令不因此失败) |
查 PG 起没起、socket 路径对不对(环境.efi.json 的 db.host) |
| 内核命令报"连不上 PG" | 内核不降级(它的内存就是 PG) | 同上;PG 恢复后 内核 重启 |
内核进程
| 症状 | 判据 | 处置 |
|---|---|---|
启动 后 0.6s 就退出 |
[守护] 启动后 0.6s 就退出 —— 内核入口是空文件 / 缺包 |
内核 日志 --级别 ERROR;跑 环境 重建 + 包 安装 |
ERROR 已经有一个内核在跑了 (抢不到调度锁) |
PG 会话级咨询锁(0x65666901)被占 |
内核 状态 看是谁;要重来用 内核 重启 |
| 内核反复重连 PG | 日志里 重连 PG 失败 (第 N/10 次) |
查 PG;连续 10 次它会退出,让引导器看门狗发现 |
| 停止时报"还有 N 个没停掉" | /proc 复查不为空 |
逐个看 状态;试跑引导器.py 的残留检查也能抓 |
驱动
| 症状 | 判据 | 处置 |
|---|---|---|
无效 (invalid),说明写 协议版本不支持 |
配置.efi.json 的 efi 不是内核支持的版本 |
改成 1 |
无效:runtime 只能是 python 或 exec |
字段拼错或值不对 | 改配置 |
无效:入口路径越界 / 入口文件不存在 |
entry 用了 ..、绝对路径、或文件真不在 |
改成驱动根内的相对路径 |
无效:驱动名重复 |
两个文件夹声明了同一个 name |
改 name(先到的先生效) |
无效:契约无人提供 |
它的 needs 没有任何驱动的 provides 对上 |
补提供方,或去掉这个 needs |
无效:依赖成环 (契约链: A -> B -> A) |
契约依赖成环 | 拆环(扫描期就能看到) |
失败 (failed),last_error 里有"日志末尾" |
拉起后秒退;尾部只算本次启动的输出 | 日志 <名> -n 50 看它临死说了什么;常驻驱动建议手动 python3 <入口> 跑一遍 |
崩了 (crashed) |
旧状态说在跑但 /proc/<pid> 没了(断电/被杀) |
看 restart / autostart;想让它自己回来就配 restart=on-failure |
挂起 (zombie) |
进程已死、父进程没回收 | 内核会自己回收;状态 如实报 崩了 |
列表里显示 待重启 |
boot_hash != config_hash(配置改过,进程还用旧配置跑) |
内核/内核.py 重启 <名> |
| 改了配置/代码没生效 | 同上(配置有指纹) | 重启;改代码同理 |
调用 / 锁
| 症状 | 判据 | 处置 |
|---|---|---|
请求一直 pending |
常驻内核不在跑 | python3 UEFI.boot.py 内核 启动 --守护 |
denied |
calls.error 写明哪一条(越权/成环/没人提供/起不来) |
见 04-契约与调用.md 第 7 节 |
卡 waiting |
同一 lock_key 有 running |
正常排队;等对方 done 会自动推进;超时由内核收权(默认 60s) |
timeout |
过了 deadline |
查提供方为什么慢;waiting 也可能超时(排队没等到头) |
事件里 依赖失效: 上游不在跑 … |
上游状态是 崩了/失败(只是没启动不算) |
起上游,或查上游为什么崩 |
日志本身
| 症状 | 判据 | 处置 |
|---|---|---|
内核 日志 是空的 |
你看的是结构化日志,内核刚起还没说什么 | 试 内核 日志 --输出(命令输出/崩溃原文在那份里) |
| 日志文件几十 MB | 轮转没生效(log_max_mb 被设成 0?) |
查 环境.efi.json;下一轮启动会自动轮转 |
| 驱动日志里几轮输出连成一片 | 那是上次启动的输出(分隔头之前) | 用 ==== 启动 … ==== 分隔头定位这一轮 |
| 事件表被刷满 | 某驱动汇报太密 | 让它的上报间隔变大(样板是 30s 一条) |
5. 断电之后(本机 22:30 断电,这是必做项)
来电后按顺序:
python3 UEFI.boot.py --check # 1 环境 (venv/包/PG 都可能被动过)
python3 UEFI.boot.py 内核 启动 --守护 # 2 内核一启动就先"收尸"
./.venv/bin/python 内核/内核.py 状态 # 3 看谁被认领 / 谁判"崩了"
./.venv/bin/python 内核/内核.py 事件 -n 20 # 4 时间线: 最后一条 heartbeat 就是死的那一刻
收尸语义(设计 02 第 5 节):
| 库里的状态 | /proc 实际 |
内核判成 | 动作 |
|---|---|---|---|
running 有 pid |
在且 cmdline 匹配 | running |
认领(不重起),驱动不受内核生死影响 |
running 有 pid |
没了 | crashed |
记 exit 事件;autostart=true 才拉起 |
running 有 pid |
在但 cmdline 不匹配 | exited |
清 pid,不杀(pid 被复用) |
stopped / 无记录 |
— | stopped |
无 |
| 其它状态 | 在且匹配 | running |
"认领回来"(有人绕过内核对它做了什么) |
内核自己死了不连累驱动(驱动是独立会话,start_new_session)—— "内核可以随时死"是设计而不是缺陷。