Skip to content

检查点成本与历史增长 ​

本页记录 Pulse #411 的有限规模测量,帮助宿主估算检查点的单次保存成本、累计写出量和保留空间。它不定义产品吞吐目标或业务保留期,也没有引入压缩、增量存储或分布式所有权能力。存储保证见存储指南,记录字段见数据模型。

普通节点图从 1 到 10000 步的同任务对照、当前公开版本与真实进程恢复成本见规模与性能。本页保留原历史测量,不改写成新版本数据。

Pending 清空以后,留下了什么 ​

StreamRecord.Pending 保存仍有读取义务的消息。确认后消息从 Pending 移除,但对应的 MessageAttemptRecord 仍保存在 RunRecord.Messages,记录消息身份、尝试预算和确认状态。它本身不保存消息的编码内容。显式状态和普通输出也可能继续保留业务值;不能把「Pending 为零」理解成「记录已变空」。

新增的 TestRecordGrowthHistorySurvivesDrainAndResume 用真实 filestore 完成 8 条消息,检查 Pending 为零、连续确认前沿为 8,同时仍有 8 条不同序号的 confirmed 历史。然后新建 Store 和 Runtime,从文件 Resume,检查历史保持一致、出口字符串相同、两个节点的业务体都没有再次执行。这是同进程内重新打开文件的恢复检查;跨进程退出恢复由已有集成用例覆盖。

当前修改路径通常先克隆记录来构造候选;runState.save 还会克隆交给 Session 的记录,并在保存成功后克隆成自己的已确认记录。cloneRecord 遍历 map、复制切片和编码字节,字符串作为不可变值复用。filestore 的更新则先读取并解码完整旧 JSON 检查 Revision,再验证元数据、编码完整新记录、写临时文件、同步和原子替换。它只保留当前 JSON 文件,不把每次 Revision 都另存一份。

测量环境和方法 ​

测量日期为 2026-10-16,运行代码基线为 Durable c80d8e86e570608603380f4f15a96ad19055dab9,固定 Pulse 依赖为 v0.4.2-0.20261015113730-990607e5593a。环境为 Windows 11 10.0.26200、amd64、Go 1.25.0、Intel Core i9-14900HX,默认 GOMAXPROCS=32;记录位于本机 D 盘 NTFS。文件系统缓存没有被清空,也没有控制其他系统负载。

本页数据对应上述历史基线,不表示当前构建仍使用同一依赖版本。当前固定依赖见仓库 go.mod;依赖或实现升级后的成本应重新测量,不能直接改写这里的测量版本。

测量分成两组,夹具生成不计入组件 benchmark:

  • **组件矩阵:**先通过 MemoryStore 跑完真实的 8 条字符串消息,以产生记录模板;再构造 8、128、1024 条 confirmed 历史。drained 的 Pending 为零;backlog 在同样历史之外另加相同数量的 Pending。字符串长度单独取 32 或 4096 字节。模板还保留一个整数状态,以及最后一条字符串的普通成果。构造记录用于复制、编码和后端保存成本,没有用它们证明大规模 Runtime 恢复正确性。
  • 组件操作:Clone 通过仅在测试中编译的入口调用真实 cloneRecord;JSON 调用 json.Marshal;FileSave 调用真实 filestore Session.Save。FileSave 先在计时外保存 Revision 1,计时内连续更新 10 次,包含旧文件读取/解码、Unicode/元数据检查、新 JSON 编码、文件同步及替换;不含 Acquire、Close 或 Runtime 的克隆。JSON 编码已包含在 FileSave 中,不能再把两列时间相加当成一次保存成本。
  • **真实流水线:**filestore 中执行 source → HandleMessage sink,缓冲为 1,无失败或重试。每条消息更新累计字节状态,最后一条发布普通字符串成果。分别运行 8、128 条和两种字符串大小。成功 Save 后用 Stat 读取实际文件长度并累计,Stat 开销包含在这组流水线时间内,不包含在组件 FileSave 时间内。

组件每个样本固定 10 次操作、重复 3 个样本;流水线每个样本固定 1 次运行、重复 3 个样本。下表为三个样本的各项中位数;时间列是每个样本的平均操作时间再取中位数,不是线上延迟分位数。短测量中的 JSON 缓冲复用、GC、文件同步和系统调度都会造成波动。

真实流水线封顶 128 条;1024 条只测构造记录上的组件操作。最大的构造 JSON 约 6.12 MB,每个 FileSave 样本只做 10 次更新,避免为了证明增长而让大流水线不断重写整份历史。测试不以耗时阈值判定性能成败。

组件结果 ​

history 是 confirmed 消息尝试数;backlog 的 Pending 数也等于 history。payload B 是原始 Go 字符串长度,编码后还包括 JSON 引号;ValueRecord.Data 在外层 JSON 中编码为 base64。所有字节数均为十进制字节。

状态historypayload BJSON BClone µs/opJSON µs/opFileSave ms/op
drained83252462.159.613.41
drained84096160822.4612.5816.95
drained12832429626.5668.7118.52
drained1284096537948.9079.2017.07
drained10243232523347.42563.8223.00
drained1024409633606947.89576.9022.03
backlog83270743.039.535.19
backlog840966123810.8542.4517.69
backlog128327215816.0997.4018.04
backlog1284096776238136.90427.2721.18
backlog102432559741172.69824.9424.55
backlog102440966116561760.493548.6852.53

JSON B 为 Revision 1 的长度;FileSave 更新后实测文件为 Revision 11,因修订号多一位,长度比本列多 1 字节。每个 FileSave 子用例在计时结束后校验文件长度与该记录的 JSON 长度一致。

分配列的格式为 B/op / allocs/op,同样分别取三个样本的中位数。它表示一次操作累计分配的堆内存和次数,不是存活堆、峰值内存或磁盘占用。

状态historypayload BCloneJSONFileSave
drained8326624 / 3210548 / 3929272 / 258
drained8409616256 / 3226130 / 3967155 / 259
drained1283232184 / 15491975 / 280209712 / 1830
drained128409641816 / 15498354 / 280245724 / 1832
drained102432223584 / 1052656551 / 20731767019 / 13497
drained10244096233216 / 1052659623 / 20721756244 / 13496
backlog8328928 / 4913370 / 3939529 / 303
backlog8409657088 / 4993676 / 40225337 / 304
backlog1283269048 / 411129652 / 280348704 / 2481
backlog1284096695131 / 411808414 / 2783266685 / 2491
backlog102432510305 / 3101998968 / 20732992355 / 18627
backlog102440965451520 / 31017816263 / 207325778228 / 18635

无 Pending 时,1024 条历史约占 325–336 KB JSON;原始字符串从 32 增至 4096 字节,并没有让每条历史都携带一次大字符串。另有 1024 条大字符串 Pending 时,记录约 6.12 MB,一次克隆分配约 5.45 MB,FileSave 分配约 25.78 MB。这些结果支持把历史条数和未确认内容的大小分成两个维度,而不是只数消息。

真实流水线:最终文件和累计写出量 ​

以下每一行都执行了真实的 HandleMessage 与 filestore 保存,并检查普通成果。累计字节是成功保存后实际 JSON 文件长度之和;给出三次运行的范围,因为生产者与消费者的调度会稍微改变中间记录。它不包含读取量、锁文件、文件系统元数据或设备实际写入量。

消息数payload B秒/运行Save 次数/运行最终 JSON B累计写出 B/运行分配 B/运行分配次数/运行
8320.57130525889336–8955111741288259
840960.510301609419796719879608302
128326.679390429758542754–854296770409880557854
12840966.19239053807995149981472648557816

流水线 RunID 较组件夹具长,最终 Revision 也不同,因此最终 JSON 字节与组件表略有差异。128 条运行的最终记录约为 43–54 KB,但生成过程中已累计写出约 8.54–9.95 MB JSON。它们回答的是两个不同问题:当前留在磁盘上多少,以及这一轮反复保存一共写出了多少。消息确认历史逐渐增加,而每次写入包含当时整份记录;即使最终 Pending 清空,中间写入量也不会被撤销。

本组是单机串行、有限样本的机制测量。文件缓存、同步和调度足以让较大 payload 的某些时间低于较小 payload;不能据此宣称更大的消息更快,也不能换算成并发、分布式吞吐或产品 SLO。

按记录总量和保留期估算 ​

对某一次运行,可先用实际业务形状估算:

text
S_run = 固定元数据 + 历史尝试字节(H) + Pending 字节(P, 编码大小) + 状态和普通成果字节
W_run = S_1 + S_2 + ... + S_K
D_JSON = 所有被保留运行的当前 S_run 之和

H 是该运行保留的尝试数,P 是未确认消息数,K 是成功保存次数;重试会让尝试数超过成功消息数。S_run 是当前快照大小,W_run 是整个运行累计写出的逻辑 JSON 字节。若每步都增加固定大小的历史并保存整份记录,最终大小随步数增长,而累计字节会包含 1 + 2 + ... + H 这样的求和项。它不能仅由最终文件大小推断,更不能直接当作耗时的增长定律。

若完成运行的到达率约为 λ 份/天,业务选择保留 T 天,在稳定到达的简化假设下:

text
保留的完成记录数 R ≈ λ × T
D_JSON ≈ 活跃记录 JSON 总量 + λ × T × 平均完成记录大小

这时保留期乘的是记录份数和每份大小。例如,同样形状的 1000 份、每份 1024 条历史且无 Pending、32 字节字符串的组件记录,仅 JSON 内容为 1000 × 325233 = 325233000 字节;它是容量算式,不是本轮做了 1000 份的负载测试。还须另计锁文件、文件系统分配、备份及应用自己的存档。

完成后延长保留期增加占用,不会让同一份已静止的记录凭空多做 Save;一个持续处理消息的长运行,则会在自己的记录里继续积累历史。两者需要分开预算。业务保留期取决于恢复、审计和成本需求,本页不替宿主选择天数,也不提供逐条删除尝试历史的安全契约。

建议先取得目标环境的活跃/完成记录数、每份历史与 Pending 大小和保存频率,用相同矩阵复测并与宿主预算比较。这里已证实增长和整记录写出的成本来源;是否需要改变存储方式,仍应由实际目标规模和可接受成本决定。

复跑 ​

新增 benchmark 位于根目录 record_growth_filestore_test.go,真实 clone 的测试入口在 record_growth_test.go。从 Durable 仓库根目录运行下面的 PowerShell 命令;示例将临时文件放到仓库同级的 validation-scale,实际测量也使用独立验证目录。

powershell
$env:GOWORK = 'off'
$env:GOFLAGS = '-buildvcs=false'
$scaleArtifacts = [System.IO.Path]::GetFullPath('../validation-scale')
$env:TEMP = Join-Path $scaleArtifacts 'tmp'
$env:TMP = $env:TEMP
$env:GOTMPDIR = Join-Path $scaleArtifacts 'go-tmp'
[void][System.IO.Directory]::CreateDirectory($env:TEMP)
[void][System.IO.Directory]::CreateDirectory($env:GOTMPDIR)

go test -run '^TestRecordGrowth' -count=1 -v -timeout=60s .
go test -run '^$' -bench '^BenchmarkRecordGrowth$' -benchtime=10x -count=3 -benchmem -timeout=180s .
go test -run '^$' -bench '^BenchmarkRecordGrowthPipeline$' -benchtime=1x -count=3 -benchmem -timeout=180s .

扩大矩阵前先设消息数、payload 和操作次数上限。增加保留期估算不需要让 benchmark 空等若干天;用业务记录到达率与实测快照大小代入公式,再按需要验证目标存储环境。恢复教程说明业务如何重新进入已有运行。

目标 v0.1.0 · Go 1.26+ · Pulse v0.5.0 · 发布状态见 GitHub Releases · MIT