第1章 服务工程首先是一组运行契约
从返回值走向生命周期
一个处理函数在正常输入下返回正确结果,只完成了接口的一部分。服务还要回答:请求最长占用多久,等待任务最多排多少,客户端断开后谁继续工作,退出时谁停止接收,失败会释放哪些资源。接口文档通常只描述状态码和字段,运行契约则把时间、数量与责任也纳入可验证范围。
随书工程提供 /work、/tasks、/stream 和 /stats。/work 的工作属于当前请求,客户端取消后继续执行没有意义。/tasks 返回接纳结果后,任务归工作池管理,它不能继续借用请求的取消信号,也不能借用即将回收的请求对象。/stream 把输出端的速度纳入资源预算。/stats 只统计工作池接纳的后台任务,包括 active、queued 与 completed;它不统计 /work 和 /stream 的 HTTP handler。后两者通过日志和测试中的 handler 退出通知观察。
这四个端点服务于同一个问题:HTTP 连接、业务工作和后台执行的生命周期并不相同。请求结束不一定代表任务完成,任务完成也不一定代表连接已经释放。将这些事件合并成一个“成功”计数,会让关闭与重试的判断失去依据。
写出真正可以失败的承诺
先为工程写下四条承诺。工作池只有固定数量的 worker;等待区只有固定容量;接纳关闭后不再成功提交;所有已启动工作最终会完成或响应服务取消。这里的“最终”必须有测试截止时间,不能写成无限等待。超时失败应保留诊断信息,而不是挂住整个测试进程。
这些承诺是有条件的。Go 无法强行终止一个任意函数,如果函数进入不检查取消、也没有网络截止时间的阻塞操作,工作池不能兑现及时退出。实现因此要求工作函数协作响应 context,外呼器把同一个 context 传入请求,流式输出设置写截止时间。工程保证必须建立在其依赖接口也遵守边界的前提上。
202 Accepted 在这里表示任务进入了本进程的内存管理范围。它不表示任务持久化,不保证机器掉电后恢复,也没有跨进程去重能力。把“进程内已接纳”写成“可靠消息投递”,会让读者误用教学工程。后续章节会讨论如何把内存任务升级为数据库状态机,但这不是本书实验的既有能力。
数量关系比一次快照更有用
假设 worker 数为 W,队列容量为 Q。在没有额外隐藏缓冲区的实现中,正在执行的任务最多 W 个,排队任务最多 Q 个。已接纳但未终结的任务数由二者共同约束。len(channel) 只能观察某一瞬间,不是接纳许可;先查看长度再发送会出现检查与动作之间的竞争。
测试通过人为阻塞工作函数建立稳定状态:先占满 worker,再装满队列,最后提交额外任务。这样可以明确证明拒绝来自容量边界,而不是机器恰好很慢。解除阻塞后,再验证所有已接纳任务进入终结状态。这种安排比发出一千个请求然后看平均延迟,更容易解释。
监控中的不同计数也要有清晰语义。accepted 在接纳发生时增加,started 在 worker 获得任务时增加,completed 表示所有终结,failed 是其中的错误终结,cancelled 又是可识别取消错误的子集。它们不能直接相加;关闭后可以检查 accepted 与 completed 是否相等。运行过程中取多个原子计数并不一定得到同一时刻的全局快照。
建立最小复现习惯
先在实验包中运行:
go version
go test -race ./...
go run ./cmd/service第一条命令是环境证据;第二条验证当前执行路径的数据竞争与行为约束;第三条启动真实进程。不要把三个命令的结论互相替代。race 通过不说明退出协议正确,启动成功不说明过载时有限,单元测试通过也不说明公网代理正确处理流式响应。
调试时应保留请求输入、服务配置、观察窗口与最终状态。只有一句“偶尔卡住”很难复现;“两个 worker、一个等待槽、三个任务被闩锁阻塞,第四次提交立即拒绝”则提供了可以行动的边界。工程交流的质量往往取决于是否能把现象压缩成这样的条件。
练习:把接口说明变成验收表
为一个你熟悉的接口分别写出请求截止、最大输入、外呼截止、并发上限与退出责任。挑选其中最容易被违反的一条,用可控阻塞而不是随机延迟构造失败。验收结果必须包含拒绝是否及时、已接纳工作是否丢失,以及解除故障后服务是否恢复。记录哪些结论由测试证明,哪些仍然只是部署假设。
如果你的接口提交异步任务,再补充一句客户端可依赖的语义:响应是否意味着持久化、任务失败到哪里查询、响应丢失后如何重试。即便本轮不实现持久化,也要写清当前能力的范围。边界清楚的简单系统,比暗示了过强保证的复杂代码更容易可靠使用。
本章实测证据
固定worker和等待槽构成边界;测试先占满运行槽再装满队列,检查多余提交立即拒绝,释放后所有已接纳工作终结。 以下摘自最终本机 go test -race -v ./...,完整命令及环境见附录B;括号中的耗时仅是当次运行记录。
--- PASS: TestPoolQueueBound (0.00s)