news 2026/9/26 12:08:01

Docker双实例与Nginx平滑切换:Ubuntu下RagFlow不停机升级实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker双实例与Nginx平滑切换:Ubuntu下RagFlow不停机升级实践

从“夜里升级翻车”到“白天也能安心切”:Ubuntu下Docker双实例平滑升级RagFlow

先说一个我踩过的坑:某次给公司知识库升级RagFlow,按官方最常规的流程操作——拉最新代码、改配置文件、docker compose up -d,结果我这边命令刚执行完,同事就在群里说知识库连不上了,紧接着又有人反馈“正在解析的那份合同文档白搞了”。我当场意识到,这种部署方式天生带有一个空窗期:旧容器停止、新容器创建的间隙,服务是断的,而且RagFlow启动并不是秒级,实际上要等好几分钟。后来我把升级方案改成“双实例加Nginx平滑切换”,才真正做到了白天也能升级、用户无感知。

这篇文章就是针对Ubuntu服务器上Docker部署的RagFlow,完整记录我验证过的不停机升级方案。适合负责RagFlow运维、企业知识库/Agent应用部署的同学参考,尤其是那些“知识库不能断、文档解析不能丢”的生产环境。

1. 为什么RagFlow的常规升级一定会打断服务

1.1 官方默认升级流程的“隐藏停机窗口”

RagFlow官方文档里推荐的升级方式说白了就是两步:先git pull拉最新代码,再docker compose up -d让服务按新配置重建。听起来没什么问题,但如果你深挖Docker Compose的执行逻辑就会发现,up -d并不是“原地热替换”,它的实际动作是:检测到镜像或配置变化后,先停止旧的容器,再用新配置创建并启动一个新容器。这里的关键矛盾是端口,RagFlow默认把宿主机的9380端口映射到容器内的HTTP服务上,同一时间只能有一个进程监听这个端口,所以新容器必须等旧容器完全停止并释放端口,才有办法启动。

这个“先停后起”的间隙就是停机窗口。窗口本身可能只有几秒,但那只是“容器状态”层面的几秒。真正的不可用时间远比这个长,因为Docker把容器标记为“Up”不意味着RagFlow应用已经就绪。我刚接手RagFlow部署的时候,就因为只看了docker compose ps里状态变成 Up 就宣布升级完成,结果前端一直转圈,查了半天才发现应用内部还没初始化完。

1.2 RagFlow应用启动链路比你想的慢得多

RagFlow服务端启动不是一个简单的Python进程监听端口。它启动时要依次完成这些事:连接MySQL并校验元数据、连接Redis、连接Elasticsearch并校验/初始化索引、连MinIO对象存储、加载配置的Embedding模型(本地模型或在线模型),最后才是HTTP服务开始监听。任何一个依赖出问题,容器都会进入CrashLoopBackOff。

我实测过一组数据:在一台4C16G的Ubuntu服务器上,RagFlow容器从创建到健康检查通过,最快也要1分钟,最慢的一次接近4分钟——那次是因为Embedding模型要从远端拉取,网络抖动导致模型下载重试。也就是说,官方默认升级方式的真实不可用时长,不是命令执行的那几秒,而是“建容器+初始化依赖+加载模型”的完整链路。对外提供的知识库一旦断这么久,基本就是事故。

1.3 端口独占让“原地重启”这条路走不通

很多人会想:那我不重建容器,直接docker restart行不行?不行。restart只是重启同一个容器的进程,镜像和版本没变,谈不上“升级”。想要换版本,就必然要经历“旧容器退出-新容器绑定端口”的过程。除非你能接受换端口对外提供服务,否则只要入口还指向9380,就必须有一个瞬间是没人监听的。

所以结论很清晰:想做到“不停机”,不是把重启动作变快,而是要把版本切换动作从用户请求链路上摘除,让请求始终有一个可用的后端在响应,用户感知不到后端在换人。

2. 不靠运气的不停机方案:双实例加反向代理

2.1 核心思路:让新旧两个版本同时存在,用网关切流量

我的做法是在RagFlow前面加一层Nginx作为统一入口,宿主机上同时跑两套RagFlow实例:旧实例继续监听9380端口处理生产流量,新实例监听9390端口处于“待命”状态。日常Nginx把请求全部转发给旧实例;等新实例验证完毕,我修改Nginx的upstream配置把流量平滑切到新实例。

可能有人会问:这不就是蓝绿部署吗?对,就是蓝绿部署,只不过是在单机Docker Compose环境下的轻量实现。蓝绿部署看起来“土”,但对RagFlow这种单体Web应用反而是最合适的。它不依赖Kubernetes的滚动更新能力,也不需要额外搭建服务发现组件,一台Ubuntu服务器加上Nginx就能完成。

整个链路可以描述为:

用户请求 -> Nginx(:80/443) -> 旧实例 127.0.0.1:9380 -> 新实例 127.0.0.1:9390

Nginx的reload是平滑重载,执行瞬间不中断现有TCP连接,已经进入旧Worker的请求会在旧Worker上正常走完,新请求则由新Worker按新配置处理。对RagFlow这种“网页+HTTP API”的短连接形态来说,平滑程度足够。

2.2 数据层怎么处理:共享还是隔离

双实例方案里最核心的技术决策不是Nginx配置,而是“新实例的数据层怎么接”。我在这上面纠结过很久,实践下来可以给出一个决策表:

升级场景数据层策略理由
小版本升级,官方release notes未提及数据库结构变更共享现有MySQL/Redis/MinIO/ES成本最低,数据实时一致,切换后无数据同步问题
大版本升级,官方说明涉及数据库迁移或索引变更完全隔离,新实例独立数据卷避免新版迁移逻辑污染生产数据,切换失败可直接切回
无法确认兼容性,或生产环境数据极其重要优先隔离,先导数据验证再切用资源换安全,回滚容错率最高

共享数据层的连接方式其实不复杂。RagFlow官方docker compose里的服务名是mysql、redis、minio、elasticsearch,这些名字在Docker自定义网络里就是DNS主机名。要让新实例的server容器直接用这些名字连上生产依赖,只需要把新实例的compose网络指向生产环境的默认网络,网络名通常是<项目目录名>_default,比如/opt/ragflow/docker这个目录启动的项目网络就叫ragflow_default。

隔离数据层则是新起一套完整的compose,用-p ragflow-green指定独立项目名,这样数据卷、网络都和生产分开,切换前做备份导入。缺点是要准备两套资源,而且数据导入也需要时间,我这里给一个真实的成本参考:我迁移过约60GB的MinIO对象存储、几百MB的MySQL库,大概用了20多分钟。这个时间窗口对于升级来说可以接受,但不适合频繁切换。

2.3 资源开销到底要多少

单机跑两套RagFlow,很多人第一反应是“服务器扛得住吗”。我梳理一下各组件的大致内存占用:ragflow-server容器本身吃2-4G,Elasticsearch吃1-2G,MySQL加Redis加MinIO加起来2G左右,一套全栈下来大概6-8G。如果采用共享数据层,多跑一个新server容器,额外增加2-4G内存就够了;如果采用隔离方案,那就是两份全栈,约12-16G起步。

所以我的经验是:16G内存的Ubuntu服务器是“共享数据层升级”的舒适线,8G会有点紧张但也不是不能用,只是新实例启动和文档解析并发时要注意观察。磁盘方面,新实例的数据卷需要预留空间,建议至少留出当前数据量1.5倍的空闲容量,这样MinIO、ES快照备份和迁移都有地方放。

坦诚说,不停机升级不是免费的,本质是用富余资源换取业务连续性。如果服务器内存只有8G,我建议优先考虑隔离数据层方案里的“停旧起新”变体,把停机时间压缩到分钟级,而不是硬撑双实例导致整机OOM。

3. 在Ubuntu上完整执行不停机升级的实操过程

3.1 第零步:升级前把备份做扎实

无论采取共享还是隔离方案,备份都是必经步骤。这不是求心安,而是给回滚埋锚点。我在升级前固定执行三件事:备份MySQL、备份关键数据卷、保存当前compose配置快照。

MySQL备份用mysqldump,注意不要影响线上写入,加--single-transaction参数即可:

cd /opt/ragflow/docker docker compose exec -T mysql sh -c \ 'exec mysqldump -uroot -p"$MYSQL_PASSWORD" --single-transaction ragflow' \ > /backup/ragflow_mysql_$(date +%F).sql

数据卷备份需要先确认卷名。不同版本的RagFlow compose对数据卷的命名略有差异,执行docker volume ls | grep ragflow看一下实际的卷名,然后逐个打包:

docker run --rm -v ragflow_minio-data:/data -v /backup:/backup \ alpine tar czf /backup/minio-data_$(date +%F).tgz -C /data . docker run --rm -v ragflow_esdata:/data -v /backup:/backup \ alpine tar czf /backup/esdata_$(date +%F).tgz -C /data .

MinIO里的原始文件和ES索引在备份时可能有少量写入,这一点不用太纠结,我们回滚真正依赖的是MySQL dump加MinIO文件,ES索引如果真丢了,最坏情况下重新解析一遍文档也能重建,虽然代价不小,但至少有退路。

3.2 搭第二套实例:共享数据层的最小化操作

如果你决定采用共享数据层方案,操作重点是把新版server独立拉起来,同时避免和生产服务的端口、容器名冲突。先把官方docker目录复制一份:

cp -a /opt/ragflow/docker /opt/ragflow-green cd /opt/ragflow-green

在生产目录里拉取新版本镜像。这里注意,不要用latest这种浮动标签,必须锁死版本号:

docker pull infiniflow/ragflow:v0.18.0

以你实际要升级的release tag为准,我这里只是举例。拉完镜像后,编辑/opt/ragflow-green/docker-compose.yml,做三处修改:

  • 把ragflow-server的镜像tag改成目标版本,比如v0.18.0;
  • 把端口9380:80改为9390:80,避免与生产冲突;
  • 删除mysql、redis、minio、elasticsearch这些服务定义,只保留ragflow-server,因为我们共享生产的数据层。

然后修改网络部分,让新server加入生产网络:

services: ragflow-server: image: infiniflow/ragflow:v0.18.0 container_name: ragflow-server-green ports: - "9390:80" networks: - default networks: default: external: name: ragflow_default

里面的ragflow_default要换成你生产项目实际的网络名,不确定就执行docker network ls查。启动新实例:

docker compose -p ragflow-green up -d

-p指定独立项目名,即使你在/opt/ragflow-green目录下操作,数据卷和容器名也会和生产的ragflow项目区分开。

3.3 启动后等健康检查,别看着Up就切

新实例启动后不能马上切流量,因为容器状态Up不代表RagFlow可用。正确做法是盯着日志看启动进度:

docker compose -p ragflow-green logs -f ragflow-server

等日志里出现HTTP服务监听的标志(通常是Running on或Uvicorn running一类),再验证健康检查接口。RagFlow的健康检查路径是/v1/health,如果拿不准,直接看官方compose里healthcheck用的路径,照抄过来:

curl -s http://127.0.0.1:9390/v1/health

返回的内容里包含healthy类似字样才算通过。但通过健康检查只代表服务活着,功能层面还需要进一步验证:打开新实例的Web界面,确认知识库列表、文档列表和你生产环境一致,上传一个小文件触发解析,跑一次对话测试。这一步本质是“用生产数据做冒烟测试”,非常关键,它能在切换前把大部分兼容性问题暴露出来。

我个人习惯让新实例带压运行10到30分钟再切。这段时间里反复看docker compose -p ragflow-green logs有没有ES索引告警、Redis连接告警、MinIO权限告警。这些日志虽然不致命,但往往是升级后被用户吐槽的隐患。

3.4 Nginx上线与灰度切换

Nginx这里我建议单独跑一个容器,或者用宿主机Nginx,关键是配置必须清晰。下面是我生产环境验证过的配置模板:

upstream ragflow_backend { server 127.0.0.1:9380 weight=100; server 127.0.0.1:9390 weight=0; } server { listen 80; server_name kb.example.com; client_max_body_size 200m; location / { proxy_pass http://ragflow_backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_buffering off; proxy_read_timeout 300s; } }

有四个点值得展开说:client_max_body_size默认只有1m,知识库上传的PDF、Word文档随便就几十MB,不调大必然上传失败;proxy_buffering off是给RagFlow的流式对话响应准备的,不关闭缓存的话,用户看到的回答会一截一截地卡顿;proxy_read_timeout要调长,RAG场景下一步检索加生成往往要几十秒,默认60秒很容易超时;proxy_set_header里的Upgrade和Connection是给WebSocket能力预留的,RagFlow有些交互需要用到。

切换流程我分两步走。第一步先放一小部分流量到新实例试水:

upstream ragflow_backend { server 127.0.0.1:9380 weight=99; server 127.0.0.1:9390 weight=1; }

保存配置后执行docker exec nginx nginx -s reload,Nginx会平滑重载。在1%流量下观察新实例的日志和Nginx access log,如果没有异常,第二步再全量切换。全量切换时有个细节:把旧实例标记为down,而不是单纯把weight改成0,这样已建立的连接会自然结束,新连接全部走新实例:

upstream ragflow_backend { server 127.0.0.1:9380 down; server 127.0.0.1:9390; }

再reload一次,这时候对外入口已经完全在新版本上了。

3.5 切换后不要急着“庆祝”,先观察真实请求

全量切换之后,我一般会让流量跑5到10分钟再下结论。观察三个地方:Nginx access log里9390的响应码有没有大量4xx/5xx;新实例日志有没有异常堆栈;MySQL和ES的慢查询/连接数有没有异常上升。如果这十分钟一切平稳,升级才算完成一半,接下来是回滚预案的保留和旧实例的善后。

4. 验证、灰度与回滚:最容易翻车的地方全在这

4.1 别只看健康检查,业务路径要逐个过

切换后最容易犯的错就是“看到health接口正常就宣布成功”。RagFlow的业务链路长,health只是门槛。我整理过一个验证清单,每条基本就是点几下页面的事:

  • 上传一个测试文档,确认能触发解析,并能在文档列表看到解析状态;
  • 跑一次知识库对话,确认能命中文档内容并流式返回答案;
  • 打开包含图片/附件的文档记录,确认MinIO的访问URL正常,图片能预览;
  • 如果你们用API对接外部系统,调一次/v1/chats之类的接口,确认鉴权和响应体格式没变化。

特别是MinIO这条,新实例的环境变量如果和生产不一致(比如MINIO_ENDPOINT指向了错误的地址,或者bucket权限配置有出入),前端文档预览会出现图片裂开、附件无法下载的问题,这种问题健康检查根本看不出来。

4.2 回滚不是删旧重来,而是切回去

共享数据层方案最大的优势是回滚动作非常轻:只要旧实例的容器和数据卷还在,回滚就是改Nginx的upstream,把流量从9390切回9380,再reload一次。

但这里有一个必须提前确认的前提:新版RagFlow启动时会不会对MySQL schema做自动迁移。如果官方release notes明确说了数据库结构变更,或者你启动新实例时发现日志里有DDL操作,那回滚到旧版本就可能失败——旧代码连不上新schema,报字段不存在或类型不匹配的错。这种情况就不能用共享数据层,必须走隔离数据层方案,在隔离环境里先把迁移脚本验证清楚再切。

我在这上面吃过亏。有一次升级后续版本,旧实例上还有定时任务在写数据,新实例启动时迁移了表结构,导致两边并发写同一张表,最后表数据出了问题。后来恢复的方式是拿升级前的MySQL dump直接整库还原,再手动切回旧实例,这个过程中服务是真停了一段时间。所以请大家记住:共享数据层的“优雅回滚”是有前提的,前提不满足就不要硬上。

4.3 灰度比例怎么定才合理

有朋友问我,Nginx按5%、10%这样渐进地切流量行不行。我的实际体验是:对RagFlow这种应用,“5%的流量试新版本”听起来稳妥,实际意义没有想象中大,因为5%的流量未必会打到你没验证过的功能路径上。用户可能5%的请求都集中在登录和聊天,而你最担心的文档解析恰好没被覆盖到。

所以我更推荐“先19%跑半小时,再100%切换”的两段式,而不是多次小比例切换。19%的流量足够触发一次真实的文档上传和对话请求,再加上我在切换前已经用生产数据做过业务冒烟测试,两个动作叠加,覆盖度是够的。反复多次切换反而会增加状态不一致的风险,尤其是在共享数据层下,新旧两个server同时处理写请求,对数据库的压力虽然不大,但没必要去制造这种不必要的并发窗口。

4.4 旧实例的清理时机与数据卷保留

新实例稳定运行24小时之后,可以清理旧容器了。我的建议是先停后删,不要立刻删数据卷:

docker stop ragflow-server-old docker rm ragflow-server-old

数据卷继续保留一周再决定是否释放。这一周里如果用户反馈“我上周传的文档打开有问题”“这个解析结果和之前不一样”,你还来得及把旧实例重新拉起来对照。保留旧数据卷的成本只是磁盘占用,相比数据丢失后的代价,这点成本非常划算。

最后分享一个实战细节

这套流程跑顺之后,我最大的体会是“不停机”不是靠某一条命令实现的,而是靠流程设计。Nginx前置、双实例、灰度切换、回滚预案,每一步都是提前设计好的,升级执行反而变成了一件没有惊喜的例行操作。

最后再分享一个我踩过的坑:自动化脚本里千万不要把docker compose up -d和nginx -s reload写成同一行直接执行。中间必须留出健康检查的等待逻辑,等/v1/health返回正常再执行reload。我之前图省事写过一个一键脚本,结果新实例数据库初始化没完成,Nginx已经切过去了,用户那边白屏了好几分钟。从那以后我把升级拆成两段:第一阶段“起新实例并验证”,第二阶段“切流量”,中间必须由人确认,慢即是快。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 12:07:54

srt-whiteboard-animation的7步工作流:从字幕文件到成片MP4的完整指南

srt-whiteboard-animation的7步工作流&#xff1a;从字幕文件到成片MP4的完整指南 【免费下载链接】srt-whiteboard-animation 将 SRT 字幕做成暖米黄纸张底的流式笔迹白板手绘动画 skill&#xff1a;mask 分区遮罩编排 stream 连续笔迹&#xff08;ink→color&#xff09;。 …

作者头像 李华
网站建设 2026/9/26 12:07:45

AI漫剧与AI电影后期渲染:智慧文旅沉浸式内容轻量化生产实战指南

1. 从一条政策信号说起&#xff1a;为什么"AI漫剧"突然成了文旅圈的热词前阵子跟几个做文旅数字化的老朋友吃饭&#xff0c;席间有人提到黑龙江那份关于智慧文旅的文件&#xff0c;重点圈了两个方向&#xff1a;一个是沉浸式体验新空间&#xff0c;另一个是AI漫剧制作…

作者头像 李华
网站建设 2026/9/26 12:07:24

3C连接器压装0.02mm公差控制全流程解析

做3C连接器压装的朋友&#xff0c;应该都有这种体会&#xff1a;0.02mm这个数字放在图纸上就是一行技术条件&#xff0c;落到产线上却是一道能让人翻来覆去折腾好久的坎。连接器、压装、公差这三个词绑在一起&#xff0c;就意味着你既要管得住来料尺寸&#xff0c;又要压得稳设…

作者头像 李华
网站建设 2026/9/26 12:06:23

Python实战项目怎么练?108个项目按能力阶梯拆解与源码学习指南

1. 为什么“收藏了108个项目”的人&#xff0c;最后往往一个都没跑通我见过太多人硬盘里躺着几十个G的“Python实战项目合集”&#xff0c;文件夹命名从“入门”到“进阶”再到“大厂真题”&#xff0c;结果打开率不到百分之五。问题不在项目本身&#xff0c;而在于项目清单和练…

作者头像 李华
网站建设 2026/9/26 12:05:36

设置EditText光标颜色:从 colorAccent 到 textCursorDrawable 的完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华