AI云存储学习笔记:小文件优化 / 大文件分片 / 分享与 AI 搜索
文章目录
- [AI云存储学习笔记:小文件优化 / 大文件分片 / 分享与 AI 搜索](#AI云存储学习笔记:小文件优化 / 大文件分片 / 分享与 AI 搜索)
-
- 一、项目整体结构概览
- [二、小文件存储优化:trunk + 空闲块管理](#二、小文件存储优化:trunk + 空闲块管理)
-
- [2.1 为什么要优化小文件](#2.1 为什么要优化小文件)
- [2.2 空闲块结构(思想层面)](#2.2 空闲块结构(思想层面))
- [2.3 删除小文件时的处理](#2.3 删除小文件时的处理)
- 三、大文件分片上传与断点续传
-
- [3.1 为什么要分片上传](#3.1 为什么要分片上传)
- [3.2 服务器端记录什么状态](#3.2 服务器端记录什么状态)
- [3.3 为什么不是"记录字节偏移继续传"](#3.3 为什么不是“记录字节偏移继续传”)
- [3.4 分片合并的基本流程](#3.4 分片合并的基本流程)
- 四、文件分享、访问控制和次数限制
-
- [4.1 share_id:隐藏真实路径](#4.1 share_id:隐藏真实路径)
- [4.2 访问权限的判断](#4.2 访问权限的判断)
- [4.3 仅用 MySQL 控制访问次数](#4.3 仅用 MySQL 控制访问次数)
- [五、共享排行榜:Redis Sorted Set 的使用](#五、共享排行榜:Redis Sorted Set 的使用)
- [六、AI 智能搜索:向量 + RAG 的简单实现思路](#六、AI 智能搜索:向量 + RAG 的简单实现思路)
-
- [6.1 索引构建:把文件变成"可搜索的文本片段"](#6.1 索引构建:把文件变成“可搜索的文本片段”)
- [6.2 向量化和 Faiss 索引](#6.2 向量化和 Faiss 索引)
- [6.3 查询:query → embedding → TopK → 回表](#6.3 查询:query → embedding → TopK → 回表)
- 七、小结
纯个人学习记录,方便之后面试复盘,也当自己做这个云存储项目的开发日记。技术栈主要是:C++ 后端 + FastDFS 分布式文件存储 + MySQL + Redis + Nginx,再在上面自己补了一些小文件优化、大文件分片上传、分享控制和 AI 搜索的东西。
一、项目整体结构概览
先把整体轮廓画一下,后面每一块都能找到位置:
- Nginx :
- 反向代理 + 静态资源
- 上传时做"落盘中转":先把大文件写到临时目录,再把元信息转给后端
- 通过
ngx_fastdfs_module做 FastDFS 文件访问网关
- 后端服务(C++) :
- 负责登录鉴权、文件上传下载接口
- 实现分片上传 / 断点续传 / 秒传
- 做小文件存储优化(trunk 思路)
- 分享、权限校验、排行榜、访问次数控制
- AI 搜索接口:把用户 query 转成 embedding,走向量检索
- FastDFS :
- 文件真正的存储位置,按
group+storage分布 - 支持小文件 trunk 存储
- 文件真正的存储位置,按
- MySQL :
- 存用户、文件元数据、分享记录、AI 检索的 chunk 信息等
- Redis :
- 做缓存 / 计数
- 排行榜用 Sorted Set
下面主要记三块:
- 小文件优化
- 大文件分片上传 + 断点续传
- 文件分享 / 排行榜 / AI 搜索这几个"周边能力"
二、小文件存储优化:trunk + 空闲块管理
2.1 为什么要优化小文件
在分布式文件系统里,大量特别小的文件(几 KB 级别)如果每个都当"大文件"去存,会带来几个典型问题:
- inode / 元数据开销很大
- 目录项、索引膨胀
- 随机 IO 变多
FastDFS 自带的一个思路是 trunk 文件:
- 不再一个小文件对应一个物理文件
- 而是预分配一个比较大的 trunk,比如 64 MB
- 一堆小文件按块塞进这个大 trunk 里,每个文件只是 trunk 里的
(offset, length)
我自己的理解是:把磁盘空间先"批发"成几块大仓库,然后每个小文件只是仓库里的一块货位。
2.2 空闲块结构(思想层面)
要实现这种方式,就涉及一个问题:怎么管理 trunk 内的空闲空间。
比较自然的一种做法是维护一棵"按块大小排序"的树结构(可以是 AVL / 红黑树),每个节点大致包含:
text
struct FreeBlock {
uint64_t size; // 空闲块大小
uint64_t trunk_id; // 属于哪个 trunk 文件
uint64_t offset; // 在 trunk 文件中的起始偏移
};
- 插入 / 删除空闲块:树结构保证在 O(logN) 内完成
- 分配时可以按 size 找到"第一个 >= 文件大小" 的块(类似 best-fit)
真正写文件时的思路:
-
业务层拿到一个待写入的小文件,先看大小
file_size -
在"空闲块树"里找一个
size >= file_size的块 -
把该块一部分或全部分配给这个文件
-
如果有剩余空间,再把剩余的部分回插回空闲树
-
在 MySQL 中记录:
textfile_id -> (trunk_id, offset, length)
这条映射是后面读文件、删除文件的依据。
2.3 删除小文件时的处理
删除时并不是直接 unlink trunk,而是走两步:
- 业务元数据 :
- 根据
file_id查到对应的(trunk_id, offset, length) - 把这条记录标记为删除(软删)或直接删掉
- 根据
- 空闲块合并 :
- 把这个
(offset, length)当成一个新的空闲块插回到树里 - 如果左边/右边有"紧挨着的空闲块",做一次 merge,减少碎片
- 把这个
整体优化点:
- 一个 trunk 文件对应一批小文件,减少文件数量
- 有一套空闲块结构管理碎片
- 业务层靠
file_id -> (trunk_id, offset, length)把物理位置和逻辑 ID 解耦
这块我目前更多是从原理和配置上理解,还没有把完整的 trunk 管理代码自己从零实现一遍,后面有时间可以单独写一个 demo 玩一玩。
三、大文件分片上传与断点续传
这一块是我这两天重点梳理的,尤其是"为什么按分片而不是按字节偏移记录进度"。
3.1 为什么要分片上传
主要两个原因:
- 大文件的可靠性
- 网络抖一下,如果是整文件上传,失败后要从头来过
- 分片之后,只需要重传失败的几个片
- 充分利用带宽和服务器资源
- 多个分片可以并发上传
- 服务端可以按分片做限速、排队、校验
在实现上,我会给每次大文件上传生成一个 upload_id,然后约定:
- 客户端按
chunk_index = 1, 2, 3...依次上传 - 每一片有自己的
md5,便于单片校验
3.2 服务器端记录什么状态
我现在的设计更偏向 "按分片为单位" 管理进度,而不是按"字节数"。
可以在 MySQL / Redis 里维护一张分片表(伪结构):
text
upload_id
chunk_index
chunk_size
status // PENDING / DONE
上传一片成功后,就把对应记录标记为 DONE。
这样做有几个好处:
- 协议层面清晰:最小单位就是"分片"
- 校验简单:一片一个 MD5
- 存储层配合度更高:可以按片写临时文件,最后再合并
3.3 为什么不是"记录字节偏移继续传"
从表面看,"记录已经成功的字节数,然后从这个偏移继续传"好像更精细,但实际实现起来坑比较多:
- 存储层不一定适合任意字节覆盖
- 像 FastDFS/对象存储,更多是 append / 整块写
- 从一个中间偏移去改写半片,对接上比较麻烦
- 半片数据不好做校验
- 一半老数据 + 一半新数据,中间边界出错很难排查
- 客户端/服务端逻辑都要处理"半片"
- 分片协议还在,但你又引入了"分片内偏移",复杂度明显上升
所以我最后的结论是:让分片足够小,用"分片号"作为重试单位就够了。对用户来说,体验上还是"从中断处继续传";对实现来说,其实是"从下一个没完成的分片继续"。
3.4 分片合并的基本流程
后台服务在所有分片都变成 DONE 之后,做一次合并:
- 从分片表查出该
upload_id下所有分片,按chunk_index排序 - 一片一片按顺序读出来,写入一个最终文件(可以直接写到 FastDFS,或者先在本地拼好再上传)
- 合并成功后:
- 生成最终的
file_id - 写入文件元数据表
- 标记分片记录为已归档 / 可清理
- 生成最终的
中间可以加一些防御措施:
- 总大小校验(所有分片 size 累加 == 用户声明的文件大小)
- 按分片 MD5 校验
四、文件分享、访问控制和次数限制
做网盘类系统,分享是一个很典型的功能。这里我梳理了三件事:
- 如何隐藏真实存储路径
- 如何判断某个用户有没有权限访问某个文件
- 如何控制一个分享链接的访问次数
4.1 share_id:隐藏真实路径
底层存储对外是 FastDFS 的地址,类似:
text
group1/M00/00/00/xxx...
这些信息我不直接给前端看,而是统一通过一个 share_id / file_id 兜起来:
- 用户发起分享时:
- 生成一个随机的
share_id - 在
share表里记录映射:share_id -> file_id, creator, expire_time, max_times...
- 生成一个随机的
- 对外的 URL 长这样:
https://xxx.com/s/{share_id}
- 请求进来后:
- 先到后端接口
/api/share/download?share_id=... - 后端根据
share_id查到真实file_id - 做权限 + 有效期校验
- 再去 FastDFS 拉文件返回
- 先到后端接口
对用户来说,看到的永远是一串 share_id,底层 group、磁盘路径都是隐藏的。
4.2 访问权限的判断
下载接口的一个"基本套路"是:
- 先确认是谁
- 通过 token / session 拿到当前
user_id
- 通过 token / session 拿到当前
- 再确认有没有权限
- 如果是本人上传的:
files.owner_id == user_id→ 允许 - 如果是分享链接:检查
share表:- 是否存在
- 是否过期
- 次数是否用完
- 如果是公开文件 / 团队空间:
files.is_public == 1或者在space_user里有对应关系
- 如果是本人上传的:
只有上述条件之一满足,才继续去读存储;否则返回无权限。
4.3 仅用 MySQL 控制访问次数
有一个挺典型的题目是:"分享链接已经生成了,怎么限制可访问次数?"
纯 MySQL 的方案可以简单粗暴一点:
- 在
share表加两个字段:max_times:最大允许访问次数used_times:已经访问了多少次
- 每次访问时执行一个带条件的
UPDATE:
sql
UPDATE share
SET used_times = used_times + 1
WHERE share_id = ?
AND used_times < max_times
AND NOW() < expire_time;
后端根据 受影响行数 来判断:
affected_rows = 1→ 本次访问有效,继续读文件affected_rows = 0→ 次数用尽或已过期,直接返回"链接失效"
利用 InnoDB 的行锁,这个更新在并发场景下也是原子的,不容易出现"多扣几次"的情况。
五、共享排行榜:Redis Sorted Set 的使用
排行榜这种场景跟 Redis 的 Sorted Set 非常契合,这里简单记一下结构:
- key:
share:rank - member:
share_id(或者直接用file_id) - score:热度值(目前可以直接用"下载次数";如果要复杂一点,可以是下载 + 收藏加权)
更新和查询都比较直接:
-
有一次下载成功 :
textZINCRBY share:rank 1 <share_id> -
取 Top N :
textZREVRANGE share:rank 0 9 WITHSCORES
拿到 share_id 和分数之后,再去 MySQL 里查文件名、分享者等信息就行。
六、AI 智能搜索:向量 + RAG 的简单实现思路
这一块我没有做到非常重型的知识库,只是结合项目做了一个按文件内容的语义搜索,大致思路如下。
6.1 索引构建:把文件变成"可搜索的文本片段"
整体流程是异步的:
-
用户正常把文件上传到 FastDFS,接口返回
file_id -
上传完成后,往一个"待索引表"里插一条记录:
textpending_index(file_id, user_id, status = NEW) -
后台有个索引服务定期扫这张表:
- 从 FastDFS 拉取文件
- 按类型解析文本(支持 txt / pdf / doc 等)
- 把长文本切成多段
chunk(比如每段 500~800 字) - 每个 chunk 生成一个
chunk_id - 把
(chunk_id, file_id, user_id, chunk_text, file_name...)写入 MySQL
6.2 向量化和 Faiss 索引
对每个 chunk_text:
- 调用云端的 embedding API,得到一个 d 维向量
embedding - 向量写入 Faiss 索引,业务上以
chunk_id作为 ID - MySQL 那边保存
chunk_id以及对应的文本 / 文件信息
从 Faiss 的角度看,其实就两类数据:
- 向量矩阵:
N × d - 每个向量对应的
id(就是chunk_id)
真正的文本、用户、权限之类的都放在 MySQL 里,Faiss 只负责"算相似度"。
6.3 查询:query → embedding → TopK → 回表
用户输入一句话,比如"慢查询怎么排查":
- 调用同一个 embedding API,把 query 变成向量
query_vec - 把
query_vec丢进 Faiss 做 TopK 检索:- 拿回来一组最相似的
chunk_id+ 距离
- 拿回来一组最相似的
- 根据这些
chunk_id回到 MySQL 的 chunk 表里查:- 对应的
file_id、file_name、chunk_text、user_id
- 对应的
- 根据 user_id 和文件权限做过滤
- 最后把匹配到的文件列表 + 高亮片段返回给前端
如果再往前一步,可以在这个基础上做一个简单的 RAG:
- 把召回到的几个
chunk_text拼成 context - 连同用户问题一起喂给大模型,让它组织一段更自然的回答
这一块目前先停在"检索 + 展示"的阶段,已经可以按内容找到历史文件,对我自己的使用场景来说已经够用。
七、小结
这篇基本是把一天里脑子里反复过的几块东西整理了一遍:
- 小文件优化更多是理解 FastDFS 的 trunk 机制和空闲块管理思路
- 大文件分片上传主要是理清"为什么按分片记录进度,而不是按字节偏移"
- 分享 / 权限 / 次数控制,其实是几张表 + 一点点状态机
- AI 搜索这块在项目里更多是一个"加分项",用现成的 embedding + Faiss 就能做出一个可用的语义检索
