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 优化思路。

分卷规则

packnext_file_index 起依次往 zip 里加文件,累计接近 --max-gb 就封包。

单个文件大于上限时,单独成包。

每打完一包,next_file_index 前进,并写入 pending_zip(尚未被客户端确认的包路径)。

流水线

  1. 客户端 SSH 执行远程 pack,stdout 得到 zip 绝对路径(若已有 pending 且文件还在,重复返回同一路径,不会多打一包)。
  2. SFTP download 到本机目录。
  3. SFTP delete 远程 zip,释放源盘。
  4. 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.exezipbak-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
2
3
4
5
D:\Tools\zipbak\zipbak-srv.exe init `
--dir D:/data/wwwjarapi/jkqfile/static `
--state D:/data/backup_export/state.json `
--staging D:/data/backup_export/staging `
--max-gb 2

手动打一个包(调试):

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
2
3
4
5
6
7
8
9
10
11
12
{
"host": "192.168.1.10",
"port": 22,
"user": "backup_user",
"password": "change-me",
"remote_srv": "D:/Tools/zipbak/zipbak-srv.exe",
"remote_state": "D:/data/backup_export/state.json",
"remote_source": "D:/data/wwwjarapi/jkqfile/static",
"remote_staging": "D:/data/backup_export/staging",
"local_dir": "D:/Backup/jkqfile_bak",
"max_part_gb": 2
}

首次在本机执行远程 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

验证

statuspacked 应随每包 ack 后递增,done=true 表示全部文件已进入某 zip 且 pending 已清空。

本机 D:\Backup\jkqfile_bak 下应出现 part-000001.zippart-000002.zip 等,解压后相对路径与源目录一致。

故意在下载完成后、删包前中断进程,再跑 zipbak-cli:应继续处理同一 pending 包,而不是跳过文件。

扩展

init 很慢时

瓶颈通常在:每个文件一次 stat单线程 Walk百万条记录塞进一个 JSON 的内存与序列化,而不是排序本身(排序多为 O(n log n),常比磁盘遍历便宜)。

可按成本从低到高考虑:

  1. 清单只存相对路径,pack 时再 stat
    init 只收集路径(或边扫边写文本/SQLite),不预先写 size/mtime。
    init 会快一些,pack 阶段略慢,但总时间往往更均衡。

  2. 流式落盘,不要一次加载到内存
    扫描时按行追加到 manifest.lst,或写入 SQLiterel_path 主键 + 自增 id)。
    packWHERE id > ? ORDER BY id LIMIT … 取下一批,避免巨型 state.json

  3. 并行扫子目录
    以源目录下的一级子目录为粒度,多 goroutine 各自 Walk,再在内存或磁盘 归并 成全局字典序。
    适合 SSD、目录扇出大的树;注意磁盘随机读不要开过多并发。

  4. 分片 state
    按一级目录拆多个 job(多个 state + staging),init 与 pack 并行跑不同片,客户端按片拉取。
    全局顺序变为「片内有序」,片间顺序固定即可,仍可不重不漏。

  5. init 与 pack 解耦
    业务低峰单独跑 init(或 refresh 增量),完成后才启动 zipbak-cli
    增量场景用 refresh:只重扫并校验已打包前缀与磁盘一致,再追加新文件条目,避免每次全量 init。

  6. Windows 大量小文件(进阶)
    若整树在同一 NTFS 卷,可评估 USN 变更日志 或商业备份 agent 的目录枚举 API,比 naive Walk 快,但实现与权限成本高,一般作为后续优化。

无论哪种实现,pack 仍必须按稳定顺序消费清单,且 pending / ack 语义不变,才能保证 A 下完再 B、不重不漏。

生产环境建议为 SSH 配置 known_hosts,客户端校验主机密钥,勿忽略指纹。

密码登录可改为密钥登录,在客户端 SSH 库中增加私钥认证即可。

若单文件大于 --max-gb 设定的一包上限,会单独成包;需要拆文件本身则超出本方案范围。

总结

  1. init 生成有序清单;packmax-gb 分卷并维护 next_file_indexpending_zip
  2. zipbak-cli 负责 pack → SFTP 下载 → 删远程 zip → ack,再进入下一卷。
  3. 中断重跑依赖 pending_zipstate,避免重复打包或漏文件。