上一篇我们分享了任务模型设计的踩坑经历——从四种格式混乱到参数化命令数组。这一篇我们深入到状态管理和实时监控——任务的生命周期怎么管理?机器人状态变化了怎么实时推送到前端?我们一开始用轮询,结果服务器被打崩了,后来改成事件驱动,负载降到十分之一,延迟从秒级降到毫秒级。本文分享这个过程中的踩坑和实战经验。
一、轮询的噩梦:50 台机器人把服务器打崩了
做机器人实时监控,最直接的想法就是「轮询」——前端每隔几秒向后端请求一次所有机器人的状态,拿到数据后刷新界面。
我们一开始就是这么做的,每 3 秒轮询一次。
机器人少的时候(5 台、10 台),完全没问题,页面也挺流畅。但有一次客户现场上线了 50 台机器人,同时有 10 个运维人员打开监控页面——服务器直接崩了。
算一笔账:
- 10 个页面 × 每 3 秒一次请求 = 每秒 3.3 次请求
- 每次请求查 50 台机器人的状态 = 数据库每秒查询 165 次
- 每次响应传输约 50KB 数据 = 每秒传输 165KB
- 加上每次请求的连接建立、鉴权、数据组装、JSON 序列化……
数据库连接池很快就满了,API 响应延迟从几十毫秒涨到几秒,最后直接超时。页面转圈转半天,数据也刷不出来。
更要命的是,大部分轮询请求都是「白问」——机器人状态大部分时间没变化,3 秒前和 3 秒后的数据一模一样。10 次请求里可能有 9 次返回的数据和上一次完全相同,纯粹浪费资源。
我们当时想:「把轮询间隔缩短到 1 秒,实时性不就好了?」——结果更惨,请求量翻 3 倍,服务器死得更快。
轮询模式本质上是「用资源换实时性」——想要更实时,就要更频繁地请求,就要消耗更多资源。这是一个不可调和的矛盾。
二、任务状态机:任务的「一生」要明确定义
在讲事件驱动之前,先说说任务状态机——因为事件驱动的前提是「状态变化是明确的、可追踪的」。
如果状态变化是模糊的、不可预测的,那事件驱动就无从谈起——你都不知道什么时候状态会变、变成什么,怎么推送事件?
我们把任务的生命周期定义成 5 个状态:
```
Pending(待执行)→ Executing(执行中)→ Step_Completed(步骤完成)→ Success(成功)
↘
Aborted(异常终止)
```
逐个解释:
Pending(待执行):任务已经创建,通过了校验,但还没分发给机器人。可能在等机器人空闲、等前置条件满足、等人工审批。这个状态下任务还没开始,可以取消或修改。
Executing(执行中):机器人已经收到任务,开始执行。这个状态下任务不能随意修改(改了可能导致执行混乱),但可以暂停或取消。
Step_Completed(步骤完成):任务中的某一个步骤完成了。这是一个「中间状态」——任务还没全部完成,但完成了一步。每次步骤完成都会触发一次事件,前端可以更新进度。如果还有后续步骤,回到 Executing 继续执行;如果是最后一步,进入 Success。
Success(成功):所有步骤都完成了,任务成功结束。这是「终态」——任务结束,不会再变化。
Aborted(异常终止):任务因为某种原因异常终止了。可能的原因:安全问题(机器人检测到危险)、超时(执行时间超过限制)、人工终止、依赖失败(前置任务失败)、机器人故障(离线或报错)。这也是「终态」。
状态机设计的几个原则
原则一:状态单向流转,不能回退
任务状态只能向前走,不能回退——Executing 不能回到 Pending,Success 不能回到 Executing。
为什么?因为状态回退会导致一堆问题:已经执行的步骤怎么算?回退后重新执行吗?已经上报的进度怎么处理?已经触发的后续动作怎么撤销?
单向流转让状态机简单、可预测、易调试。如果确实需要「重新执行」,那就创建一个新任务,而不是把旧任务的状态回退。
原则二:终态只有两个——Success 和 Aborted
任务最终只有两种结果:成功或失败(异常终止)。不要搞太多终态(PartialSuccess、Timeout、Cancelled 都算终态),那样会让状态机变复杂。不同的失败原因用 abort_reason 字段记录就行,不需要每个原因都搞一个状态。
原则三:每次状态转换都要触发事件
Pending → Executing,触发 Mission_Started 事件;
Executing → Step_Completed,触发 Step_Completed 事件;
Executing → Success,触发 Mission_Success 事件;
Executing → Aborted,触发 Mission_Aborted 事件。
每次状态转换都必须触发事件——这是事件驱动架构的基础。没有事件,前端和其他系统就不知道状态变了。
原则四:状态转换要有明确的触发条件
不是随便就能转换状态的,每个转换都要有明确的触发条件:
- Pending → Executing:机器人接单并开始执行
- Executing → Step_Completed:某一步骤执行完成
- Step_Completed → Executing:还有后续步骤,继续执行
- Step_Completed → Success:所有步骤完成
- Executing → Aborted:安全问题/超时/人工终止/依赖失败/机器人故障
明确触发条件,状态机才是可预测的,不会出现「莫名其妙状态就变了」的情况。
三、事件驱动架构:状态变化才推送
有了明确的状态机,我们就可以做事件驱动了。
事件驱动的思路和轮询正好相反:
- 轮询:前端主动问「现在怎么样了?」,不管有没有变化都问
- 事件驱动:后端在状态变化时主动告诉前端「出事了,你更新一下」,没有变化就不说话
```
机器人状态变化 → 触发事件 → Redis 缓冲 → 后端处理 → 推送到前端 → 前端局部刷新
```
Redis 事件缓冲层:解耦和削峰
事件从产生到被消费,中间我们加了一层 Redis 缓冲层。这层的作用是:
1. 解耦——事件生产者(网关、机器人端)和消费者(后端、前端)不需要知道对方的存在。生产者只管把事件发到 Redis,消费者只管从 Redis 收事件。
2. 削峰填谷——突发大量事件时(比如上班时间所有机器人同时启动),Redis 先存下来,后端按自己的处理速度慢慢消费,不会被瞬间打垮。
3. 可靠投递——事件存在 Redis 里,即使消费者挂了,重启后还能继续处理,不会丢事件。
4. 多消费者——同一个事件可以被多个消费者订阅(后端写日志、前端更新界面、统计系统更新数据),互不干扰。
我们用了 Redis 的两种模式:
- Redis Stream:用于后端处理关键事件(任务状态变化),支持持久化、ACK 确认、消费者组,保证事件不丢、不重复处理。
- Redis Pub/Sub:用于前端实时推送(遥测数据、状态变化),低延迟、即时推送,允许偶尔丢包(遥测数据丢一帧没关系)。
关键消息用 Stream 保证可靠,高频消息用 Pub/Sub 保证低延迟,各取所长。
前端静默刷新:只刷新变化的部分
事件推送到前端后,前端怎么处理?
传统的做法是「收到事件 → 重新请求所有数据 → 全页面重渲染」,但这样又回到了轮询的老路——浪费资源、刷新慢、页面闪烁。
事件驱动的前端应该做到「静默刷新」——收到事件后,只更新变化的那一个组件,不重新请求数据,不全页面重渲染。
实现思路很简单:
- 每个机器人卡片、每个任务条目都有唯一标识(
data-robot-id、data-mission-id) - 收到事件后,根据事件里的 ID 找到对应的 DOM 元素
- 只更新这个元素的文本和样式,不碰其他元素
- 事件里已经带了最新数据(新状态、新进度、新位置),直接用,不需要再发请求
比如收到「机器人 RB-001 状态从 idle 变成 running」的事件,前端只做:
```javascript
const el = document.querySelector('[data-robot-id="RB-001"] .status');
el.textContent = "运行中";
el.className = "status status-running";
```
就这么简单,毫秒级完成,不影响页面上其他 49 台机器人的显示。
静默刷新的好处:
- 快——局部 DOM 操作比全页面重渲染快得多,毫秒级完成
- 省——不发 HTTP 请求,不重新查数据库,不重新渲染整个页面,节省带宽、服务器算力、前端算力
- 稳——只更新变化的部分,不会因为全页面重渲染导致页面闪烁、滚动位置丢失、输入框内容丢失
- 用户体验好——用户看到的是「这个机器人的状态突然变了」,而不是「整个页面闪了一下然后变了」,体验更流畅自然
四、可靠性机制实战:幂等、重试、超时
事件驱动架构虽然好,但也有可靠性挑战——网络可能断、消息可能丢、处理可能失败。我们用了一套可靠性机制来保证事件不丢、不重复、可追踪。
幂等:重复事件怎么办?
网络不稳定的时候,事件可能重复——MQTT QoS 1 保证「至少一次」,可能收到两次;重连后离线消息重新投递,可能和之前的消息重复。
如果事件不是幂等的,重复处理就会出问题:
- 任务完成事件重复——同一个任务被标记两次完成(虽然结果一样,但日志里有重复记录)
- 「完成数量 +1」事件重复——统计数据错误
- 告警事件重复——同一个告警发两次通知
幂等的实现方式:
- 每条事件带唯一 msg_id(UUID v4),接收方维护一个「最近处理过的 msg_id 列表」(最近 1000 条或最近 1 小时),收到重复的直接丢弃。
- 状态机约束——任务状态是单向流转的,已经是 Success 的任务,再收到「执行中」的事件直接忽略。状态机的单向约束天然具有幂等性。
- 操作本身幂等——「设置状态为 Success」「更新位置为 (1200,500)」这些操作天然幂等,执行一次和十次结果一样。设计事件的时候尽量让操作本身幂等。
重试:事件发送失败怎么办?
事件发送失败了(网络断了、接收方挂了、超时没响应),需要重试。
重试的关键设计:
- 指数退避——第一次失败等 1 秒重试,第二次等 2 秒,第三次等 4 秒……避免疯狂重试把服务器打垮。
- 最大重试次数——不能无限重试,最多重试 5 次,超过后告警,人工介入。
- 死信队列——重试超过最大次数的事件,放到「死信队列」里,等待人工处理,不要直接丢弃。
- 重试不改变 msg_id——重试时用同一个 msg_id,这样接收方可以去重,保证幂等。
超时:等不到响应怎么办?
事件发出去了,但迟迟收不到响应(机器人没接单、步骤完成反馈没收到、终态回执没收到),需要超时机制。
超时分两级:
- 命令级超时——每个命令(步骤)有自己的超时时间。比如「导航到 A 点」超时 60 秒,60 秒没完成就认为这个步骤失败了。命令级超时后,可以重试这个步骤,或者终止整个任务。
- 任务级超时——整个任务有一个总超时时间。比如一个任务总超时 10 分钟,10 分钟没完成就认为任务失败了。任务级超时是「兜底」,防止某个步骤无限重试导致任务永远不结束。
超时的处理:
- 超时后触发
Timeout事件 - 根据任务配置决定是重试、跳过、还是终止任务
- 记录超时原因,方便排查和统计
- 通知运维人员(如果是重要任务)
三层回执:任务可靠执行的「安全带」
前面讲任务模型的时候提到过三层回执,这里再从事件驱动的角度强调一下:
- 受理回执(Accepted)——机器人收到任务,校验通过,发回「已受理」。平台收到后把任务状态从 Pending 改成 Executing。如果 10 秒没收到受理回执,重发任务。
- 步骤回执(Step_Completed)——每个步骤完成时发回「步骤 X 完成」。平台更新进度。如果某个步骤超时,触发告警或重试。
- 终态回执(Finished)——任务最终完成(成功或失败)时发回「任务结束」。平台更新为终态。如果总超时,主动查询机器人状态。
三层回执就像寄快递——「已揽收」(受理)、「运输中」(步骤)、「已签收」(终态),每个环节都有确认,不会出现「寄出去了就石沉大海」的情况。
五、踩坑总结:状态机与事件驱动的几点经验
回头看这段从轮询到事件驱动的经历,总结几点:
1. 轮询是「用资源换实时性」,事件驱动才是正确架构
轮询模式下,想要更实时就要更频繁请求,就要消耗更多资源,这是不可调和的矛盾。机器人数量多了之后,轮询会把服务器打垮。事件驱动——状态变化才推送、前端只刷新变化的部分——负载能降到轮询的十分之一,延迟从秒级降到毫秒级。
2. 状态机是事件驱动的前提
事件驱动的前提是状态变化是明确的、可追踪的。任务状态机要定义清楚——有哪些状态、状态之间怎么转换、什么事件触发转换、每次转换都要触发事件。状态机不明确,事件驱动就是空中楼阁。
3. Redis 缓冲层是必备的
不要让事件生产者直接调用消费者——耦合紧、扛不住突发流量、消费者挂了事件就丢了。加一层 Redis 缓冲(Stream 保证可靠、Pub/Sub 保证低延迟),解耦、削峰、可靠投递、多消费者,好处太多了。
4. 前端静默刷新是用户体验的关键
收到事件后不要全页面重渲染,只更新变化的那个组件。快、省、稳、用户体验好。实现起来也简单——给每个元素加唯一标识,收到事件后根据 ID 找到元素,局部更新就行。
5. 可靠性机制一个都不能少
幂等、重试、超时、三层回执——这些机制看起来琐碎,但每一个都是生产环境里踩坑踩出来的。少了任何一个,都可能在某个时刻出问题。建议一开始就做好,不要等出了问题再补。
6. 监控事件处理的关键指标
要监控——事件处理延迟、事件丢失率、Redis Stream 积压长度、重试次数、超时次数。这些指标能帮你提前发现问题(比如积压持续增长说明处理能力不够),而不是等客户投诉了才知道。
写在最后
从轮询到事件驱动,不仅仅是技术方案的改变,更是架构思想的转变——从「主动拉取」到「被动推送」,从「全量更新」到「增量更新」,从「资源换实时」到「事件驱动高效实时」。
我们在这个问题上踩了不少坑——轮询把服务器打崩、状态机定义不清导致事件混乱、前端全页面重渲染导致体验差、可靠性机制没做好导致事件丢失。最后才搭起一套「状态机 + Redis 缓冲 + 事件驱动 + 静默刷新」的稳定架构。
这些经验都是我们(越微智能)在实际项目中一点点踩出来的。我们团队一直在做机器人二次开发、多品牌调度、工业 AI 视觉、边缘计算部署这些落地工作,实时监控和任务调度是每天都要面对的问题。如果你也在做机器人调度、ROS2 开发、实时监控系统相关的工作,欢迎交流,我们一起踩坑、一起进步。
关于作者: 越微智能(Yuewell)是一支专注具身智能与工业 AI 视觉落地的技术团队,提供全品牌机器人二次开发(适配宇树/优必选/智元/傅利叶等)、ROS2 系统开发、工业级视觉算法定制(30+ 算法)、具身机器人调度管理平台、RK3588 边缘一体机等产品和服务,支持从算法、硬件到产线实机部署的全栈交付。
下一篇预告: 《南向插件化:我们是怎么做到新增一个品牌只写一个驱动的》,我们将分享多品牌接入的踩坑经历——从核心代码里塞满品牌 if/else,到 RobotDriver 抽象 + AdapterRegistry 动态分发,以及「四个严禁」和新增品牌的标准流程,敬请关注。