调试与排错:日志、fail-fast 清单与演练
XServer 没有管理后台,排错靠三样东西:结构化的启动日志、点名到字段的 fail-fast 错误、以及可以自己跑的测试与演练基线。本章把它们整理成你的第一份排错手册。
本章导读
6.1 引入
一个没有管理界面的服务器,排错能力必须长在别的地方。XServer 的选择是:把诊断信息做成确定性事件——同一份配置、同一个状态,日志永远按同样的顺序出现同样的行;配置错误永远在启动时点名具体字段退出,绝不"带病运行"。确定性意味着可对照:你拿到的每一行异常输出,都能在源码的测试用例里找到孪生。
本章的三节分别覆盖三个故障域:启动期(日志顺序 + fail-fast 清单)、停机期(优雅停机序列)、运行期(测试与演练基线)。
6.2 日志体系
启动序列字典
第 2 章逐行解读过正常序列,这里补上异常分支——每行日志"没出现"或"变了样"意味着什么:
| 观察 | 诊断 | 动作 |
|---|---|---|
停在 config load failed | xs.json 字段非法(见 6.3 对照) | 按错误信息里的字段名修配置 |
没有 engine started | 并发引擎初始化失败(极少) | 检查系统资源 / 线程数限制 |
没有 script loaded | TCC 编译失败——错误信息含编译器输出 | 修 C 代码;错误定位到 devfile 行号 |
[应用前缀] 行缺失 | ServiceInit 没执行或没打日志 | 确认 devfile 正确、Init 已导出 |
停在 bind failed | 端口占用 / 权限不足 | netstat 查占用;多监听时已成功部分会回滚 |
反复出现 gen N 递增 | 有热重载在发生(或反复重启) | 确认是谁在提交 reload |
停机序列
Ctrl+C / SIGTERM 后的五阶段(测试基线对最后两行有断言):
[xs] stopping (停止接入并退役脚本 → 等待网络终态 → 退役 custom → 排空析构队列) [xs] engine stopped [xs] bye
应用日志从哪来
带 [demo] 之类前缀的行是你自己的 printf——XServer 的脚本日志就是标准输出。想结构化,用 xrt 的 logger 族(76 个 API:分级、格式化、多输出),见章尾延伸阅读。
6.3 fail-fast 错误清单
xs.json 的校验用例有 21 个(功能测试 func_test 的 A 矩阵),全部断言"exit=1 且错误信息含具体字段"。高频摘录:
| 错误信息(节选) | 原因 | 修复 |
|---|---|---|
field 'services' expect array | 根级 services 不是数组 | 检查 JSON 结构与拼写 |
| duplicate name ... | server 名 / host 名重复 | name 全局唯一、host 名 server 内唯一 |
| missing port | 非 custom 服务没给 port | 补 port(1–65535) |
| tls on udp | udp 服务配了 tls | 去掉——UDP 无 TLS 概念 |
| recv limit out of range | udp recv_limit > 65535 等 | 降到范围 内 |
| static headers 保留头 | static.headers 里放了 Transfer-Encoding 等保留头 | 移除 8 种保留头之一 |
| error_pages 键越界 | 状态码不在 400–599 | 修正键值 |
| 静态根缺失 | host path 指向的目录不存在 | 建目录或改 path |
certificate uses RSA but private key uses EC | 证书与私钥算法族不匹配 | 换配套的 cert/key(诊断给出双向命名) |
| expects exactly one supported PEM block | 私钥文件里有多个 / 残缺 PEM 块 | 文件只留一个完整支持的块 |
设计立场:错误宁可拒绝启动也不静默降级——你不会遇到"配了 TLS 但实际跑的是明文"这类最危险的静默失败。
6.4 停机与演练
"是不是只有我的环境坏?"——仓库自带的测试基线就是回答这个问题的对照组:
| 基线 | 内容 | 用法 |
|---|---|---|
test.bat / test.sh | 构建 → 冒烟(HTTP/WS/TCP/UDP 回显 + 11 条日志断言)→ 功能矩阵 → 热重载矩阵,一条命令 | 怀疑环境问题先跑它 |
func_test | 40 个断言场景:21 配置 fail-fast、路径逃逸八向量、reload 风暴 | 定位协议边界行为 |
reload_matrix | 四协议 × TLS 共 7 端点的换代 handoff 验证 | 热重载行为对照 |
drill | 6 小时长时演练:slowloris、pipeline 炸弹、坏脚本重载炸弹 + RSS 采样 | 内存 / 稳定性疑虑 |
routebench | 与 Go / PHP / FastAPI 同机时延对比 | 性能回归判断 |
内存级排错(崩溃、越界、泄漏)使用 xrt 的内存调试体系:前后 canary、UAF 隔离队列、分配事件环形日志、TEXT/JSON 报告——在宿主构建配置中开启,具体见章尾延伸阅读的 xrt 教程第 6 章。
6.5 契约
- 确定性线:同样的输入永远产生同样的日志序列——任何"玄学"都值得怀疑配置或环境。
- fail-fast 线:启动期错误全部点名字段并退出;运行期的持久错误以连接级状态码 / 断连呈现,不会拖垮进程。
- 排错顺序线:先日志定位阶段 → 再 fail-fast 清单对照 → 最后测试基线隔离环境因素。
常见错误
- 症状
- 改了 port / workers / 新增 host,行为不变。
- 原因
- xs.json 的多数字段在启动时装配;运行期可热更新的只有脚本与证书(经 reload 提交)。配置结构变更需重启。
/* 改 xs.json 后干等 */
vim xs.json # 端口没变?/* 脚本/证书:reload 提交(卷四)
* 结构变更:重启进程 */- 症状
- 脚本 include 到了正确路径下不存在的旧头文件,编译行为诡异(用了错误的声明)。
- 原因
- 相对 dev_inc 路径回退搜索时命中了当前工作目录下的同名文件——测试套件里有专门的"诱饵头"用例看护这个边界。
#include <config.h>
/* 到底命中了 includes/config.h
* 还是 CWD 下别的 config.h? *//* 专有名前缀,避免通用名 */
#include <myapp_config.h>- 症状
- Ctrl+C 后进程迟迟不消失。
- 原因
- 优雅停机在等存量连接自然结束(无超时强拆)。长连接(WebSocket / 推送)会让等待很久。
/* "死锁了!" → 直接 kill -9
* 丢掉排错现场与优雅语义 *//* 先看日志走到哪一阶段;
* 应用层提供 /shutdown 让连接收尾;
* 服务管理器提供外部超时(第 42 章)*/编程练习
故意制造 6.3 清单里的三种配置错误(如 dup name、tls on udp、error_pages 越界),记录错误信息,然后修复。
把一份正常启动日志打乱顺序,交给同伴按装配阶段重排;再删掉其中一行,让对方说出"缺了哪步、下一步会卡在哪"。
在 modules 层写一个日志模块:每请求输出方法、路径、状态码、耗时(提示:xrt 高精度时钟;在第 3 章的契约线内确定它能拿到什么)。对照 drill 的金丝雀思路验证它不影响功能。
小结
| 要点 | 结论 |
|---|---|
| 排错三武器 | 确定性日志(6.2)、fail-fast 清单(6.3)、测试基线(6.4) |
| 启动字典 | 每行日志的缺失都有明确诊断分支 |
| fail-fast | 21 个配置用例点名字段退出,绝不带病运行 |
| 停机 | 五阶段无强拆;外部超时由服务管理器兜底 |
| 内存排错 | xrt 内存调试体系(canary / UAF / 报告),深链 xrt 教程 |
- xrt 教程 · 内存调试 — canary / UAF 隔离 / 故障注入 / 泄漏报告的完整用法
- xrt 参考 · logger — 结构化日志 API(替代裸 printf)