
1. 先看全局四层存储到底在 RAG 系统里各自扛什么活很多人第一次部署 RAGFlow 时看着 docker-compose 里 mysql、minio、elasticsearch、redis 四个服务一脸懵一个做 RAG检索增强生成问答的系统为什么要同时拉起来数据库、对象存储、搜索引擎和缓存我最早也是这么想的直到后来排查“文档解析成功但检索为空”的问题时才发现如果不把元数据、对象存储、检索、缓存这四层的分工搞明白排错根本无从下手。这篇文章把拆解 RAGFlow 四层存储的思路完整梳理一遍会讲清楚每一层到底存了什么、为什么选这个组件、各层之间如何协作也会分享几个真实遇到的故障现场。适合刚接触 ragflow 的开发者也适合已经跑起来但想搞清内部数据流的运维同学。1.1 一套 RAG 系统里其实混着三类完全不同性质的数据先回答那个最基础的问题为什么不能像传统 web 应用一样把数据全塞进一个 MySQL因为 RAG 系统的数据按性质至少分成三类关系型/状态型数据知识库和文档的记录、解析状态、chunk 元信息、会话和消息。这类数据有强关联关系需要事务、外键、按条件筛选、频繁小更新。大文件二进制数据原始 PDF、Word、PPT、解析出的图片和版面结果。这类数据体积大、不会跑 SQL 查询只需要按 key 快速存取。检索型数据分块后的文本和对应的嵌入向量。这类数据需要全文检索、向量相似度计算以“召回 top-K”为目标。如果强行把这三类放一个库里要么 MySQL 存大字段把 buffer pool 撑爆要么搜索引擎去做事务导致索引频繁重建。RAGFlow 走的路线是典型的“专业组件干专业事”MySQL 管元数据、MinIO/S3 管对象文件、Elasticsearch 管检索、Redis 管缓存与临时状态。四套系统组合起来才构成一个完整的存储体系。1.2 一条问答请求横跨四个存储组件的过程先记住完整的数据流转用户在界面上传 PDFWeb 服务把原始文件写入对象存储层默认是 MinIO同时在 MySQL 中插入一条 document 记录状态为“解析中”。任务执行器拿到解析任务用版面解析引擎把 PDF 拆成文本块、表格和图片图片等中间产物再写回对象存储。每个文本块按知识库配置的分块策略切成 chunk生成向量连同文本一起写入 Elasticsearch一个知识库对应一个索引。解析完成MySQL 里的 document 状态更新为“完成”chunk 数量和 token 数写回。用户提问时问题文本被向量化ES 做向量检索加全文检索的混合召回拿到 top-K chunk。会话上下文、限流状态、消息记录中的高频部分由 Redis 承担最终结果落回 MySQL 的 message 表。这条链路里任何一个存储环节出问题对外表现都完全不同。用一个表格总结四层分工存储层核心组件存放内容数据特点典型操作元数据层MySQL知识库、文档、chunk 元信息、会话消息行式、强关系、频繁更新增删改查、事务对象存储层MinIO / 云 S3原始文件、解析中间产物大文件、不可变、按 key 读写PUT / GET / DELETE检索层Elasticsearchchunk 文本与向量倒排索引加向量字段混合检索、召回 top-K缓存层Redis会话、限流计数、任务信号临时状态、TTL 过期set / get / incr / pub-sub2. 元数据层文档状态、知识库关系这类“消息”为什么必须走 MySQL2.1 元数据层具体存了什么以我部署的 ragflow 版本为例MySQL 里核心数据大致包括知识库表记录 kb 的 id、名称、embedding 模型、chunk 配置、权限范围文档表记录文件名、文件类型、md5、解析状态、chunk 总数、token 总量、解析耗时、所属知识库chunk 元数据表记录 chunk 的 id、所属文档、序号、关键词、重要关键词会话与消息表记录 conversation 和 message还有任务表和用户表负责跟踪解析任务状态与 API 调用权限。你会发现这些数据几乎全是“小而频繁更新”的行记录文档解析状态从 parsing 变到 donechunk 数随着重分块不断变化会话消息不断追加。这些都需要行级事务保证一致性这正是关系型数据库的主场也是它和其他三层的本质区别。元数据层的设计原则是永远只存“关于数据的描述”不存数据的本体。2.2 元数据和对象存储、检索层之间的映射关系元数据层不是孤立存在的它相当于整个系统的“主索引”记录了另外三层的对应关系。document 表里有存储引用指向对象存储的某个 key知识库表里记录了使用的 embedding 模型这个模型决定 ES 索引里向量字段的维度chunk 元数据通过文档 id 与 ES 索引中的文档建立映射检索命中的 chunk 需要回表拿原文和业务信息。因此一旦 MySQL 数据丢失光有 MinIO 里的文件和 ES 里的向量是拼不回一个可用系统的。对象存储和检索层是“结果”元数据是“索引关系”。打个比方MySQL 像图书馆的检索目录对象存储像藏书库ES 像按内容主题编排的卡片柜Redis 是前台高频使用的热门书单。目录丢了书还在但你不知道哪本书在哪、哪本被谁借走所有读者就只能凭运气乱翻。3. 对象存储层MinIO 与 S3 协议背后的二进制文件归宿3.1 哪些文件会真正落进对象存储很多人以为“对象存储 存上传的文档”其实不止。RAGFlow 默认用 MinIO遵循 S3 协议落到这一层的内容主要有三类一是原始上传文件包括 PDF、Word、PPT、Excel、图片按文档 id 或 md5 生成对象 key二是解析中间产物包括版面分析输出、OCR 结果和页面切图部分版本里图片会单独存成对象供文档预览和图文问答使用三是知识库导出包和其他附件。为什么要用对象存储而不是直接放服务器磁盘最直接的理由是解耦与扩展解析 Worker 可以横向扩容多台机器共享一个 MinIO 就能一致地读写原始文件不需要做共享磁盘S3 协议意味着你可以无缝换成阿里云 OSS、腾讯 COS 等公有云对象存储服务。使用云上对象存储服务还有一个隐性好处上传下载都走 HTTP 协议和 Web API 天然契合CDN 和网关都好接。对于内容不可变、只按 key 读写的文件对象存储几乎是性价比最高的方案。3.2 从本地 MinIO 切换到云 OSS/COS 的配置差异本地测试用 MinIO 默认配置就能跑通但上生产时很多人会换成云 OSS。切换的核心是在 RAGFlow 的存储配置里设置 access_key、secret_key、bucket、endpoint 四个参数。其中有几个坑是我实际踩过的endpoint 一定要写对协议MinIO 常用http://minio:9000这类地址云 OSS 则是https://oss-cn-hangzhou.aliyuncs.com。漏掉协议前缀或把地域域名写错会直接报连接错误而且这个错误经常被误判为网络问题。内网与公网 endpoint 要分清RAGFlow 部署在云上时解析服务与 OSS 之间应使用内网 endpoint省流量且更快但 Web 服务给前端生成临时下载链接时要确认走外网 endpoint否则用户拿到的 URL 在内网环境根本访问不了。bucket 权限遵循最小化本地 MinIO 图省事可以给 full access key但云对象存储建议只授予该 bucket 的 PutObject、GetObject、DeleteObject 权限避免 key 泄露后影响同账号的其他资源。数据迁移别重新上传从本地 MinIO 切到云 OSS建议用兼容 S3 的同步工具做增量同步比如mc mirror可以直接把已有对象按 bucket 和前缀搬到云上避免几十 GB 文档重新解析和向量化。4. 检索层Elasticsearch 上向量检索与全文检索的协同方式4.1 为什么选 ES 而不是“专用向量数据库”聊到 RAG很多人第一反应是 Milvus、Qdrant、pgvector 这类专用向量库。但 RAGFlow 的检索层用的是 Elasticsearch理由其实很朴素真实知识库的检索需求从来不只是“向量相似度”。业务上经常要精确匹配某个专有名词、搜表格里的数字范围、按关键词权重过滤这些是纯向量检索不擅长的而 ES 天生就是全文搜索引擎从 7.x 开始又支持 dense_vector 字段和 kNN 检索等于一套系统同时提供 BM25 全文检索和向量检索。RAGFlow 在召回阶段做的是“混合检索”把全文分值和向量相似度按权重融合再取 top-K。这种融合如果拆成两套系统就要自己处理两个结果集的归并和权重平衡工程复杂度直接翻倍。从运维角度看ES 的成熟度、监控生态和故障恢复能力也比年轻的专用向量库完善。对大多数 GB 级知识库ES 完全够用只有到千万级 chunk、对延迟极度敏感的场景才需要考虑引入独立向量数据库。4.2 从文档入库到索引生效的完整过程检索层不是简单把向量丢进去就完事里面有一套明确流水线文档解析产出结构化文本去页眉页脚、表格转文本、图片单独抽离按知识库的 chunk 策略分块比如按标题层级、固定长度或 Q/A 对切分每个 chunk 通过 embedding 模型生成向量把 chunk 的文本、向量、文档 id、序号一起写入该知识库对应的 ES 索引然后更新 MySQL 的 chunk 计数和文档状态索引刷新后文档才能被检索到。这里有两个实际经验。一是refresh_interval会决定新写入数据多久可见批量处理几十上百个文件时建议调低刷新频率、分批提交向量避免大量小请求把 ES 打爆二是在你刚入库一个文件就想立刻验证效果时可以直接对索引执行_refresh不用干等。最容易被忽略的点ES 索引的 mapping 一旦建立vector 字段的维度就固定了后面换了 embedding 模型但维度不一样新数据写入会直接报 mapping 冲突这个坑到第 6 节再展开。4.3 混合检索的评分与相似度阈值RAGFlow 的检索参数里有几个很关键召回条数 top_k、相似度阈值、是否启用重排rerank。向量相似度和 BM25 的分值区间不同直接相加没有意义必须先做归一化常见做法是 max-min 归一化或 rank 融合。相似度阈值做的是过滤低于阈值的 chunk 哪怕排进 top-k 也会被丢弃这就是“宁可少给也不能给错”的取舍。我的调参经验是知识库内容专业、用户问题句式规范时阈值可以调低一点0.1 到 0.2多召回候选知识库碎片化、噪声多时阈值调高到 0.3 以上减少无关片段进 prompt。启用 rerank 模型后阈值的作用会减弱因为重排模型会在更细粒度上过滤无关 chunk此时建议把 top_k 调大比如 50让重排模型有更多候选可挑再取前 3 到 5 个送 LLM。这其实就是“语义检索”落地时的最后一关召回要宽但以什么标准收窄由这些参数决定。5. 缓存层Redis 在 RAGFlow 里缓存了什么又坚决不碰什么5.1 Redis 里的三类数据会话、限流与任务信号RAGFlow 的 Redis 不只是常见的“热点数据缓存”从我观察到的实际用途看至少承担四类工作。第一是会话上下文临时存取多轮对话时如果每轮都去 MySQL 翻全部历史消息延迟高且数据库压力大Redis 用 hash 或 list 结构暂存当前会话最近若干条消息设置 TTL 自动过期这就是典型的分布式缓存用法。第二是限流与配额API 请求的令牌桶、用户调用次数统计放在 Redis 里做原子自增多个 Web 实例共享同一份计数保证限流全局生效。第三是任务信号与事件通知文档解析任务的状态变更、客户端轮询任务进度借助 Redis 的发布订阅或临时 key 广播。第四是可能的 embedding 或 LLM 结果缓存相同问题在窗口内重复查询时直接返回缓存结果节省模型调用费用。这一块不同版本实现差异较大我会直接进 redis-cli 用KEYS看实际 key 分布来确认。Redis 坚决不碰的是两类数据文档原文和二进制文件那都是对象存储的活最终的 chunk 全量和向量是 ES 的活。如果你发现生产环境 Redis 内存异常暴涨首先要怀疑是不是有人往里面塞了 chunk 文本很多“缓存层 OOM”其实都是用法不对不是容量不够。5.2 缓存穿透在 RAG 场景下的实际表现与规避传统缓存穿透是查询一个不存在的 key每次都打到数据库。RAGFlow 里同样存在只是表现不太一样。比如用户频繁问知识库之外的问题如果系统对“无检索结果”也做缓存那么缓存了“空结果”后后续相同问题就不会再触发新的检索如果不对空结果做缓存每次都要走一遍完整检索链路。更常见的穿透是用户删除或重建知识库后Redis 里旧的会话缓存还指向旧知识库 id导致对话返回“知识库不存在”或旧内容这就是缓存与元数据层之间的失效联动没做好。我的规避习惯是会话类 key 一律带上知识库 id 和版本号删除知识库时用DEL或通配匹配删除相关 key对“无结果”这种空缓存设置很短的 TTL比如 30 秒既不穿库又能快速恢复。说白了“缓存失效”和“缓存一致”在 RAG 系统里的落地规则就是缓存必须跟着元数据层的事件走文档变了、库删了、模型换了缓存立刻失效而不是靠自然过期。6. 四层协作最容易出问题的三个现场6.1 现场一解析显示成功ES 检索永远是空第一次遇到这种事故用户上传一份 PDF界面显示解析完成但在对话框里怎么问都返回“没有找到相关内容”。我当时按这个顺序排查先看 MySQL 里 document 表的状态和 chunk 数发现 status 是 done、chunk_num 是 124说明解析和分块都完成了再看 ES 索引是否存在索引也在但查索引里的文档数时结果居然是 0。到这里基本锁定解析任务崩在了向量生成之后、提交 ES 之前。翻任务执行器日志发现是 embedding 接口超时重试几次后任务被标记完成但 ES 写入没成功。也就是说 MySQL 元数据认为“完成”检索层却是空的两层状态不一致。修复动作本身很简单把 document 状态改回 parsing让任务重跑ES 重新写入。但根子是 embedding 服务不稳定后续我给调用加了更长的超时和更合理的重试策略。这个案例最能说明四层协作的特点任何一层成功都不能代表整条链路成功排错时必须跨层对账。6.2 现场二删除知识库后对象存储空间越用越大另一次线上知识库反复重建跑了一周后发现磁盘占用比实际文档总量大出好几倍。查了一圈发现删除知识库时 MySQL 的元数据和 ES 的索引都删了但 MinIO 里的原始文件还留在桶里。原因是对象存储为了复用经常用 md5 做去重同一份文件可能被多个知识库引用删除某一个知识库时不能直接删除对象文件而本地 MinIO 又没有云对象存储那样的生命周期规则删除操作被跳过后就产生了孤儿对象。我后来的处理思路给对象存储配置生命周期规则只清理 multipart 残留和超过 N 天的临时对象不动正常文件再写一个对账脚本从 MySQL 的 document 表里把仍在引用的对象 key 拉出来用mc ls列出桶内全部 key两者的差集就是可删除的孤儿对象。先导出清单人工确认一遍再删。这个经验对云 OSS 同样适用别指望删除接口帮你做垃圾回收GC 要自己设计。6.3 现场三更换 embedding 模型后新老数据在同一个索引里打架某次为了提升中文效果我把知识库从 bge-base 换成 bge-large结果新上传的文档全部入不了库日志里报mapper_parsing_exception。原因很直接bge-base 系列向量维度是 768bge-large 是 1024ES 索引的 mapping 不允许同一个向量字段出现两种维度。换模型这个动作牵动了元数据层知识库配置、检索层ES 索引、缓存层可能残留旧向量结果三处。处理办法有两种。一是换模型后新建一个知识库让新库生成新索引老知识库保留旧模型继续服务这是最省事、零风险的二是想统一模型就把原知识库的文档全部删除重新上传或者写脚本重建索引但注意重建期间检索会短暂不可用建议在低峰期操作。连带还要把 Redis 里对应知识库的缓存 key 清掉否则会继续用旧模型时代的缓存结果。这个案例说明模型层变更不是改个配置就完事必须按“四层联动”的思路通盘考虑。7. 部署与运维中的容量、备份和迁移实践7.1 四层服务的资源规格参考按我的部署经验给一个内网私有化场景的资源参考规模MySQLMinIO / 对象存储ElasticsearchRedis演示 / 小库1 万 chunk 以内1C2G按文件量定磁盘2C4G堆 1 到 2G1C1G中等知识库50 万 chunk 以内2C4G500G 到 1T 磁盘4C8G堆 3 到 4G2C2G较大生产百万 chunk 以上4C8G独立对象存储集群8C16G 多节点4C4G注意 ES 的堆内存不要超过物理内存一半且不要超过 32G否则会踩到 JVM 压缩指针的坑。MinIO 的磁盘建议用独立数据盘避免系统盘被日志撑满。Redis 大部分情况 1 到 2G 就够除非你把整库会话缓存撑得很大。另外ES 的数据目录和 MinIO 的数据目录一定要分开监控这两个是最容易被文件撑爆的位置。7.2 备份与迁移的实操清单按重要性排序备份优先级应该是 MySQL 大于对象存储大于 ES 大于 Redis。元数据层丢了整个系统就“失忆”对象存储丢了文件可以重新上传ES 索引丢了大不了重新向量化但要花时间和模型调用费用Redis 本身是可重建的缓存。我常用的方案MySQL 每天凌晨mysqldump全量备份保留最近 7 天对象存储目录用mc mirror --watch增量同步到另一台机器或云存储配合对象版本控制ES 用 snapshot API 把索引快照备份到存储目录数据量小就直接对数据目录做文件级快照Redis 开启 AOF 持久化保证崩溃时能恢复到分钟级。迁移到新机器时正确顺序是先起好四层服务且版本保持一致再恢复 MySQL然后校验对象存储的 key 路径是否一致RAGFlow 内部存的路径如果带主机名或 IP 会出问题最后恢复 ES 索引并重启 Web 服务。如果只是换机器没换版本一般不需要重新向量化耗时主要取决于对象存储数据的拷贝速度。最后再补一句个人体会RAGFlow 的四层存储看起来是四套独立系统但排错时一定要把它们当成一个整体。先问自己“这条数据现在应该在哪一层”再判断“它有没有真的到那一层”远比盲目重启容器有效。希望这篇拆解能帮你少走点弯路。