
下午,最怕的就是在准备进入“摸鱼”状态时,工作群里弹出急促的 @全体成员。这次也不例外,消息来自我们合作的一家现代化工厂的生产线主管:“紧急求助!我们产线的温度告警工作流又失效了!后台数据显示温度连续几次都超标了,但告警灯没亮,钉钉群里也没收到通知!这已经是本周第三次了!”
看到“又”和“第三次”,我的头皮瞬间发麻。这种“随机发生”的 Bug,是每个程序员的噩梦。它不像必现 Bug 那样可以稳定复现,而是像一个幽灵,在你最意想不到的时候出现,又在你准备好工具要抓它时消失得无影无踪。
在深入排查之前,有必要先介绍一下我们这套系统的架构。它是一个典型的IOT 系统,旨在通过传感器和自动化工作流,提升工厂生产线的智能化水平。
组件/层面 | 技术实现 | 职责 |
|---|---|---|
数据采集层 | 产线温度传感器 | 通过 MQTT 协议,每 10 秒上报一次产线实时温度数据。 |
数据接入层 | EMQ X (MQTT Broker) | 接收并分发所有 IoT 设备的遥测数据。 |
核心处理层 | 自研低代码工作流引擎 | 允许产线主管通过拖拽节点的方式,自定义业务规则。本次出问题的就是其中一个告警工作流。 |
数据存储层 | InfluxDB + MySQL | InfluxDB 存储海量的时序传感器数据;MySQL 存储工作流定义、执行日志和告警记录。 |
告警触达层 | 硬件控制模块 + 钉钉机器人 | 接收工作流指令,点亮物理告警灯,并向指定钉钉群发送消息。 |
出问题的低代码工作流逻辑非常简单,由产线主管自己配置:
当A产线的温度传感器,连续 3 次检测到的温度都高于 85℃ 时,立即触发“一级告警”动作(亮灯 + 发钉钉)。
这个逻辑在低代码平台里,看起来就像下面这样一目了然的流程图:
根据主管的反馈和我们后台的日志,Bug 现象被精确地描绘了出来:
14:30:10、14:30:20、14:30:30 这三个时间点,温度分别是 86.1℃、87.5℃、86.8℃,确实满足了“连续3次高于85℃”的条件。数据流是通的,但逻辑流在某个环节被“神秘力量”中断了。
最直接的怀疑对象,自然是产线主管自己配置的低代码工作流。会不会是哪个参数配错了?比如把“连续3次”配成了“连续30次”?
我立刻登录平台,仔细检查了工作流的每一个节点配置。结果是:配置完全正确。主管并没有犯这种低级错误。这条线索断了。
接下来,我开始怀疑时间。MQTT 消息里带有时间戳,我们的工作流引擎在判断“连续”时,也依赖于时间。会不会是传感器、MQTT Broker、工作流服务器之间的时区不统一,导致时间戳解析错误,使得系统认为这三条消息并非“连续”?
这是一个非常常见的分布式系统 Bug。我立刻拉取了三方服务器的系统时间,并检查了日志中记录的各个环节的时间戳。
虽然时区不同,但我们的代码中做了规范的转换处理。经过一番详细计算,确认时间戳的连续性判断逻辑没有问题。这条路也走不通了。
两次碰壁后,我不得不回到最底层的架构去思考。我们的低代码工作流引擎为了保证高可用,是集群化部署的,一共有 4 个节点(Node)在运行。
这时,一个念头如同闪电般划过我的脑海:状态!计数的“状态”存储在哪里?
我冲到代码前,找到了负责处理这个工作流的核心逻辑。不看不知道,一看吓一跳。为了性能,当时的开发人员将“连续次数”这个计数器,直接存放在了工作流引擎节点的内存中!
问题瞬间清晰了!
14:30:10,第一条超温消息 (86.1℃) 被发送到了 Node A。Node A 在自己的内存里记录:“传感器X,超温次数:1”。14:30:20,第二条超温消息 (87.5℃) 被发送到了 Node B。Node B 在自己的内存里记录:“传感器X,超温次数:1”。14:30:30,第三条超温消息 (86.8℃) 被发送到了 Node C。Node C 在自己的内存里记录:“传感器X,超温次数:1”。灾难发生了! 在任何一个节点看来,超温次数都从未达到过 3 次!它们各自为战,信息完全孤立。那个作为触发条件的计数器,被无情地分散在了集群的各个角落,永远无法累加到我们期望的阈值。
这就是 Bug“随机”发生的原因:只有当 MQTT Broker 碰巧将连续 3 条消息都发给了同一个工作流节点时,告警才能成功触发。而大部分情况下,消息会被分发到不同节点,导致告警失效。
定位了根源,解决起来就简单了。问题的核心在于本地状态无法在分布式环境下共享。我们需要一个所有节点都能访问的、统一的、原子的状态存储。
最佳选择:Redis。
我们对工作流引擎的“计数器”节点进行了改造:不再使用节点内存,而是引入 Redis 来进行分布式计数。
改造后的伪代码逻辑:
// 当工作流执行到“计数器”节点时
public class CounterNodeHandler {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void process(SensorData data) {
if (data.getTemperature() > 85.0) {
String counterKey = "workflow:counter:" + data.getSensorId();
// 使用 Redis 的 INCR 命令,这是一个原子操作
Long count = redisTemplate.opsForValue().increment(counterKey);
// 每次计数后,都设置一个合理的过期时间(例如60秒)
// 防止因消息中断导致计数器永远存在
redisTemplate.expire(counterKey, 60, TimeUnit.SECONDS);
if (count >= 3) {
// 触发告警
triggerAlarm(data.getSensorId());
// 告警后立即清除计数器,避免重复告警
redisTemplate.delete(counterKey);
}
} else {
// 如果温度正常,则立即清除计数器
String counterKey = "workflow:counter:" + data.getSensorId();
redisTemplate.delete(counterKey);
}
}
}通过这个改造,无论 MQTT 消息被分发到哪个工作流节点,它们操作的都是 Redis 中同一个 key。INCR 的原子性保证了计数的准确性,EXPIRE 机制则保证了系统的健壮性。
部署上线后,我们观察了两天,那个“幽灵”般的 Bug 再也没有出现过。
这次惊心动魄的 Debug 经历,给我带来了深刻的教训和反思:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。