Go 分卷 zip 流水线备份碎文件
前言
远程 Windows 上若是海量小文件,直接用 SFTP 逐文件拷贝会非常慢。
先在本机按固定大小打成 zip,再传少量大包,通常更现实。
但若一次打完全部 zip 再传,源盘要同时堆很多临时包,中断后也难以续传。
本文给出两个 Go 命令行工具:zipbak-srv 跑在源服务器,zipbak-cli 跑在本机。
流水线是:压缩 A → 下载 A → 删除 A → 再压缩 B,B 里的文件在清单里紧接 A 之后,靠 state.json 保证不重复、不丢失。
下文约定两个自行开发的 Go 程序:zipbak-srv(源机)、zipbak-cli(本机),只说明行为与用法,不涉及具体仓库目录。
思路
有序清单
首次 init 递归扫描源目录,按相对路径字典序得到文件列表,写入状态文件(下文以 state.json 为例)。
清单里存的是相对路径:以 init 时 --dir 指向的源目录为根,不含盘符与源根前缀。
例如源为 D:/data/wwwjarapi/jkqfile/static,磁盘上的 D:/data/wwwjarapi/jkqfile/static/a/1.jpg 在清单中记为 a/1.jpg(建议统一用 /)。
pack 时用 源目录 + 相对路径 定位真实文件;打进 zip 时也用同一相对路径,解压后才能与源目录结构一致。
每个文件还可记录大小、修改时间(可选,见扩展中的 init 优化)。
文件量极大时,这一步可能耗时很久;见下文 扩展 中的 init 优化思路。
分卷规则
pack 从 next_file_index 起依次往 zip 里加文件,累计接近 --max-gb 就封包。
单个文件大于上限时,单独成包。
每打完一包,next_file_index 前进,并写入 pending_zip(尚未被客户端确认的包路径)。
流水线
- 客户端 SSH 执行远程 pack,stdout 得到 zip 绝对路径(若已有 pending 且文件还在,重复返回同一路径,不会多打一包)。
- SFTP download 到本机目录。
- SFTP delete 远程 zip,释放源盘。
- SSH ack,清除
pending_zip,才允许下一包 pack。
中断后重新跑 zipbak-cli:若 pending 还在,会先补传同一个 zip;若已 ack,则从下一文件继续打 B。
已写入 zip 的文件不会再次进入后续包。
中断恢复
可以续跑:进度在源机 state.json,本机重新执行 zipbak-cli 即可,不要轻易删掉 state.json 或 staging 里未处理的 pending 包。
| 断点位置 | 再跑时行为 |
|---|---|
| 正在 pack(zip 未写完) | 视实现:可能留下 .part 或半成品,需清理后 pack;已写入 state 的进度以文件为准 |
| pack 完成,尚未下载 | pending_zip 有效,pack 会返回同一个 zip 路径,客户端继续下载 |
| 下载到一半 | 本地可删 .downloading 半成品后重下;远程 zip 仍在则仍走 pending |
| 已下载,尚未删远程 zip | 可重新下载或校验本地文件后删远程并 ack |
| 已删远程 zip,尚未 ack | 危险:state 认为该包未确认;应立刻 ack,否则可能重复打同一批文件 |
| 已 ack | 从清单下一文件继续 pack 下一卷 |
整任务完成后 status 显示 done=true,且 staging 无残留 zip。
依赖
源服务器需 OpenSSH 服务器 与备份账号(如 backup_user),本机可 SSH/SFTP 登录。
Go 1.22+ 用于编译两个 CLI(版本按你本机为准)。
实现
部署
用 Go 分别编译 zipbak-srv.exe、zipbak-cli.exe 后,将 zipbak-srv.exe 放到源机,例如 D:\Tools\zipbak\。
本机 zipbak-cli.exe 与配置文件 zipbak-cli.json 可放在 D:\Tools\zipbak\。
源机目录分工
D:/data/backup_export/ 是备份任务用的工作区,与真实业务目录分开。
state.json 记录有序清单、打包进度、pending_zip 等,体积可能较大,单独放此路径。
staging/ 是临时 zip 输出目录:pack 生成的 part-000001.zip 等写在这里。
流水线要求「下完 A 删 A 再压 B」,因此同一时刻 staging 里通常只有当前待下载的那一包(或中断恢复时的 pending 包)。
客户端 SFTP 拉走并删远程 zip 后,staging 腾出空间,下一包再写入。
不要与 --dir 源目录混用:源目录只读业务文件,staging 只放可删的压缩包。
服务端命令
在源机上初始化任务(路径按你的环境修改):
1 | D:\Tools\zipbak\zipbak-srv.exe init ` |
手动打一个包(调试):
1 | D:\Tools\zipbak\zipbak-srv.exe pack --state D:/data/backup_export/state.json |
查看进度:
1 | D:\Tools\zipbak\zipbak-srv.exe status --state D:/data/backup_export/state.json |
客户端删远程 zip 后,在源机或由客户端 SSH 触发 ack:
1 | D:\Tools\zipbak\zipbak-srv.exe ack --state D:/data/backup_export/state.json |
源目录有新增文件时,先 refresh(会校验已打包前缀与磁盘一致,防止清单漂移):
1 | D:\Tools\zipbak\zipbak-srv.exe refresh --state D:/data/backup_export/state.json |
客户端配置
在本机新建 zipbak-cli.json,填写主机、账号与远程路径,例如:
1 | { |
首次在本机执行远程 init:
1 | D:\Tools\zipbak\zipbak-cli.exe -config zipbak-cli.json -init |
之后一键跑完整流水线(循环 pack → 下载 → 删远程 → ack):
1 | D:\Tools\zipbak\zipbak-cli.exe -config zipbak-cli.json |
验证
status 中 packed 应随每包 ack 后递增,done=true 表示全部文件已进入某 zip 且 pending 已清空。
本机 D:\Backup\jkqfile_bak 下应出现 part-000001.zip、part-000002.zip 等,解压后相对路径与源目录一致。
故意在下载完成后、删包前中断进程,再跑 zipbak-cli:应继续处理同一 pending 包,而不是跳过文件。
扩展
init 很慢时
瓶颈通常在:每个文件一次 stat、单线程 Walk、百万条记录塞进一个 JSON 的内存与序列化,而不是排序本身(排序多为 O(n log n),常比磁盘遍历便宜)。
可按成本从低到高考虑:
清单只存相对路径,pack 时再 stat
init 只收集路径(或边扫边写文本/SQLite),不预先写 size/mtime。
init 会快一些,pack 阶段略慢,但总时间往往更均衡。流式落盘,不要一次加载到内存
扫描时按行追加到manifest.lst,或写入 SQLite(rel_path主键 + 自增 id)。
pack 用WHERE id > ? ORDER BY id LIMIT …取下一批,避免巨型state.json。并行扫子目录
以源目录下的一级子目录为粒度,多 goroutine 各自 Walk,再在内存或磁盘 归并 成全局字典序。
适合 SSD、目录扇出大的树;注意磁盘随机读不要开过多并发。分片 state
按一级目录拆多个 job(多个 state + staging),init 与 pack 并行跑不同片,客户端按片拉取。
全局顺序变为「片内有序」,片间顺序固定即可,仍可不重不漏。init 与 pack 解耦
业务低峰单独跑 init(或refresh增量),完成后才启动 zipbak-cli。
增量场景用 refresh:只重扫并校验已打包前缀与磁盘一致,再追加新文件条目,避免每次全量 init。Windows 大量小文件(进阶)
若整树在同一 NTFS 卷,可评估 USN 变更日志 或商业备份 agent 的目录枚举 API,比 naive Walk 快,但实现与权限成本高,一般作为后续优化。
无论哪种实现,pack 仍必须按稳定顺序消费清单,且 pending / ack 语义不变,才能保证 A 下完再 B、不重不漏。
生产环境建议为 SSH 配置 known_hosts,客户端校验主机密钥,勿忽略指纹。
密码登录可改为密钥登录,在客户端 SSH 库中增加私钥认证即可。
若单文件大于 --max-gb 设定的一包上限,会单独成包;需要拆文件本身则超出本方案范围。
总结
- init 生成有序清单;pack 按
max-gb分卷并维护next_file_index与 pending_zip。 - zipbak-cli 负责 pack → SFTP 下载 → 删远程 zip → ack,再进入下一卷。
- 中断重跑依赖 pending_zip 与 state,避免重复打包或漏文件。