边缘 AI 设备如何做到 7×24 不死机:Yuewell-Guardian 生产守护规范实战
越微智能(Yuewell)工业边缘 AI 工程实践系列 · 第 2 篇 关键词:systemd、生产守护、崩溃自动拉起、维护锁、Preflight、优雅关闭、OOM
一、nohup 跑不住生产环境的三个致命问题
很多团队做边缘设备,程序写好了,部署上去了,然后用 nohup ./app & 让它在后台跑。开发阶段看起来没问题,一到生产现场就翻车:
问题 1:崩溃了没人拉
现场设备跑在机房、电线杆上、垃圾站角落里,没人盯着。程序因为一个空指针、一次 OOM、一个驱动异常崩溃了,就再也起不来了。等客户发现"设备不工作了",可能已经过了好几天。
问题 2:开机不自启
现场停电、设备重启、系统更新后,程序不会自动启动。需要有人 SSH 上去手动 nohup 一下。对于部署在偏远位置的设备,这意味着一次上门服务的成本。
问题 3:维护时误操作
运维人员上去维护,stop 了程序做调试,调试完忘了 start。或者更糟——维护期间程序崩溃了,自动拉起机制把它拉起来,干扰了维护操作。
我们在早期项目中就踩过这些坑:一次现场设备因为推理引擎 OOM 崩溃,核心服务还在跑但连不上推理引擎,形成了"半栈状态"——管理面能连上但所有算法任务都失败,客户报障后我们花了半天时间才定位到是推理引擎挂了。
从那以后,我们意识到:边缘设备的生产守护,不是"加个 nohup"就能解决的,需要一套完整的守护规范。
二、systemd 守护架构:target 聚合 + 双服务依赖
Linux 的 systemd 是目前最成熟的服务守护方案。我们的边缘 AI 设备采用 target 聚合 + 双服务依赖 的架构:
multi-user.target(系统启动目标)
└── yw-avis-backend.target(边缘AI后端聚合目标,开机自启)
├── yw-algo-server.service(推理引擎,先启动)
│ └── 监听 50052 + 创建 UDS 套接字
└── yw-core.service(核心服务,后启动)
├── Requires: yw-algo-server.service(强依赖)
└── After: yw-algo-server.service(启动顺序)
2.1 为什么用 target 聚合
把两个服务归到一个 target 下,有三个好处:
- 统一启停:
systemctl start/stop/restart yw-avis-backend.target一条命令管理整个后端栈,不需要分别操作两个服务 - 开机自启:只需要
enable一个 target,两个服务都会开机自启 - 依赖管理:target 内的服务可以定义启动顺序和依赖关系,systemd 自动按顺序启动
2.2 启动顺序的硬性约束
核心服务(core)强依赖推理引擎(algo):
Requires=yw-algo-server.service:algo 启动失败,core 也不会启动After=yw-algo-server.service:algo 先启动,core 后启动
但这里有一个坑:After 只保证 algo 的 systemd 启动动作完成,不保证 algo 内部的 UDS 套接字已经创建好。algo 进程启动后,需要加载模型、初始化 NPU、创建 UDS 套接字,这个过程可能需要几秒钟。如果 core 在 algo 还没准备好时就启动,会因为连不上 UDS 而启动失败。
解决方案:core 的 ExecStartPre 增加一个 UDS 等待脚本,轮询 /tmp/yw_algo_fd.sock 是否存在,超时则启动失败。
三、崩溃自动拉起:Restart=on-failure 的坑
systemd 的 Restart=on-failure 是崩溃自动拉起的核心配置:进程异常退出(退出码非 0)时,systemd 会在 RestartSec 秒后自动重启。
但这里有几个容易踩的坑:
坑 1:OOM SIGKILL 不触发 on-failure
当进程因为内存不足被 Linux OOM Killer 强杀时,进程收到的是 SIGKILL(信号 9),退出码是 137(128+9)。
理论上 137 是非零退出码,应该触发 on-failure。但在实际场景中,OOM Killer 强杀进程时,systemd 可能无法正确捕获退出状态,或者进程被 cgroup 内存限制直接杀掉,导致 on-failure 不触发。
我们的解决方案是多层防护:
| 防护层 | 机制 | 覆盖场景 |
|---|---|---|
| 第 1 层 | Restart=on-failure + RestartSec=10 | 普通崩溃、异常退出 |
| 第 2 层 | 进程内内存压力软降级 | 内存达到 warn 阈值时,预览降帧、释放 JPEG 缓存,避免 OOM |
| 第 3 层 | cron 内存巡检脚本 | 每 5 分钟检查 RSS,超过 pressure 阈值时记录告警 |
| 第 4 层 | dmesg OOM 日志监控 | 检测到 OOM Killer 事件时,主动重启服务 |
坑 2:启动频率限制
systemd 默认有启动频率限制:如果一个服务在短时间内频繁重启(默认 10 秒内启动 5 次),systemd 会认为这个服务"启动失败",停止自动拉起。
对于边缘设备,如果程序有一个必现的启动崩溃 bug(比如配置文件损坏导致启动即退出),systemd 会在几次重试后放弃,设备就彻底挂了。
我们的处理:
StartLimitInterval=300(5 分钟窗口)- 启动失败时,Preflight 检查会先拦截明显的配置问题,避免进入"启动-崩溃-重启"的死循环
- 运维日志记录每次启动失败的原因,便于远程诊断
坑 3:正常停止 vs 崩溃的区分
Restart=on-failure 会在进程异常退出时重启,但运维人员执行 systemctl stop 时,进程是正常退出(退出码 0),不会触发重启。
这看起来没问题,但有一个边界场景:运维人员 stop 了服务做维护,维护期间服务因为某种原因异常退出了——这时候 on-failure 会把它拉起来,干扰维护操作。
这就引出了下一个机制:维护锁。
四、维护锁机制:防止维护时被自动拉起干扰
为了解决"维护期间被自动拉起干扰"的问题,我们设计了**维护锁(maintenance.lock)**机制:
| 操作 | 行为 | 锁文件 |
|---|---|---|
systemctl stop | ExecStopPost 自动创建维护锁 | ✅ 创建 |
| 进程崩溃 | 不经过 ExecStopPost | ❌ 无锁 |
systemctl start | ExecStartPre 自动删除维护锁 | ❌ 删除 |
核心逻辑:
- 正常
stop后,锁文件存在,服务保持停止状态,不会被自动拉起 - 崩溃时没有锁文件,
Restart=on-failure在 10 秒后自动重启 start时自动清锁,恢复自动拉起能力
锁文件是一个 JSON 格式的小文件,记录了停机时间、原因、操作的服务名:
{"event":"MAINTENANCE_LOCK","ts":"2026-06-08T07:59:54+0800","reason":"systemctl_stop","unit":"yw-algo-server"}
这个机制看起来简单,但在实际运维中非常有用——它让"计划内停机"和"意外崩溃"有了明确的区分,运维人员不需要担心维护时被自动拉起干扰,也不需要手动禁用/启用 systemd 服务。
五、启动前 Preflight Check:12 项不可越界的断言
边缘设备最糟糕的故障模式是"程序启动了但运行不正常"——进程在跑,端口在监听,但模型没加载、配置有问题、动态库缺失,导致所有业务功能都失败。这种故障比"程序启动失败"更难排查,因为表面看起来一切正常。
为了避免这种情况,我们在服务启动前增加了 Preflight Check(启动前预检),12 项检查全部通过才允许启动:
| # | 检查项 | 失败级别 | 说明 |
|---|---|---|---|
| 1 | 部署根目录存在 | FAIL | 目录结构完整性 |
| 2 | backend/ 目录存在 | FAIL | 后端目录完整性 |
| 3 | yw_core 二进制可执行 | FAIL | 文件存在且有执行权限 |
| 4 | yw_algo_server 二进制可执行 | FAIL | 文件存在且有执行权限 |
| 5 | configs/*.json 配置文件存在 | FAIL | 核心配置完整性 |
| 6 | models.toml 模型路由配置存在 | FAIL | 模型路由完整性 |
| 7 | models/ 目录存在且含 ≥1 个 .rknn | FAIL | 至少一个 AI 模型可用 |
| 8 | 关键动态库存在(librknnrt、librockchip_mpp、librga) | FAIL | 硬件加速库完整性 |
| 9 | data/、logs/ 目录可写 | FAIL | 运行时数据目录可写 |
| 10 | 磁盘可用空间 ≥ 500MB | FAIL | 避免磁盘满导致运行时崩溃 |
| 11 | 端口 50051/50052 未被非本进程占用 | FAIL | 避免端口冲突 |
| 12 | /tmp 可写(UDS 套接字创建) | WARN | 临时目录可写性 |
Preflight 的执行时机:ExecStartPre 阶段,在真正启动业务进程之前运行。任何一项 FAIL,启动脚本退出码非 0,systemd 认为启动失败,不会拉起业务进程。
Preflight 的日志:所有检查结果写入独立的运维日志(yw_ops.log),包含 PREFLIGHT_FAIL / PREFLIGHT_WARN / START_PRE_OK 事件,便于远程诊断"为什么设备启动不起来"。
这套 Preflight 机制把"启动了但不正常"的故障模式,变成了"启动失败且有明确日志"的可诊断模式,大幅降低了现场排障成本。
六、优雅关闭:先停 core 再停 algo 的时序设计
边缘设备的关闭不是"kill -9"那么简单。两个进程之间有 gRPC 长连接、UDS 零拷贝通道、正在进行的推理任务、SQLite 数据库写入操作。粗暴地强杀进程可能导致:
- 正在进行的推理任务中断,帧数据泄漏
- SQLite 数据库写入中断,数据损坏
- UDS 套接字文件残留,下次启动时地址已被占用
- gRPC 长连接的对端收到 RST,客户端异常
我们的优雅关闭时序设计:
systemctl stop yw-avis-backend.target
│
├─ 先停 yw-core.service(核心服务)
│ ├── SIGTERM 信号 → 进程注册的 handler 被触发
│ ├── 停止接受新的 gRPC 连接
│ ├── 等待当前调度任务完成(超时 60 秒)
│ ├── 关闭所有 RTSP 拉流会话
│ ├── SQLite WAL checkpoint + sqlite3_close
│ └── 进程正常退出
│
└─ 再停 yw-algo-server.service(推理引擎)
├── SIGTERM 信号 → handler 被触发
├── 等待当前推理任务完成(超时 30 秒)
├── 释放 NPU 上下文
├── 关闭 UDS 套接字
└── 进程正常退出
关键设计点:
-
停止顺序与启动顺序相反:启动时 algo 先 core 后,停止时 core 先 algo 后。因为 core 依赖 algo,如果先停 algo,core 会因为依赖消失而异常退出,不是优雅关闭。
-
超时 SIGKILL 兜底:
TimeoutStopSec设置了超时时间(core 60 秒,algo 30 秒)。如果进程在超时时间内没有正常退出,systemd 会发送 SIGKILL 强杀。这是兜底机制,避免进程卡死导致关机/重启挂住。 -
KillMode=mixed:主进程收到 SIGTERM,子进程在超时后收到 SIGKILL。这种模式允许主进程有机会优雅退出,同时保证子进程不会成为孤儿。
-
UDS 套接字清理:algo 退出时主动 unlink UDS 套接字文件。同时
ExecStopPost也会做一次兜底清理,避免套接字文件残留。
七、systemd 安装后常见告警的判读
设备上首次安装 systemd 守护后,journalctl 或 systemctl status 里经常会出现一些告警,运维人员看到"红色/黄色"就紧张,以为安装失败了。其实很多告警是正常的。
告警 1:mpp_platform: client 12/18 driver is not ready
来源:Rockchip MPP(视频硬解)SDK,在 core 初始化视频解码能力时打印。
含义:MPP 内部客户端类型编号 12/18,表示当时内核侧 MPP 设备节点尚未就绪或首次探测失败。
是否影响安装:完全不影响。这是业务层的告警,和 systemd 安装无关。
是否影响业务:在 RK3588 上这个提示常在启动瞬间出现,MPP 后续重试或走软解/降级路径后,系统仍可长期正常运行。只有当客户端预览长期黑屏、日志持续出现 no_mpp 时,才需要排查 MPP 驱动。
排查命令:
ls -l /dev/mpp_service # 检查 MPP 设备节点
grep 'no_mpp|PREVIEW_DECODE' logs/yw_core_*.log | tail -20
告警 2:Connection to tcp://192.168.1.32:8554 ... Connection refused
来源:core 按数据库中的相机列表拉 RTSP 流,对端 8554 端口拒绝连接。
含义:RTSP 服务(相机/NVR/流媒体服务器)未启动、相机离线、IP 变更或网络不通。
是否影响安装:完全不影响。这是业务层拉流告警,和 systemd 安装无关。
是否影响业务:仅影响该路相机的预览和巡检,其他在线相机不受影响。core 对 RTSP 失败有退避重试机制,不会因此崩溃。
排查命令:
timeout 2 bash -c 'echo >/dev/tcp/192.168.1.32/8554' && echo open || echo closed
grep 'RTSP 打开失败' logs/yw_core_*.log | tail -10
快速判断安装是否成功
不需要看告警,只看这三项:
systemctl is-active yw-algo-server yw-core # 均应输出 active
ss -tlnp | grep -E '50051|50052' # 两个端口都在监听
ls -l /tmp/yw_algo_fd.sock # UDS 套接字存在
三项全过 = 安装成功,业务告警另行排查。
八、nohup 与 systemd 的互斥保护
一个常见的运维事故是:设备上已经用 systemd 托管了服务,运维人员又用 nohup 或 run.sh start 手动启动了一份,导致两个实例同时运行,端口冲突、UDS 冲突、数据库写入冲突,设备彻底混乱。
为了防止这种情况,我们设计了互斥保护:
- systemd 安装时:自动停止所有 nohup 实例,创建
systemd_managed标记文件 - run.sh 启动时:检测到
systemd_managed标记且 target 已 enable 时,拒绝启动,输出提示:[backend] 错误: systemd 模式已启用,请使用 systemctl start yw-avis-backend.target - systemd 卸载时:删除
systemd_managed标记,恢复 nohup 模式
这个互斥保护确保了一台设备上永远只有一种守护模式在运行,避免双实例冲突。
九、越微自研:Yuewell-Guardian 生产守护规范
以上所有机制,我们沉淀为越微智能内部的 Yuewell-Guardian 生产守护规范,核心组件包括:
- target 聚合架构:双服务统一管理,开机自启
- 崩溃自动拉起:
Restart=on-failure+ 四层 OOM 防护 - 维护锁状态机:计划内停机 vs 意外崩溃的明确区分
- 12 项 Preflight 断言:启动前环境校验,失败即拒绝启动
- 优雅关闭时序:core 先 algo 后,超时 SIGKILL 兜底
- nohup 互斥保护:一台设备永远只有一种守护模式
- 运维事件日志体系:
yw_ops.log记录所有守护事件,便于远程诊断
这套规范让我们的边缘设备从"偶尔崩溃需要上门恢复",做到了"7×24 无人值守,崩溃 10 秒内自动恢复,维护时不被干扰"。在多个量产项目中,设备年度可用率达到了 99.9% 以上。
十、写在最后
边缘设备的生产守护,本质上是在回答一个问题:当没有人盯着设备的时候,它能不能自己好好活着?
nohup 只能回答"能跑",回答不了"崩了能自己恢复""开机能自己起来""维护时不被干扰""启动前知道自己正不正常"。这些问题的答案,需要一套完整的守护规范来支撑。
越微智能在 RK3588 边缘 AI 视觉设备的量产交付中,把这些踩过的坑沉淀成了 Yuewell-Guardian 生产守护规范,并集成到了我们的工业级边缘 AI 视觉基座(Yuewell Edge Framework)中。我们相信,生产守护能力是边缘 AI 产品从"能演示"到"能交付"的核心门槛。
如果你也在做边缘设备的生产化运维,欢迎交流。
关于越微智能(Yuewell)
上海越微信息技术有限公司是一支专注具身智能与工业 AI 视觉落地的技术团队,以自研 VLA 具身智能、视觉与语言大模型及 RK3588 边缘算力为底座,为工业与服务场景提供从算法、硬件到机器人集成的全栈交付。