Go 38:构建配置与诊断端点怎样交付任务服务
上篇追到了一个 worker 被唤醒的路径。能解释 goroutine 的状态,还不能把服务交给另一个人在干净目录启动:错误配置在哪一步拒绝?profile 会不会暴露到业务端口?收到了退出信号,谁等待任务 worker?本篇沿用累计任务服务的 handler、worker 和 SQL 恢复逻辑,增加实际可构建的 serve 命令。完整代码及实验包、SHA-256、解包复跑方法保存在同名素材目录。 先把启动失败留在监听之前 命令形如 taskstats serve --listen 127.0.0.1:0 --admin 127.0.0.1:0 --workers 1 --queue-capacity 2 --max-words 3 --sql-dsn '...'。不传 DSN 时仍可使用内存服务;传入 DSN 则沿用 28 的恢复构造函数。serve 只在原 CLI 的入口增加一个分支,没有复制统计、领取和任务状态转移的代码。既有 --batch 与 --text 继续通过回归。 runTaskServer 在打开 listener 之前拒绝非 loopback 的 --lis...
Go 37:一个阻塞接收从源码到任务终态
上篇按任务 ID 对齐了等待的事件与栈。仅凭 [chan receive] 四个字还不能回答:什么操作让接收方返回、返回时 ok 是多少、服务何时才可以宣布 worker 退出?这里选累计任务服务里一个阻塞接收,把公开语义、冻结版源码和正反两次受控实验接在一起。完整 源码证据包、SHA-256与独立复跑步骤附在同名素材目录。 公开契约先于调度实现 Go 1.27.1 随附语言规范的 Receive_operator 指出,接收表达式在没有可取值时会阻塞;从已经关闭并且排空的 channel 接收会立即返回元素类型零值。Close 章节与 go doc builtin.close 还规定了二值接收:关闭且排空后 ok=false。未关闭时与发送者配对接收一个值,ok=true。返回值先按冻结规范判定,再由测试确认;goroutine 栈用于识别阻塞点。 新增加的 TestChannelWakeAndWorkerExit 将当前 HTTP handler 接收的 task[0] 停在分析器的无缓冲 <-release;先查状态仍为 RUNNING,再让 task[1] 进入唯一...
Go 36:trace 与阻塞 profile 怎样拆开等待时间
上篇用 CPU/heap 采样找函数和分配热点;任务卡在队列和锁上时,CPU profile 的低样本不表示请求没有延迟。本篇沿用同一任务服务制造两个可定位的等待点。原始 trace 和源码包、SHA-256、独立复跑步骤包含完整双流、退出码和可重新解析的二进制文件。 在两个任务 ID 上固定先后 TestTaskWaitProfiles 的执行器只有一个 worker、一个排队槽。task[0] 的分析器在 channel 闸门等待,主测试确认它进入 RUNNING 后提交 task[1];第二条被接受但只能排队。第三次提交触发 503,并断言 task[2] 根本不存在。随后主测试短暂持有当前 TaskStore.mu,另一 goroutine 发出 GET task[0],真实栈显示该 GET 卡在 TaskStore.Lookup 的 sync.Mutex.Lock。它不是“看见 GET 被调用就推测会阻塞”:测试必须看见该锁等待栈才解锁,超时则失败。 释放 channel 后调用 service.Close() 等 worker 退出,再检查两条记录均为 SUCCEED...
Go 35:profile 如何定位热点而不替代测量
上篇看到等待中的 worker 并不等于正在消耗 CPU。要找实际计算热点,先选负载,再采样;profile 给出下一步该改哪里的线索,改动是否有收益则要回到多轮基准与业务断言。本篇仍使用同一任务服务的 AnalyzeContext 和已完成任务 HTTP GET。工程包、SHA-256及独立复跑步骤带完整原始 profile。 一组 profile 必须注明两种负载 新增的 BenchmarkTaskAnalyzeContext 在计时循环外固定 strings.Repeat("Go one two 你好 ", 128),每轮实际运行当前 worker 使用的 stats.AnalyzeContext,校对词数、字节和 rune 数以及错误返回;原有 BenchmarkTaskHTTPRead 则查已完成任务,检查 200/终态/词数。两者都在 profile 采样的进程里跑,但不是同一个业务路径。未开 profile 的五轮基线、原始 -cpuprofile 和 -memprofile 文件,以及 go tool pprof -top 原文分别存于 wri...
Go 34:goroutine 等待时调度器看到了什么
上篇分别记录了分配、回收和服务等待完成的时间。这些数字还不能说明等待发生在 worker 的哪一处:正在消耗 CPU、卡在 channel 接收,还是等待网络可读?本篇继续同一个任务服务,把现有 worker 的分析函数切换到三种受控入口,保留 完整源码与证据包、SHA-256 和 独立复跑步骤。 RUNNING 不等于正在占用 CPU TestWorkerWaitKinds 仍使用真实的 POST /tasks handler、当前任务存储和一个 worker。先发送空文本并确认 400、没有占用任务 ID;再提交 Go one,收到 202 才等待 worker 进入分析函数。此时任务记录均为 RUNNING,但这只是业务状态,无法告诉调度器此时的 G 在做什么。 三种分析函数只改变阻塞点。CPU 场景在等待释放的循环里给原子计数器加一;channel 场景在 release channel 上接收;network 场景连接本机 TCP,执行实际 net.Conn.Read,对端在测试放行前不写字节。对每种场景,主测试在 worker 报告到达检查点之后读取任务记录,并通过 ...
Go 33:GC 预算怎样影响任务服务的堆与延迟
上篇用编译信息和基准报告解释了一次分配变化。可任务服务在处理多个请求时,分配了多少、最后留下多少,以及用了多久,不能靠一个函数的 B/op 推断。本篇在同一个任务服务增加固定 HTTP handler 负载,保留 完整工程包、SHA-256和独立复跑步骤。 分配的字节不是存活堆 TestTaskGCBudget 用两个 worker、容量 1,025 的队列,提交 1,024 个相同请求。每个请求 JSON 体是 1,803 字节,文本里有 512 个单词;HTTP 适配层必须返回 202。这里使用 httptest.NewRequest 和当前服务的原 handler,没有走 TCP。收齐应答之后调用 service.Close(),等待服务拥有的 worker 退出,再逐个按 ID 核对 1,024 条记录均为 SUCCEEDED、Words=512。如果漏了任务,只有速度的数字就不能拿来讨论 GC。202 是入队应答,逐 ID 的终态检查才是这次业务完成的证据。 负载开始前后,测试从 runtime/metrics 读累计分配字节和自动 GC 轮次;分配字节除以从开始提交到...
Go 32:逃逸诊断为什么不是分配账本
上篇把造输入排除在计时循环外,记录了 CountWords 从 strings.Fields 换成逐 rune 扫描前后的分配和时间。累计工程没有换服务:这里仍比较同一个统计函数,并且把编译器对这个函数的判断和实际 benchmark 并排看。工程与证据包、SHA-256、独立复跑步骤可重跑。 同一个函数,两个版本的编译诊断 旧版来自固定提交 351b838365057d66f7a845801469bc267ddb4055 的 examples/go-language/stats/count.go,函数只有一行:return len(strings.Fields(text))。在新临时目录解出该提交的整个模块,和当前模块分别运行 go build -gcflags='example.com/go-language-task/stats=-m=2' ./stats。指定包匹配避免把依赖的诊断淹没在输出里。标准输出都为空,编译器将优化信息写到标准错误;两个命令都退出 0。冻结 Go 1.27.1 的 go doc cmd/compile 明确 -m 打印优化决策,这不是语言规范要求的...
Go 31:benchmark 为何不能只测准备工作
上篇用有界 fuzz 寻找任务接收与状态序列的反例。这一次要回答另一个容易混淆的问题:数字下降了,究竟是计数函数更快,还是测量时少做了一部分工作?沿用同一个任务服务;累计工程包、SHA-256及独立复跑步骤保留完整源码与原始命令。 准备一次任务,不等于执行一次任务 原先的 CountWords 用 len(strings.Fields(text)) 计算词数。如果基准把 strings.Repeat 生成输入也放进计时循环,数字同时包含造字符串、分配和扫描。服务的 HTTP 查询又是另一条路径:已完成任务的 GET 会读取存储、编码 JSON,不会调用 CountWords。把这两种耗时混在一个基准里,优化统计函数后看到的请求耗时,既不能归因,也不能检验 HTTP 是否受益。 冻结版 Go 1.27.1 的 testing.B.Loop 会在第一次调用时重置计时器,循环结束停止计时。因此两个基准都在循环外造输入和服务实例,在循环内执行要测的操作及最小业务断言: 12345678text := strings.Repeat("Go one two 你好 ", 6...
Go 30:fuzz 能发现哪些输入假设
上篇用两份固定输入分别验收函数、HTTP 和真实数据库。固定输入清楚,但没遍历异常 JSON 或状态操作的交错次序。本篇沿用同一个任务服务写两个 Go fuzz 目标,检验有限的输入不变量;累计工程包、SHA-256和独立复跑步骤附原始日志。 HTTP 接收之后必须有一个可解释的结果 FuzzHTTPTaskSubmission 的种子包括合法的 {"text":"Go"}、UTF-8 文本、空白、text 类型错误、未知字段、两个相邻 JSON 对象和超过 64 KiB 的 JSON。每一轮拿 httptest.ResponseRecorder 调原有 POST /tasks handler,而非复制一套解析器。服务关闭要等待自己的 worker;若响应为 202,测试要求分配一个任务 ID,并在退出后读到 SUCCEEDED 或 FAILED。若返回 400/413,必须没有分配 ID 或留下记录。这使“报错时仍有后台任务”成为会导致 fuzz 失败的反例。 种子的坏输入也有明确预期:空白文本和类型不符不会当成已接受,拼接两个 JSON...
Go 29:单元、HTTP 与数据库测试分别证明什么
上篇用一个真实子进程证明了异常退出后 RUNNING 与 QUEUED 的不同恢复动作。但恢复测试通过,不代表文本计数、HTTP 应答和 SQL 落库都在同一个输入上成立。本篇不另造示例程序,继续检验累计任务服务;完整工程与原始证据、摘要、独立复跑说明放在同名素材目录。 同一条任务走三条证据链 固定输入 "Go one two" 的期望是 Words=3, Bytes=10, Runes=10;"你好 Go" 为 Words=2, Bytes=9, Runes=5。表驱动用 testing.T.Run 分别运行 unit、http、postgresql 三个子测试。后者两个汉字占 6 个 UTF-8 字节,字节数不应按 rune 数填写。stats.Analyze 与状态转移的单元子测试直接操作 TaskStore:Add→Start→Complete,读取终态和值;没有建立 HTTP 连接,也没有打开 SQL 池。若这里失败,先查计数口径与顺序状态逻辑,不必把错误归咎于网络。 HTTP 子测试使用 httptest.NewServer 的...










