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 就能做出一个可用的语义检索
相关推荐
NAGNIP6 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab7 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab7 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
十日十行10 小时前
Linux和window共享文件夹
linux
AngelPP11 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年11 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼11 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS11 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区12 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈12 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能