RAGFlow 解析慢?并发锁与并发度调优实战
发布时间:2026/9/28 16:37:38 作者:尧图编辑部 阅读量:1,286

1. 从一次线上卡顿说起RAGFlow 解析为什么快不起来RAGFlow 的本地化部署很多人第一次跑通之后都会有一个共同的感受上传文档、点解析、然后就是等。等五分钟、等十分钟甚至更久。明明机器配置不差CPU 和内存都没跑满磁盘 IO 也没到瓶颈但解析队列就是慢吞吞地往前走一个几十页的 PDF 能卡上大半天。我这次遇到的场景也差不多——一套基于 Docker Compose 部署的 RAGFlow用来做内部知识库的文档解析文档量大概几百份格式混杂有纯文本、有扫描件、有带复杂表格的 PDF。用户反馈说“上传完半天没反应”我上去一看任务确实在跑但并发度低得可怜同一时间只有一个 chunk builder 在工作其余全在排队。这个问题表面上看是“解析慢”但真正的原因往往不在解析算法本身而在于并发锁和并发度配置这两个环节。RAGFlow 的解析流程大致是这样的文件上传后进入任务队列由 task executor 调度然后交给 chunk builder 去做分块和向量化。这里面有两个关键的环境变量——MAX_CONCURRENT_TASKS和MAX_CONCURRENT_CHUNK_BUILDERS它们分别控制任务级并发和分块级并发。如果这两个值配得不合理或者底层数据库的锁机制和它们打架就会出现“配置看起来没问题但实际并发上不去”的情况。这篇文章就是把我这次排查的完整过程记录下来包括怎么定位、怎么验证、怎么调参以及中间踩过的几个坑。适合已经在用 RAGFlow 做本地化部署、并且遇到解析速度瓶颈的同行参考刚接触 RAGFlow 的朋友也可以先了解一下它的解析调度逻辑后面调优的时候心里有数。2. 先把解析链路拆开看RAGFlow 的并发模型到底怎么跑的2.1 从上传到入库一条文档的完整旅程要搞清楚并发锁的问题得先知道 RAGFlow 把一份文档从上传到变成可检索的向量中间经历了哪些环节。我按自己的理解画了一条链路不涉及具体代码只说流程。文档上传之后RAGFlow 会先把它存到对象存储里默认是 MinIO这也是热词里经常被提到的“图片存放 minio 和存放到 ragflow”那个话题。然后数据库里会插入一条 task 记录状态是未开始。接着 task executor 会轮询这个队列拿到任务后开始解析。解析本身分几个阶段先是文件类型识别和内容抽取比如 PDF 用对应的解析器抽文本图片走 OCR然后是 chunk 切分把长文本切成适合向量化的片段最后是 embedding 和入库。其中 chunk 切分和 embedding 这两个阶段是最耗时的也是并发控制主要作用的地方。MAX_CONCURRENT_TASKS控制的是同时有多少个 task 在被 executor 处理而MAX_CONCURRENT_CHUNK_BUILDERS控制的是同时有多少个 chunk builder 在干活。你可以把它们理解成两层漏斗第一层决定有多少文档能同时进入解析流程第二层决定有多少分块任务能同时执行。如果第一层开得很大第二层很小那就会有一堆 task 卡在“等待 chunk builder”的状态反过来如果第二层开得很大第一层很小那 chunk builder 又吃不饱。所以这两个值需要配合着调不是随便改一个就行。2.2 并发锁在哪个环节出现数据库行锁与任务状态更新RAGFlow 用数据库来维护任务状态每次 task 状态变更、chunk 进度更新都会涉及数据库写操作。在并发场景下多个 executor 或 chunk builder 可能同时去更新同一条记录或者同时去抢同一个待处理任务。这时候数据库的行锁就会起作用。如果锁的粒度或者事务的持有时间不合理就会出现“一个任务在更新状态其他任务全在等锁”的情况表现出来就是并发度上不去。我这次遇到的现象是MAX_CONCURRENT_CHUNK_BUILDERS设成了 8但实际观察下来同一时间只有一个 chunk builder 在活跃其他的要么在等锁要么在空转。用docker compose logs -f看日志能看到大量类似“waiting for lock”或者“task status update retry”的记录。这就说明问题不在配置值本身而在于锁的竞争太激烈了。后来我把数据库的隔离级别和连接池参数一起看了下发现默认的连接池太小多个 chunk builder 抢几个连接自然就串行化了。这个后面会详细说。2.3 为什么 Docker Compose 部署下这个问题更常见用 Docker Compose 部署 RAGFlow 的时候默认的资源配置往往偏保守。比如 MySQL 或 PostgreSQL 的连接数、Redis 的超时时间、MinIO 的分片上传并发这些在单机环境下都容易成为瓶颈。而且 Compose 模式下所有服务跑在同一台机器上CPU 和内存是共享的数据库和解析服务互相抢资源锁的持有时间会被拉长进一步加剧竞争。热词里“docker compose 部署 nacos 3.x”“ubuntu 安装 docker compose”这些搜索量高说明很多人都在用 Compose 做本地化部署但很少有人专门去调数据库和并发相关的参数。我这次也是踩了这个坑一开始只盯着 RAGFlow 自己的环境变量忽略了底层数据库的连接池和锁等待超时。3. 排查过程实录从日志到数据库一步步定位锁竞争3.1 第一步确认并发配置有没有生效排查任何性能问题第一步都是确认“你以为的配置”和“实际生效的配置”是不是一回事。RAGFlow 的环境变量可以在.env文件里设置也可以在docker-compose.yml的environment段里覆盖。我先把容器里的实际环境变量打出来看docker compose exec ragflow-server env | grep -E MAX_CONCURRENT|LOCK|DB_结果发现MAX_CONCURRENT_CHUNK_BUILDERS确实是 8MAX_CONCURRENT_TASKS是 4。但日志里 chunk builder 的活跃数始终是 1。这说明配置读进去了但运行时没有按这个并发度去调度。接着我去看 task executor 的日志发现大量任务卡在pending状态而 chunk builder 的日志里频繁出现“acquire lock timeout”之类的信息。到这里基本可以确定是锁竞争问题而不是配置没生效。提示改完环境变量后一定要重启对应的容器RAGFlow 的 task executor 和 server 是分开的只重启 server 不一定能让 executor 重新加载配置。用docker compose restart ragflow-server或者直接docker compose up -d重建。3.2 第二步看数据库的锁等待和连接数确认配置生效后下一步就是看数据库层面到底发生了什么。我用的是 MySQL先查当前的连接数和活跃事务SHOW STATUS LIKE Threads_connected; SHOW STATUS LIKE Threads_running; SELECT * FROM information_schema.innodb_trx\G发现Threads_running经常在 1 到 2 之间而Threads_connected也不高说明连接池本身就不大。再看innodb_trx能看到有事务在等待行锁等待时间还不短。这就解释了为什么 chunk builder 并发上不去——多个 builder 线程去更新同一条 task 记录的状态数据库行锁导致它们串行执行。RAGFlow 默认的数据库连接池配置偏小在 Compose 部署下MySQL 的max_connections默认是 151但 RAGFlow 自己的连接池可能只开了几个多个 chunk builder 抢连接自然就排队了。3.3 第三步用最小化复现验证锁竞争为了确认是锁的问题而不是其他原因我做了一个最小化复现只上传一份文档把MAX_CONCURRENT_CHUNK_BUILDERS设成 1观察解析速度然后再设成 4观察速度变化。结果发现设成 1 的时候解析时间反而更稳定设成 4 的时候总时间没有明显缩短日志里的锁等待反而更多了。这个结果很反直觉但正好说明并发度提高之后锁竞争带来的开销抵消了并发带来的收益。后来我把数据库连接池调大再把事务的隔离级别从默认的REPEATABLE READ改成READ COMMITTED锁的持有时间明显缩短并发度才真正上来。4. 调参实操把并发度真正提上去的完整步骤4.1 调整 RAGFlow 自身的并发参数RAGFlow 的并发参数主要在.env文件里我按自己的机器配置做了一轮调整。机器是 8 核 16G跑 Docker Compose 全套服务。以下是我最终用的值供参考# .env MAX_CONCURRENT_TASKS6 MAX_CONCURRENT_CHUNK_BUILDERS12这里解释一下为什么这么设。MAX_CONCURRENT_TASKS控制的是同时处理的文档数设成 6 是因为我的文档平均大小不大6 个任务同时跑不会把内存吃满。MAX_CONCURRENT_CHUNK_BUILDERS设成 12是因为 chunk builder 主要是 IO 密集和轻量计算可以开得比 CPU 核数多一些。但这两个值不是越大越好后面会讲怎么根据实际负载调。注意MAX_CONCURRENT_CHUNK_BUILDERS设得太大会导致数据库连接不够用反而加剧锁竞争。建议先从 CPU 核数的 1.5 倍开始试观察日志里的锁等待情况再往上加。4.2 数据库连接池与锁等待超时的配套调整RAGFlow 的数据库连接池参数可以通过环境变量控制具体名称不同版本可能略有差异我这边用的是DB_MAX_CONNECTIONS和DB_POOL_TIMEOUT。把最大连接数从默认的 10 调到 30超时时间从 30 秒调到 60 秒。同时MySQL 服务端的innodb_lock_wait_timeout也从默认的 50 秒调到 120 秒避免因为锁等待超时导致任务失败重试。# docker-compose.yml 片段 services: mysql: command: --innodb-lock-wait-timeout120 --max-connections300 ragflow-server: environment: - DB_MAX_CONNECTIONS30 - DB_POOL_TIMEOUT60改完之后重启服务再用之前的 SQL 查Threads_running能看到并发活跃连接数上来了锁等待的时间也短了很多。4.3 验证调参效果用同一批文档做对比调参前后我用同一批 20 份文档做了对比测试每份文档大小在 2MB 到 10MB 之间格式包括 PDF、Word 和纯文本。测试结果如下指标调参前调参后总解析时间约 42 分钟约 14 分钟平均单文档耗时约 2.1 分钟约 0.7 分钟chunk builder 峰值活跃数19日志中锁等待次数高频偶发这个提升还是很明显的。需要注意的是调参后的第一次解析可能会稍慢因为数据库需要预热缓存也没建立起来。跑完一轮之后速度会稳定下来。5. 踩过的坑与常见问题速查5.1 改了环境变量但并发没变化这是最常见的问题。原因通常有三个一是改错了文件RAGFlow 的.env和docker-compose.yml里都可能定义环境变量后者优先级更高二是只重启了 server 没重启 executor三是容器里的环境变量被其他配置覆盖了。排查方法就是进容器env | grep MAX_CONCURRENT确认实际值。5.2 并发调高后任务反而更容易失败并发调高之后数据库连接不够用、锁等待超时、内存不足都可能导致任务失败。这时候不要继续往上加并发而是先看日志里的错误类型。如果是连接超时就调连接池如果是锁等待超时就调锁等待时间或者降低并发如果是 OOM那就得加内存或者降低并发。我自己的经验是并发度调到 CPU 核数的 2 倍左右就差不多了再往上收益递减。5.3 MinIO 和 RAGFlow 存储混用导致的额外延迟热词里有人问“图片存放 minio 和存放到 ragflow”的区别。RAGFlow 默认把原始文件和解析后的图片都存到 MinIO如果 MinIO 和 RAGFlow 跑在同一台机器上磁盘 IO 会互相影响。我的做法是把 MinIO 的数据目录挂到单独的磁盘上或者至少和数据库的数据目录分开。这样解析时的 IO 竞争会小很多间接也能提升并发效率。5.4 Docker Compose 下资源限制没放开Compose 默认不会给容器设置 CPU 和内存限制但有些系统上 Docker 的默认 cgroup 限制会影响并发性能。可以在docker-compose.yml里显式设置deploy.resources把 CPU 和内存给足。另外ulimit里的文件描述符数量也要检查并发高的时候打开的文件数会明显增加。services: ragflow-server: deploy: resources: limits: cpus: 6 memory: 8G ulimits: nofile: soft: 65536 hard: 655366. 几个容易被忽略的细节从锁竞争延伸出去的优化点6.1 事务隔离级别的选择MySQL 默认的REPEATABLE READ在并发更新时持有的锁范围更大改成READ COMMITTED可以明显减少锁竞争。RAGFlow 本身对事务隔离级别没有强依赖改完之后我实测下来没有出现数据不一致的问题。修改方法是在 MySQL 配置文件里加transaction-isolation READ-COMMITTED然后重启数据库。6.2 任务状态更新的批量提交RAGFlow 在解析过程中会频繁更新 task 和 chunk 的状态。如果每次更新都单独提交事务锁的持有和释放就会很频繁。我后来在数据库层面观察发现有些更新可以合并但 RAGFlow 本身没有提供这个配置项。一个折中的办法是适当降低状态更新的频率比如把进度上报的间隔调大但这个需要改代码不适合所有人。更实际的做法还是把连接池和锁等待调好让每次更新都能快速拿到锁并释放。6.3 日志级别与排查效率排查锁问题的时候把 RAGFlow 的日志级别调到 DEBUG 能看到更多细节但 DEBUG 日志本身也会增加 IO 负担影响并发性能。我的做法是排查阶段开 DEBUG定位到问题后调回 INFO。另外Docker Compose 的日志驱动默认是json-file日志量大时会占满磁盘建议设置max-size和max-file。services: ragflow-server: logging: driver: json-file options: max-size: 50m max-file: 36.4 和 Redis 的配合RAGFlow 用 Redis 做任务队列和缓存。如果 Redis 的连接数或者超时设置不合理也会间接影响并发。我这边把 Redis 的maxclients调到了 1000timeout设成 0 表示不主动断开空闲连接。这些参数在 Compose 部署下都可以通过 command 或者配置文件传入。7. 我个人的调参心得并发不是越高越好这次排查下来最大的体会是并发锁的问题表面上是“锁”实际上是“资源竞争”和“调度策略”的综合结果。单纯把MAX_CONCURRENT_CHUNK_BUILDERS调大如果不配套调数据库连接池和锁等待时间效果可能适得其反。我自己的调参顺序是先确认配置生效再看数据库连接和锁等待然后小步调整并发值每调一次都跑一批文档做对比。不要一次改好几个参数否则出了问题不知道是哪个引起的。另外不同版本的 RAGFlow 在并发实现上可能有差异我用的这个版本里 chunk builder 的锁粒度相对较粗后续版本可能会优化。如果你在调参过程中发现某个值改了没效果先去看看对应版本的 release note 或者源码里的默认值别盲目照搬网上的配置。最后再分享一个小技巧如果你不确定并发该设多少可以从MAX_CONCURRENT_CHUNK_BUILDERS4开始每次加 2观察日志里的锁等待和任务完成时间找到一个收益开始递减的拐点那个值就是你这台机器上的最优解。