第 6 章 / 共 51 章
卷一 · 起步与心智模型(卷末)

调试与排错:日志、fail-fast 清单与演练

XServer 没有管理后台,排错靠三样东西:结构化的启动日志、点名到字段的 fail-fast 错误、以及可以自己跑的测试与演练基线。本章把它们整理成你的第一份排错手册。

本章导读

站位:卷一收官章。它不教你新语法,而是给你三件武器:读日志的字典(6.2)、配置报错的对照表(6.3)、验证"是不是只有我这里坏"的基线(6.4)。之后每一卷遇到问题都值得回到本章。内存级排错(崩溃 / 越界 / 泄漏)深链 xrt 教程。

6.1 引入

一个没有管理界面的服务器,排错能力必须长在别的地方。XServer 的选择是:把诊断信息做成确定性事件——同一份配置、同一个状态,日志永远按同样的顺序出现同样的行;配置错误永远在启动时点名具体字段退出,绝不"带病运行"。确定性意味着可对照:你拿到的每一行异常输出,都能在源码的测试用例里找到孪生。

本章的三节分别覆盖三个故障域:启动期(日志顺序 + fail-fast 清单)、停机期(优雅停机序列)、运行期(测试与演练基线)。

6.2 日志体系

启动序列字典

第 2 章逐行解读过正常序列,这里补上异常分支——每行日志"没出现"或"变了样"意味着什么:

观察诊断动作
停在 config load failedxs.json 字段非法(见 6.3 对照)按错误信息里的字段名修配置
没有 engine started并发引擎初始化失败(极少)检查系统资源 / 线程数限制
没有 script loadedTCC 编译失败——错误信息含编译器输出修 C 代码;错误定位到 devfile 行号
[应用前缀] 行缺失ServiceInit 没执行或没打日志确认 devfile 正确、Init 已导出
停在 bind failed端口占用 / 权限不足netstat 查占用;多监听时已成功部分会回滚
反复出现 gen N 递增有热重载在发生(或反复重启)确认是谁在提交 reload

停机序列

Ctrl+C / SIGTERM 后的五阶段(测试基线对最后两行有断言):

优雅停机 · 日志形态
[xs] stopping
(停止接入并退役脚本 → 等待网络终态 → 退役 custom → 排空析构队列)
[xs] engine stopped
[xs] bye
停机没有超时强拆:长连接不关闭,进程会一直等待。这是"正确性优先"的有意取舍——生产环境用服务管理器(如 xs.service / systemd)提供外部超时兜底(第 42 章)。

应用日志从哪来

带 [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 udpudp 服务配了 tls去掉——UDP 无 TLS 概念
recv limit out of rangeudp 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_test40 个断言场景:21 配置 fail-fast、路径逃逸八向量、reload 风暴定位协议边界行为
reload_matrix四协议 × TLS 共 7 端点的换代 handoff 验证热重载行为对照
drill6 小时长时演练: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 提交(卷四)
 * 结构变更:重启进程 */
dev_inc 里的"幽灵头文件"
症状
脚本 include 到了正确路径下不存在的旧头文件,编译行为诡异(用了错误的声明)。
原因
相对 dev_inc 路径回退搜索时命中了当前工作目录下的同名文件——测试套件里有专门的"诱饵头"用例看护这个边界。
✗ 危险
#include <config.h>
/* 到底命中了 includes/config.h
 * 还是 CWD 下别的 config.h? */
✓ 正确
/* 专有名前缀,避免通用名 */
#include <myapp_config.h>
把 Ctrl+C 后"没退"当死锁
症状
Ctrl+C 后进程迟迟不消失。
原因
优雅停机在等存量连接自然结束(无超时强拆)。长连接(WebSocket / 推送)会让等待很久。
✗ 误判
/* "死锁了!" → 直接 kill -9
 * 丢掉排错现场与优雅语义 */
✓ 正确
/* 先看日志走到哪一阶段;
 * 应用层提供 /shutdown 让连接收尾;
 * 服务管理器提供外部超时(第 42 章)*/

编程练习

基础制造并修复三个错误

故意制造 6.3 清单里的三种配置错误(如 dup name、tls on udp、error_pages 越界),记录错误信息,然后修复。

验收:三条错误信息都能在 6.3 表里找到对应行,修复后启动成功。
进阶日志断句

把一份正常启动日志打乱顺序,交给同伴按装配阶段重排;再删掉其中一行,让对方说出"缺了哪步、下一步会卡在哪"。

验收:能用 6.2 的字典表解释每一行的位置与缺失后果。
挑战给自己的应用加请求日志

在 modules 层写一个日志模块:每请求输出方法、路径、状态码、耗时(提示:xrt 高精度时钟;在第 3 章的契约线内确定它能拿到什么)。对照 drill 的金丝雀思路验证它不影响功能。

验收:日志能区分两个并发请求的完成顺序;压测下无功能回归。

小结

要点结论
排错三武器确定性日志(6.2)、fail-fast 清单(6.3)、测试基线(6.4)
启动字典每行日志的缺失都有明确诊断分支
fail-fast21 个配置用例点名字段退出,绝不带病运行
停机五阶段无强拆;外部超时由服务管理器兜底
内存排错xrt 内存调试体系(canary / UAF / 报告),深链 xrt 教程
xrt 延伸阅读