AI云存储学习笔记:小文件优化 / 大文件分片 / 分享与 AI 搜索

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)

真正写文件时的思路:

  1. 业务层拿到一个待写入的小文件,先看大小 file_size

  2. 在"空闲块树"里找一个 size >= file_size 的块

  3. 把该块一部分或全部分配给这个文件

  4. 如果有剩余空间,再把剩余的部分回插回空闲树

  5. 在 MySQL 中记录:

    text 复制代码
    file_id -> (trunk_id, offset, length)

这条映射是后面读文件、删除文件的依据。

2.3 删除小文件时的处理

删除时并不是直接 unlink trunk,而是走两步:

  1. 业务元数据
    • 根据 file_id 查到对应的 (trunk_id, offset, length)
    • 把这条记录标记为删除(软删)或直接删掉
  2. 空闲块合并
    • 把这个 (offset, length) 当成一个新的空闲块插回到树里
    • 如果左边/右边有"紧挨着的空闲块",做一次 merge,减少碎片

整体优化点:

  • 一个 trunk 文件对应一批小文件,减少文件数量
  • 有一套空闲块结构管理碎片
  • 业务层靠 file_id -> (trunk_id, offset, length) 把物理位置和逻辑 ID 解耦

这块我目前更多是从原理和配置上理解,还没有把完整的 trunk 管理代码自己从零实现一遍,后面有时间可以单独写一个 demo 玩一玩。


三、大文件分片上传与断点续传

这一块是我这两天重点梳理的,尤其是"为什么按分片而不是按字节偏移记录进度"。

3.1 为什么要分片上传

主要两个原因:

  1. 大文件的可靠性
    • 网络抖一下,如果是整文件上传,失败后要从头来过
    • 分片之后,只需要重传失败的几个片
  2. 充分利用带宽和服务器资源
    • 多个分片可以并发上传
    • 服务端可以按分片做限速、排队、校验

在实现上,我会给每次大文件上传生成一个 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 为什么不是"记录字节偏移继续传"

从表面看,"记录已经成功的字节数,然后从这个偏移继续传"好像更精细,但实际实现起来坑比较多:

  1. 存储层不一定适合任意字节覆盖
    • 像 FastDFS/对象存储,更多是 append / 整块写
    • 从一个中间偏移去改写半片,对接上比较麻烦
  2. 半片数据不好做校验
    • 一半老数据 + 一半新数据,中间边界出错很难排查
  3. 客户端/服务端逻辑都要处理"半片"
    • 分片协议还在,但你又引入了"分片内偏移",复杂度明显上升

所以我最后的结论是:让分片足够小,用"分片号"作为重试单位就够了。对用户来说,体验上还是"从中断处继续传";对实现来说,其实是"从下一个没完成的分片继续"。

3.4 分片合并的基本流程

后台服务在所有分片都变成 DONE 之后,做一次合并:

  1. 从分片表查出该 upload_id 下所有分片,按 chunk_index 排序
  2. 一片一片按顺序读出来,写入一个最终文件(可以直接写到 FastDFS,或者先在本地拼好再上传)
  3. 合并成功后:
    • 生成最终的 file_id
    • 写入文件元数据表
    • 标记分片记录为已归档 / 可清理

中间可以加一些防御措施:

  • 总大小校验(所有分片 size 累加 == 用户声明的文件大小)
  • 按分片 MD5 校验

四、文件分享、访问控制和次数限制

做网盘类系统,分享是一个很典型的功能。这里我梳理了三件事:

  1. 如何隐藏真实存储路径
  2. 如何判断某个用户有没有权限访问某个文件
  3. 如何控制一个分享链接的访问次数

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}
  • 请求进来后:
    1. 先到后端接口 /api/share/download?share_id=...
    2. 后端根据 share_id 查到真实 file_id
    3. 做权限 + 有效期校验
    4. 再去 FastDFS 拉文件返回

对用户来说,看到的永远是一串 share_id,底层 group、磁盘路径都是隐藏的。

4.2 访问权限的判断

下载接口的一个"基本套路"是:

  1. 先确认是谁
    • 通过 token / session 拿到当前 user_id
  2. 再确认有没有权限
    • 如果是本人上传的: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:热度值(目前可以直接用"下载次数";如果要复杂一点,可以是下载 + 收藏加权)

更新和查询都比较直接:

  • 有一次下载成功

    text 复制代码
    ZINCRBY share:rank 1 <share_id>
  • 取 Top N

    text 复制代码
    ZREVRANGE share:rank 0 9 WITHSCORES

拿到 share_id 和分数之后,再去 MySQL 里查文件名、分享者等信息就行。


六、AI 智能搜索:向量 + RAG 的简单实现思路

这一块我没有做到非常重型的知识库,只是结合项目做了一个按文件内容的语义搜索,大致思路如下。

6.1 索引构建:把文件变成"可搜索的文本片段"

整体流程是异步的:

  1. 用户正常把文件上传到 FastDFS,接口返回 file_id

  2. 上传完成后,往一个"待索引表"里插一条记录:

    text 复制代码
    pending_index(file_id, user_id, status = NEW)
  3. 后台有个索引服务定期扫这张表:

    • 从 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

  1. 调用云端的 embedding API,得到一个 d 维向量 embedding
  2. 向量写入 Faiss 索引,业务上以 chunk_id 作为 ID
  3. MySQL 那边保存 chunk_id 以及对应的文本 / 文件信息

从 Faiss 的角度看,其实就两类数据:

  • 向量矩阵:N × d
  • 每个向量对应的 id(就是 chunk_id

真正的文本、用户、权限之类的都放在 MySQL 里,Faiss 只负责"算相似度"。

6.3 查询:query → embedding → TopK → 回表

用户输入一句话,比如"慢查询怎么排查":

  1. 调用同一个 embedding API,把 query 变成向量 query_vec
  2. query_vec 丢进 Faiss 做 TopK 检索:
    • 拿回来一组最相似的 chunk_id + 距离
  3. 根据这些 chunk_id 回到 MySQL 的 chunk 表里查:
    • 对应的 file_id、file_name、chunk_text、user_id
  4. 根据 user_id 和文件权限做过滤
  5. 最后把匹配到的文件列表 + 高亮片段返回给前端

如果再往前一步,可以在这个基础上做一个简单的 RAG

  • 把召回到的几个 chunk_text 拼成 context
  • 连同用户问题一起喂给大模型,让它组织一段更自然的回答

这一块目前先停在"检索 + 展示"的阶段,已经可以按内容找到历史文件,对我自己的使用场景来说已经够用。


七、小结

这篇基本是把一天里脑子里反复过的几块东西整理了一遍:

  • 小文件优化更多是理解 FastDFS 的 trunk 机制和空闲块管理思路
  • 大文件分片上传主要是理清"为什么按分片记录进度,而不是按字节偏移"
  • 分享 / 权限 / 次数控制,其实是几张表 + 一点点状态机
  • AI 搜索这块在项目里更多是一个"加分项",用现成的 embedding + Faiss 就能做出一个可用的语义检索
相关推荐
小马爱打代码2 小时前
Spring Boot Actuator 学习笔记
spring boot·笔记·学习
Calebbbbb2 小时前
如何用 GitHub 下载单一目录 / 子目录
笔记·github
kubernetes-k8s2 小时前
计划开始学习:OpenStack从入门到精通
linux·运维·服务器
IT_陈寒2 小时前
Vite 5大实战优化技巧:让你的开发效率提升200%|2025前端工程化指南
前端·人工智能·后端
嵌入式×边缘AI:打怪升级日志2 小时前
USB设备枚举过程详解:从插入到正常工作
开发语言·数据库·笔记
学习是生活的调味剂2 小时前
在大模型开发中,是否需要先完整学习 TensorFlow,再学 PyTorch?
pytorch·学习·tensorflow·transformers
Jerryhut2 小时前
OpenCv总结5——图像特征——harris角点检测
人工智能·opencv·计算机视觉
天码-行空2 小时前
【大数据环境安装指南】ZooKeeper搭建spark高可用集群教程
大数据·linux·运维·zookeeper·spark
笨鸟先飞的橘猫2 小时前
mongo权威指南(第三版)学习笔记
笔记·学习