3.1 秒传:指纹去重的账与坑
场景:第三次上传同一个安装包
团队里常有这样的场景:设计小姐姐把同一份素材传了三遍,安装包每个人电脑里都有一份又各自往网盘传一份。从网盘角度看,同一份 2GB 的文件被上传三次,流量费交了三份,存储费三份,用户等了三遍。而服务器上存的其实是三份字节完全相同的副本。
秒传的逻辑简单到像一句废话:上传前先问一句这个内容传过没有。传过就返回已有记录,不传了。但这一问省下的钱和等待时间相当可观。我们的账单里,开启秒传后上传带宽消耗降了四成——我们团队里相同素材重复率就是这么高。
方案:指纹怎么算才稳
秒传的成败全在指纹。指纹错了有两种结局:误判为相同导致用户下错文件,或误判为不同导致重复存。我们的选型经历了两版:
| 方案 | 准确度 | 计算成本 | 适用 |
|---|---|---|---|
| 文件名加大小 | 差,改名即失效 | 极低 | 否 |
| 全量哈希 | 满分 | 高,大文件要读全文件 | 小文件 |
| 头部数据加大小 | 有碰撞风险 | 低 | 初筛 |
| 分片哈希 | 满分 | 中,可流式计算 | 大文件采用 |
实际方案是两级:先以头部 4MB 加文件大小做快速初筛,命中后再算全量哈希确认。全量哈希命中才算真秒传,否则老老实实走上传。这套设计让小文件秒传几乎零延迟,大文件也只多读一小段。
秒传的命中流程也值得列表化,因为每一步都可能出岔子:
| 步骤 | 动作 | 失败处理 |
|---|---|---|
| 1 客户端算指纹 | 头部 4MB 与大小 | 失败则放弃秒传 |
| 2 后端初筛 | 查索引表 | 未命中走上传 |
| 3 全量确认 | 客户端算全量哈希 | 不一致走上传 |
| 4 建立引用 | 元数据指向已有对象 | 冲突时重建 |
配置:Go 侧指纹与索引
客户端算指纹,后端只存索引。Go 代码示意(剥掉错误处理):
// 客户端:先拿头部粗判
f, _ := os.Open(path)
head := make([]byte, 4<<20)
io.ReadFull(f, head)
quick := fmt.Sprintf("%d:%x", size, sha256(head))
// 后端:粗判命中后要求全量确认
if hit := index.Find(quick); hit != nil {
return hit.NeedFullHash // 让客户端算全量
}
全量确认之后秒传才真正成立。后端建立的是一条引用记录:逻辑文件指向物理对象,多个逻辑文件共享一份存储。删除时要做引用计数,真正没人引用了才删 COS 上的对象,否则删一次删掉所有人的文件,事故级别。
// 后端:引用计数式删除
if refs := index.DecRef(key); refs == 0 {
cos.DeleteObject(ctx, key) // 真正物理删除
}
踩坑:秒传的四个坑
第一个坑,初筛碰撞。两个不同文件头部相同大小相同是小概率事件,但文件量一大必然发生。我们就遇到过两个安装包头部 4MB 完全一致(同一套打包工具),大小恰好相同,被误判。全量确认这道闸门必须有,靠概率赌安全性早晚出事。
第二个坑,引用计数的并发安全。两个用户同时秒传同一份文件,同时建立引用,计数可能乱套。必须给对象加锁或用数据库的原子更新,不然计数错一个就敢给你表演消失。
第三个坑,秒传击穿了私密性。A 用户传了一份文件,B 用户拿到相同内容也能秒传成功并访问前者的文件——秒传只认内容不认人。我们的做法是秒传只在同一用户内生效,跨用户即便内容相同也各存一份,存储换安全。账面上多花一点存储费,换来的是隐私边界清晰。
第四个坑,指纹索引的存储成本。索引表按指纹建唯一索引,文件多了写入变慢,定期归档冷数据索引。还有个小账:全量哈希对 CPU 消耗不小,客户端算大文件哈希时风扇呼呼响,低优先级线程跑让它别影响用户操作,线程优先级也是体验。 再细聊聊秒传收益的量化。上线一个月后我们拉了数字:上传请求里有 38% 命中秒传,这些请求的平均耗时从分钟级降到 300 毫秒内;上传带宽费用环比下降 42%,因为大头文件不再重复传。仅这一项就抵消了开发成本。判断一个功能值不值得做,先看它能不能算出一笔钱或一个百分比,算不出来就等着被领导砍预算。
引用计数的存储设计也值得单独说。我们用一张独立的引用表而非在文件表上加字段,原因是秒传引用关系会频繁增删,和文件元数据的生命周期不同步。表结构极其简单:对象键加引用者加时间,联合主键,删除时按条件删行,查计数就是一句 count。简单到面试题级别,但扛住了几百万行数据。
| 设计点 | 选择 | 理由 |
|---|---|---|
| 引用表独立 | 是 | 高频增删与元数据解耦 |
| 计数实现 | 实时 count | 数据量小,省去一致性麻烦 |
| 并发控制 | 数据库行锁 | 简单可靠 |
| 冷数据归档 | 按季度 | 保持热表小而快 |
客户端算指纹还有个细节必须在 WebWorker 里做。浏览器主线程算大文件哈希会卡死 UI,用户以为浏览器崩了。我们的做法是单独开 worker,主线程只管进度条,哈希算完由 worker 回传结果。移动端 app 则交给原生线程,顺手做低优先级,别让算指纹把手机烘烤到降频。
最后讲一个真实事故。秒传上线第二周,一个用户传了份合同,另一个用户传的同名同大小的另一份合同。因为是跨用户我们没给秒传,两人互不影响,平安无事。但上线初期我们曾经图省事放开过跨用户秒传,幸亏内测期就发现了一个同名 excel 串号的 bug,赶紧关掉。永远记住,内容去重是存储层的优化,不是权限层的捷径,两者绝不能混。 再谈谈监控。秒传的命中率我们做成每天的固定指标:今天的命中率是 36% 还是 40%,代表团队这周在搬运的到底是新货还是老货。命中率突然掉到个位数是坏信号,说明指纹索引坏了或客户端哈希没算成功。有一次命中率先兆性下跌,我们提前发现了索引重建任务挂掉,没有等用户投诉。给关键功能一个健康指标是运维层面的标配,比出事再查从容得多。
哈希算法也换过。最初用 sha256,后来换 blake3,大文件计算时间降到三分之一,客户端机器负担明显减轻。换来换去没人拦着,因为指纹对外的语义只是唯一标识,算法可换、索引要重建一次而已。内部工具的好处就是沟通成本低,换算法不需要开三天会。
给秒传留一条降级路径也是被坑出来的。指纹服务依赖索引表,索引数据库迁移那天全天在锁表,秒传接口全部超时。客户端发现秒传接口无响应时直接降级走正常上传,用户无感知。永远假设任何依赖的服务都可能挂,然后让他挂得悄无声息。
指纹本身还有个微妙的隐私点:哈希能反推内容存在。别人手里有某文件,算哈希一查你的接口就知道你这儿有没有。对我们内部工具无所谓,对做开放平台的团队要注意秒传查询接口的防枚举。安全书上没写这种细节,只能靠踩坑长见识。
多说一句数据层面的经验。秒传日志建议保留,它是去重收益的直接证据,年底做成本复盘时是最好看的一页 PPT。我们的月报里有一句常驻:本月秒传为团队节省 212GB 重复传输。领导看了开心,开发看了也有成就感,写代码的人需要这种正反馈。 顺嘴说一句秒传的边界条件。压缩包是重灾区:同一个人把素材打成 zip,前后两次内容略不同,指纹不同,秒传失效,用户不理解为什么不行。解释成本不低,我们干脆在秒传失败时给个温和提示,告诉用户本次未命中去重,总比让用户干等强。产品文案是技术功能的最后一公里。
还有一类文件根本不该秒传:被审核标记为违规的内容,指纹命中也不能放行。去重逻辑必须先查屏蔽状态再返回成功,否则违规文件换个用户就复活。这类漏洞只能在安全评审时被发现,做去重的同学一定要拉上做审核的同学互相 review。 秒传还有一个少有人提的收益:它倒逼团队做内容管理。因为指纹表存在,我们顺手做了一个重复文件查询工具,谁存了重复内容一目了然。每个月清理一次,团队的存储习惯也变好了。功能顺带出来的管理价值,往往比功能本身更长远。
秒传最怕指纹碰撞。头部哈希初筛理论上会撞,我们加了一道兜底:命中秒传后仍然校验全量哈希,不一致立即回退为正常上传。多一次哈希计算的成本,换来的是零误判的确定性。
引用计数的删除逻辑也演练过。某文件秒传引用了七次,删除一次只是计数减一,直到归零才真正删源文件。这套机制让一个存储位置被多用户共享时仍然安全,是秒传系统能长期运行的地基。
秒传上线第一个月我们就监测到存储增长放缓,新增上传里四成是重复文件。监控面板上重复率曲线是秒传价值的直观证明,这张图后来出现在每次季度汇报里,老板最爱看。
指纹计算放在客户端还是服务端,我们争论过一周。客户端算节省带宽,服务端算架构简单。最终两段式:客户端先算头部指纹预查,服务端再算全量确认,各取所长。
小结
秒传是典型的用计算换带宽的设计。算得对,省下的全是真金白银;算不对,付的是隐私与数据的代价。上线前把两级指纹、引用计数、并发安全这三关各自写测试用例覆盖一遍,再敢放给用户用。我们的秒传上线后,每月上传带宽费用下降四成,唯一一次事故来自初筛碰撞,靠全量确认兜住了,毫发无损。
