blog-edge-ops-platform

用 Go 给边缘 AI 一体机做运维管理平台:一周实战踩坑与优化

背景

我们在边缘 AI 一体机上跑着二十多个 Docker 服务------基础设施(数据库、缓存、搜索引擎、对象存储等)、大模型推理服务、OCR/向量化/重排序模型、文档解析与语音识别,以及上层的应用系统。这些服务之间有严格的启动依赖:数据库要先于应用、大模型要先于下游的 OCR、重排序要先于文档解析。

最早这套环境用 shell 脚本启动,逻辑很简单------按顺序 docker compose up,每步之间 sleep 一段固定时间(总共 18 分钟)。问题显而易见:不管服务实际多久就绪,都得睡满;某个服务没起来,脚本也不知道,照样往下走。

后来我们用 Go 写了一个单二进制的运维管理平台,核心是一个编排引擎 :按 depends_on 做拓扑排序,同层并行启动,每个组 up 之后轮询健康检查,就绪才启动下游。配一个 Vue3 的 Web 界面,能看到实时状态、操作日志、容器日志、操作历史。

这套东西跑了一阵子,本周集中做了一批优化。下面挑几个值得记的。


一、挪 compose 文件,容器得重建

现象

我们最初把 compose 文件放在两个目录:基础设施和应用系统放一个目录,AI 模型相关放另一个目录。后来为了统一管理,把所有 compose 文件挪到同一个目录下。

挪完之后,界面上点「停止」某个 AI 模型组,日志显示成功,但容器还在跑------假成功 。点「启动」则报 container name already in use。

原因

Docker compose 的 project name 来自 compose 文件所在目录的目录名 。容器创建时,这个 project name 会写入容器的 com.docker.compose.project 标签,之后无法修改。

当 compose 文件换了目录,project name 变了,compose 命令就认为这些容器不属于自己管了:

  • stop / restart → 按新 project 名找不到容器,退出码 0,什么都不做。界面收到 0 以为成功了。
  • up -d → 想在新 project 下创建同名容器,但旧容器还占着名字,冲突。

解法

没有任何捷径------先 down 旧 project 的容器(或直接 docker rm -f),再从新目录 up -d 重建。只改配置里的路径,不重建容器,是没用的。

这一点看着简单,但排查时容易误判:日志显示成功、退出码 0,你会以为是别的问题(服务名对不上?权限?网络?),实际是 project 标签对不上。

教训:compose 的 project name 是「写死在容器标签上」的,不是运行时动态算的。文件换目录 = project 换名 = 容器归属变了。


二、编排引擎从「按层等齐」改成「依赖驱动」

现象

我们的依赖图大概长这样:

复制代码
infrastructure(9 个服务,约 30 秒就绪)
    ├── applications(15 个 Java 服务,只依赖 infrastructure)
    └── llm(大模型,加载模型约 9 分钟)
            └── ocr_embedding → rerank + funasr → doc_parse

按设计,applications 只依赖 infrastructure,应该 infrastructure 就绪后立刻启动,不等大模型。但实际日志里,infrastructure 30 秒就绪后,applications 迟迟不启动,一直等到 llm 加载完(9 分钟后)才开始。

原因

引擎的 startGroups 是按层处理 的:找出当前所有依赖已满足的组("就绪层"),并行启动它们,然后 wg.Wait() 等这一层全部完成,再进入下一层。

复制代码
Layer 0: infrastructure + llm → 两个一起启动
  infrastructure 30 秒完事
  llm 要 9 分钟
  wg.Wait() 卡在这里
  → applications 虽然依赖已满足,但开不了

本质上,层调度把无依赖关系的组强行同步了。infrastructure 和 llm 没有依赖关系(可以并行),但 applications 和 ocr_embedding 分别依赖它们中的一个,却被层调度逼着等慢的那个。

解法

把调度模型从「按层」改成「依赖驱动 」:每个组一个 goroutine,只等自己的 depends_on 对应的信号,不等同层。

具体来说,给每个组建一个 done channel,组的 goroutine 先阻塞在「等自己所有依赖的 channel 关闭」,依赖都就绪后立刻启动自己,启动完关闭自己的 channel。主 goroutine 等所有组的 goroutine 结束。

改完之后:

复制代码
0:00  infrastructure + llm 同时启动
0:30  infrastructure 就绪 → applications 立刻启动(不等 llm)
~1:30 applications 15/15 就绪 → 执行组级回调
~9:00 llm 就绪 → ocr_embedding 启动 → rerank + funasr 并行 → doc_parse

总启动时间从约 15 分钟降到约 11 分钟,省下的 4 分钟就是 applications 不再等 llm。更重要的是,应用系统(用户实际用的东西)提前 8 分钟可用。

教训:编排引擎的调度粒度要从「层」下沉到「依赖」。层调度在「同层有快有慢」时必然浪费------快的那几个白等慢的。


三、组级 post_start:某组就绪后立即执行,不等别人

现象

我们有一个「全平台启动完成后清理 redis 登录 token」的逻辑------强制所有用户重新登录,避免重启后 token 状态不一致。这个命令原来挂在全局 post_start,意思是所有组都启动完才执行。

问题:所有组就绪要等大模型加载 9 分钟、OCR 5 分钟......redis 清 token 要 15 分钟后才跑。但这个命令只依赖应用系统组(redis 在 infrastructure 里,应用系统起来后 redis 一定在了),完全没必要等 AI 模型。

解法

给每个组加一个 post_start 字段,在这个组自己的健康检查通过后立刻执行,不等其他组。

redis 清 token 从全局 post_start 挪到 applications 组的 post_start 下。applications 约 1 分钟就绪后,立刻清 token,不用等大模型。

全局 post_start 保留,留给真正需要「全部就绪后才执行」的命令(目前留空)。

一个小细节:组级 post_start 在「单组启动」时也会跑(你手动点 applications 组的启动按钮,也会清 token),但在「单组重启」时不跑(重启走的是 compose restart,不经过启动流程)。这个行为我们觉得合理------重启一个组不应该把所有人踢下线,只有完整启动时才清。

教训:回调要跟着依赖走,不要挂全局。并行启动场景下,全局回调几乎总是太晚。


四、开机后认证中心没起来:加一个等待页

场景

我们的平台接了 SSO 单点登录。一体机开机后,systemd 拉起管理程序,管理程序自动调「全平台 restart」------先停所有服务,再按依赖重新启动。

问题:restart 过程中,认证中心(SSO 后端)也被停了再起。如果用户在这个窗口期访问平台首页,会被重定向到认证中心的授权 URL,但认证中心还没起来,浏览器看到一个 502 或连接拒绝------用户体验很差,也不知道该等多久。

解法

在登录跳转之前加一道探测:

  1. 用户访问 /,未登录 → 跳 /oauth/login
  2. /oauth/login 先探测认证中心是否可达(短超时 HTTP GET,返回 <500 认为就绪)
  3. 就绪 → 正常跳认证中心授权 URL
  4. 未就绪 → 跳 /oauth/wait(一个静态等待页)

等待页每 3 秒轮询 /oauth/ping,认证中心就绪后自动跳回 /oauth/login,进入正常登录流程。超过 10 分钟还没好,切换到超时态:spinner 变成警告图标,提示「请联系管理员检查服务状态」。

几个要点:

  • 等待页纯内联 HTML/CSS/JS,没有任何外部资源(边缘环境可能没外网,不能依赖 CDN)
  • 探测用独立的 3 秒短超时客户端,不和登录流程共用 15 秒超时,避免等待页卡顿
  • /oauth/ping 返回 {"ready": true/false},探测逻辑跟随后判断 ≥500(如 502)为未就绪------nginx 起来了但后端没起,返回 502,正确判定为未就绪

教训:SSO 场景下,认证中心本身也是被编排的服务,开机时它不一定先就绪。登录入口要能优雅等待,而不是把用户甩到一个 502 页面上。


五、vllm 服务的健康检查 start_period

现象

大模型推理服务(基于 vllm)启动后,健康检查一直报 connection reset by peer,页面红灯闪好几分钟,日志刷一堆失败记录。但过几分钟后服务就正常了,最终会变绿。

原因

vllm 加载模型期间,端口是开着的(容器起来了),但 vllm 进程还在加载权重到显存,此时对 /health 发请求会被 connection reset by peer------这是 vllm 的正常行为,不是故障。

我们的健康检查默认 start_period: 10s,意思是容器启动后 10 秒就开始探测。大模型加载要几分钟,前几分钟全是失败日志。

解法

给大模型类服务单独配 start_period: 180s(3 分钟),探测延后开始。同时调大 interval 和 retries,给够总容忍时间。

其他 vllm 服务(OCR、语音识别)同样的问题,也加上各自的 start_period。

关键认知:connection reset by peer 在 vllm 启动期间是正常的 ,健康检查报红不代表服务挂了。start_period 的作用就是告诉探测器「这段时间内的失败不算,别报」。

教训:不同类型的服务启动时间差异巨大,健康检查参数不能全用全局默认值。模型加载类服务必须配够 start_period,否则日志噪音和误报满天飞。


六、编排日志落盘

现象

原来编排引擎的日志(「启动服务组: 基础设施」「nginx: 检查 1/60 - HTTP 200」「后置命令执行成功」)只通过 SSE 实时推送给浏览器。没连浏览器的时候,这些日志直接丢弃 。journalctl 和日志文件里只有启动横幅,没有任何操作记录。

排查「上次全平台启动为什么失败」只能靠 history 表里那条 failed 记录的 message 字段(只存第一个错误),执行细节全丢。

解法

给引擎的 log 方法加一行 log.Printf,所有编排日志同时写两处:

  • SSE 实时推送(原有行为,浏览器可见)
  • log.Printf → systemd StandardOutput=append:日志文件(落盘)

格式 [组名] [级别] 消息,例如 [applications] [success] 组后置命令执行成功。

另外,pre_start/post_start 命令原来用 cmd.Run() 丢弃输出,改成 CombinedOutput() 捕获 stdout+stderr 并打印。这样能看到 redis 清 token 的实际返回值(如 (integer) 15 表示清了 15 个 key),而不是只知道「执行了」。

API 入口也加了日志:谁触发了什么操作(全平台启动/停止/重启、组级操作、容器级操作),记录操作人。

教训:SSE 是实时的好东西,但不能当持久化用。运维类系统必须有落盘日志,否则事后排查抓瞎。


七、其他小改进

单容器停止 stop → down :原来全平台/组级停止用 down(删容器),单容器停止用 stop(不删)。导致一个陷阱:组级停止后点「重启」会失败(restart 只能重启已存在的容器,容器被 down 删了)。统一改成 down,行为一致。

确认弹框显示中文名 :原来弹框显示内部标识(如 infrastructure、kkfileview),用户看不懂。改成显示中文 label(如「基础设施」「文件预览」)。接口调用仍然用内部名,只改展示。

页面展示顺序 = 配置顺序 :前端 v-for 按接口返回的顺序渲染,接口按 services.yaml 里 groups 列表的先后返回。想让某个组在页面上排前面,改配置里的顺序就行,不动代码、不动依赖。


小结

痛点 解法 核心认知
挪 compose 文件后假成功 先 down 再 up project name 写死在标签上
被同层慢组卡住 依赖驱动调度 调度粒度要下沉到依赖
回调要等全部完成 组级 post_start 回调跟着依赖走
开机 SSO 没起来 等待页 + 轮询 认证中心也是被编排的服务
vllm 前几分钟报红 start_period 配够 connection reset 是正常行为
日志不落盘 log.Printf 旁路一份 SSE 不是持久化

这周最大的心得就两条:

  1. 编排引擎的调度粒度要从「层」下沉到「依赖」。层调度在「同层有快有慢」时必然浪费------快的白等慢的。依赖驱动才是正确的模型。
  2. 回调要跟着依赖走,不要挂全局。并行启动场景下,全局回调几乎总是太晚。组级回调让「某组就绪后立即做事」成为可能。

都不是高深技术,但每一个都对应一个真实的运维痛点。边缘场景的特点就是------服务多、启动慢、依赖复杂、没有外网、出问题靠 SSH 看日志。把这些小细节打磨好,运维才不至于每次开机都提心吊胆。

相关推荐
AI人工智能+电脑小能手2 小时前
一次请求打几个下游,这个Java并行任务工具类6个方法按序聚合
java·线程池·并发编程·1024程序员节·批量处理·并行任务
CarIise2 小时前
JavaWeb 文件上传的四种实现方案
1024程序员节
长欢喜一场2 小时前
豆包:字节跳动AI助手的全方位透视
人工智能·搜索引擎·1024程序员节
4SAPI2 小时前
搭建 AI 数据分析工具大模型 API 选型指南
数据库·人工智能·架构·php·1024程序员节
KingOTC2 小时前
国产模块测评:基于国产芯片的实际表现
rfid·1024程序员节
小嘉丶学长2 小时前
Day04 · 程序入门
1024程序员节
Alsn862 小时前
Windows 安装 Ollama 及本地模型部署
学习·1024程序员节
霂鳯2 小时前
新手勇闯网络安全 | MySQL基础(二)
1024程序员节
俊哥V2 小时前
每日 AI 研究简报 · 2026-10-24
人工智能·ai·1024程序员节