05.12 并发服务复盘:从慢设备到退出失败的因果链
返回第五卷 · 队列前置:05.03 · GC 前置:05.07 · 组合前置:05.08 · 验证前置:05.09 · 诊断前置:05.10
05.12 并发服务复盘:从慢设备到退出失败的因果链
DeepTutor 初稿经技术与教学审阅后的静态课程。这是一场完全虚构的 IM 教学故障:没有运行 Go、IM 服务、负载、故障注入或站点,也不声称 OpenIM 曾发生这些事件。时间线、P95、队列和内存账都带纸上假设;学习者将来需要用真实测试和观测另行验证。
一、先定影响与确认点,再画技术时间线
未来的虚构群聊 c-g 面向很多目标设备。假设从 10 个目标扩到 10000 个目标后,教学群处理 P95 从 80 ms 升到 300 ms,同时有少量设备读取很慢。这个跨规模变化只是症状,不能立即把 GC、锁或网络宣布为根因。对用户先区分:发送请求是否被服务受理、每个目标是否发生在线写入尝试、对端应用是否确认、用户是否展示/已读;本章只画进程内尝试和它的资源边界,不修改 S2 accepted_in_memory 或 S3 提议的数据库提交合同。
为了能手算,先缩小到 A–F 六个目标:2 个 worker、容量 3 的等待队列。05.08 已求出接纳上限为 2+3=5,第六个 F 在 t0 仍由生产者持有。再把这个机制迁移到未来 10000 目标:必须按需枚举、有界队列与明确过载结果,不能一次创建 10000 个等待 goroutine 后再说“worker 只有两个”。
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
| 症状/影响、A–F 时间线、候选机制 | 取消/退出、内存账、修复不变量、验证/交接 | 一页虚构复盘、证据缺口表、下一阶段工作清单 |
本卷是 S4 原理与性能学习的收束。05.01–05.11 的概念应在此被用于解释一件事为什么坏、怎样知道修好;S5 的持久重试、离线补拉和多端同步仍是后续课程,不由一份本地队列复盘宣称完成。
二、t0–t4:两名 worker 如何被慢目标占住
下表选择一种可发生但未实际运行的交错。B、D 是慢目标;A、C 可以完成。结果通道 results 由 worker 向协调者发送,只有全部 worker 结束后协调者才有资格关闭。
| 时刻 | worker 1 | worker 2 | 等待队列/生产者 | 汇聚与退出状态 |
|---|---|---|---|---|
| t0 | A 处理中 | B 处理中 | [C,D,E] 满;F 尚未接纳 | 尚无完整六目标结果 |
| t1 | A 完成后取 C | B 因慢设备等待 | C 出队腾槽,F 入队;队列 [D,E,F] | A 可记录完成;B 仍未知 |
| t2 | C 完成后取 D,D 也因慢设备等待 | B 仍等 | 队列 [E,F];两 worker 都无空位 | A/C 已知,B/D 未知,E/F 尚未处理 |
| t3 | 若 D 不响应取消则继续等 | 若 B 不响应取消则继续等 | 停止新接纳;E/F 去向必须记账 | cancel() 已发出,不等于 B/D 已退出 |
| t4 失败变式 | D/B 后来仍可能尝试发送结果 | 同左 | 若协调者提前 close(results),晚到结果会触发发送已关闭通道问题 | 或若一直等不退出,会超过有限关闭期限 |
这个链条解释“一个慢目标为什么可能拖住别的目标”:两个 worker 都停在 B/D,E/F 虽已排队却无人处理;fan-in 若无限等六个终局,又会把请求拖长。若真实实现有更多 worker、非阻塞写、单目标期限或隔离队列,结果会不同,所以不能从“B 很慢”直接推断所有群都受影响。
t4 是错误设计的反例,不是建议“超时就 close(results)”。正确关闭要先停止发送者,等 B/D 或其可取消替代路径有可记录的终局,再由唯一协调者关闭结果通道。若某外部调用无法及时取消,关闭期限到达只能记录未退出与未知副作用,不能通过提前关通道假装完成。
三、热点与级联等待:把竞争解释写成证据问题
至少有两种不同候选:
- 慢接收方占满共享 worker。
B/D输出阻塞,队列 E/F 等待,block profile 或运行时 trace 应显示 worker 在对应发送/等待路径;每设备输出队列、TCP 流控与超时数据应和慢目标同窗变化。若 worker 在 B/D 上并没有停留,这个候选被削弱。 - 共享会话锁保护范围过长。 worker 其实在等待群成员表的锁或锁内编码,mutex profile 应指向造成争用的临界区,block profile 可见等待点;缩短锁内工作可能改善。若锁等待不显著,却有设备写入等待,优先回到第一个候选。
两者可能同时存在,也可能与 GC/数据库等待叠加。05.10 已说明 CPU profile 看 on-CPU 热点,mutex/block 看不同等待关系,execution trace 看时间线,跨进程请求 Trace 才能对照数据库与设备阶段。P95 300 ms、RSS 变大或 NumGoroutine 增长,都不是单独的根因证明。 本复盘的“观察”全为纸上输入,实际证据栏应由学习者以后填真实版本、窗口、采样方法与限制。
| 候选 | 本章纸上支持 | 缺失的证据 | 应能反驳它的观察 |
|---|---|---|---|
| 慢目标占 worker | t2 B/D 均等待,E/F 排队 | 实际等待栈、持续时间、单目标队列 | B/D 很快返回而锁/CPU 占主时延 |
| 会话锁热点 | 扇出前可能共用成员表 | mutex 归因、锁内工作与同窗请求 | 锁等待低而写目标等待高 |
| GC/内存压力 | 队列与 G 可能保留对象 | alloc_space/inuse_space、GC 与 RSS 对照 | 存活/分配稳而等待主要在连接 |
四、积压与内存:队列有上限,数据账仍要算
把群扩到 10000 个目标,若纸上 5% 慢设备,即 500 个;每个慢设备的用户态输出队列容量为 2,反复消息使这些队列都满时,最多有 500×2=1000 个该子集的排队槽位。若每槽独立保存 1 KiB 数据,其纯数据约 1000 KiB≈0.98 MiB。这只是该慢设备子集的上限模型,不含连接缓冲、队列节点、G 栈、TLS、别的目标和内核 socket 资源;若共享不可变载荷,独立数据量又会不同。
容量 2 的意义是能说明什么时候拒绝、背压或断开,不是保证 RSS 恰好 0.98 MiB。05.06 已解释小视图可能牵连大底层数组;05.07 已解释只要队列和阻塞 G 仍引用对象,GC 不能回收。若队列持续满,生产者也可能停在等待接纳的位置,导致上游任务、结果通道与请求 Context 的引用继续存活。一个“低 CPU、高 RSS”现象可由等待与保留产生,但要靠同窗数据确认,而不是只调低 GOGC。
反过来,若所有队列都已排空、HeapAlloc 下降而 RSS 暂不降,03.06 的分配器复用/归还机制仍需纳入解释。对象可达、堆指标、进程 RSS是三层数据,复盘必须写单位和统计范围。
五、取消与退出:要证明停止,而不只是发出信号
按 05.09 的 Open→Stopping→Closed 协议重画修复:
- Open:接纳有上限;目标任务与结果记录由明确所有者管理。
- 进入 Stopping:关闭新接纳入口,通知取消;阻塞在输入、输出或下游 I/O 的 worker 都需有期限或可取消路径。
CancelFunc不等待工作结束。 - 等待可观察的退出:逐个收 B/D 等 worker 的
done或记录其超过有限期限,已写入的字节与未知结果不能因取消被抹掉。 - Closed:仅在全部会向
results发送的 worker 已结束后,由唯一协调者关闭results;连接和其他资源按所有权释放或报告未完成。
若 select 中发送和取消同时就绪,Go 可选任一分支;若要求“停止确认后绝不接纳新项”,必须让接纳与停止由一个所有者串行决定,或定义锁内状态转换并等确认,不能只写 select{send,ctx.Done} 就宣称严格关闭。关闭共享 channel 太早会 panic,永远不关闭又可能让汇聚器挂住;两者都要用可控制的纸上交错与未来测试覆盖。
即便所有 goroutine 退出,本章仍不能回答“进程在 t3 前已向设备写了多少”或“断线后如何重试”。这些是未知副作用与可靠交付问题,交给 S5 的持久记录、幂等和补拉课程。
六、改进后的处理链:上限、结果与顺序各有主人
| 机制 | 改进的不变量 | 仍未获得的保证 |
|---|---|---|
| 按需枚举 10000 个目标 | 不先为所有目标物化完整任务和 G | 成员快照/权限在并发变化下的业务语义 |
| 有限 worker + 有界总队列 | 活跃与已接纳等待量均可计数 | 生产者与每设备输出队列自动有界 |
| 每设备容量/期限 | 慢目标不能无限占用内存与 worker | 设备应用已收到或可离线补拉 |
| fan-in 带目标 ID 和结果状态 | 已知成功、明确失败、未知与未接纳可区分 | 超时副作用可凭空撤回 |
| 按会话 key 本地串行 | 本进程的已接纳 c-g 工作有定义的局部顺序 | 跨节点 seq、数据库提交与重试顺序 |
05.08 的“2 worker + 3 槽”只为六目标手算,不是适用于 10000 目标生产流量的容量建议。真实容量取决于到达速率、处理时间、每项字节、慢设备比例、下游能力与容器限制。若错误或拒绝增加,回退或限流也必须遵守既定 IM 合同。05.11 讨论过的少复制/池化/批量可作为候选,但先核对 profile 与业务结果,再逐项试,不应把复盘直接写成“改成无锁必然恢复”。
七、验证、回退和交接:缺一项都不叫闭环
学习者的验证方案应先构造确定的纸上负例:B/D 长时间不响应、E/F 排队、上游在取消后仍尝试发送、结果通道被提前关、生产者迟迟不报 done。门闩控制时序,不靠 Sleep;race detector 只覆盖实际执行路径,业务结果和资源退出分别断言。随后在学习者自己的隔离环境,才有资格用同样 10000 目标负载记录 P95/P99、错误、队列、活跃 G、RSS、heap alloc_space/inuse_space、block/mutex 与 trace;这一步本课程没有运行。
| 验收门 | 所需证据 | 回退/停止条件的类型 |
|---|---|---|
| 正确性 | 权限、序号、每目标状态、无重复/串改 | 任一业务不变量失败 |
| 停止与资源 | 取消后每个 worker 的 done、通道唯一关闭、有限期限 | worker 不退出或状态无法对账 |
| 性能与容量 | 同 10000 目标负载的 P95/P99、队列、CPU、heap、RSS | 尾延迟或资源超过事先定义的预算 |
| 交付边界 | 请求受理、在线尝试与设备确认分层 | 未知结果被报告成成功 |
交接文档还要列出未解决项:跨节点投递任务的持久化、重复提交、离线设备补拉、多端顺序、进程崩溃后对账。它们不能靠本章的进程内队列、Context 和 GC 自动完成;S5 再建立相应前置与协议。复盘中的“修复”是设计草案,未经实施/运行,不得写成已消除真实事故。
八、交付虚构事故复盘与分层练习
交付一页报告:症状与影响范围、t0–t4 状态、候选机制及反例、纸上内存账、修复不变量、未来验证矩阵和 S5 遗留清单。每个数字标单位,每个结论标“纸上假设/待测”,不把 OpenIM 或当前教学服务写成已经发生了这个事故。
分层练习:先答,再展开反馈
1. t0 哪两个目标在 worker 中?
A、B;C/D/E 排队,F 尚未接纳。
2. t1 A 完成后谁开始处理?
C;F 趁 C 出队后的空位进入等待队列。
3. t2 哪两个慢目标占住 worker?
B、D;E/F 仍等 worker。
4. B 慢就足以证明所有群一定变慢吗?
不能。还要看共享 worker、队列、期限和结果汇聚方式。
5. t2 fan-in 可把 E/F 标为成功吗?
不能。它们尚未由 worker 处理。
6. t3 `cancel()` 返回就证明 B/D 已退出吗?
不能。取消发意图,不等待退出。
7. worker 仍可能发结果时提前关闭 results 会怎样?
晚到发送可能触发向已关闭通道发送的 panic。
8. 一直等 B/D 不退出是可接受的关闭策略吗?
不应无限等;要有有限期限、退出证据和未完成状态记录。
9. slow=5%×10000 是多少设备?
500 个,仍是虚构比例。
10. 500 个慢设备各 cap2,最多多少该子集槽位?
1000 个,前提是重复消息足以填满。
11. 每槽独立 1 KiB,纯数据约多少 MiB?
1000 KiB≈0.98 MiB,不等于 RSS 或全部业务内存。
12. 队列还引用对象,强制 GC 能清空吗?
不能。仍可达的工作由业务所有者决定何时移除。
13. CPU 不高、RSS 高可直接断言 goroutine 泄漏吗?
不能。还需 G 退出/阻塞栈、heap 对象与系统 RSS 的同窗证据。
14. block profile 与 mutex profile 各偏向哪种问题?
前者看同步等待点,后者看造成锁争用的持锁位置。
15. 当前 t0–t4 表是运行 trace 吗?
不是,它是带假设的纸上时间线。
16. `select` 的取消分支就绪就必定优先吗?
不必定;若发送也就绪,Go 可选任一分支。
17. 谁负责最终关闭共享结果通道?
唯一协调者,且需先确认所有可能发送结果的 worker 已结束。
18. 两个 worker 加三个队列槽能直接推荐给 10000 人群吗?
不能。它是六目标纸上模型,容量需真实负载与资源预算验证。
19. fan-in 的结果为什么要带目标 ID?
完成顺序可变,必须把成功/失败/未知准确映回目标。
20. 本地按 key 串行能保证跨节点消息序号吗?
不能。跨节点提交、重试与恢复另需协议。
21. P95 改善但错误率升高,可把设计标完成吗?
不能。正确性和失败合同是性能验收门。
22. 本卷复盘解决了离线补拉和多端同步吗?
没有。这些列入 S5 的持久可靠性与同步课程。
来源与下一步
- Go Pipeline 与取消和
context:下游退出、取消传播与 CancelFunc 的停止边界。 - Go GC 指南与内存模型:可达对象与并发读写的语言保证。
- Go 诊断指南、
runtime/pprof与runtime/trace:等待、锁、GC 与调度的未来运行证据。
按学习路线,S4 主线下一章06.04 页与缓冲池进入存储页,再用 06.05–06.06 深入有序索引和查询执行。离开本章前,应能把症状、局部机制、竞争假设、缺失证据、修复不变量、验证门和未解决的 S5 保证放进一份可交接的静态复盘。