Skip to content
Belltoy

一套规则引擎,为什么最后被我们删除了?

Published at:

上一篇《AI 越来越会写代码,软件工程师的价值在哪里?》中,我谈到软件开发真正困难的往往是工程判断。这种判断不只发生在设计之初。需求讨论时看不到的约束,可能在实现中出现;原本合理的方案,也可能在长期运行之中暴露出问题。

我之前维护的一套大规模 MQTT 网关系统的消息链路中,有一套独立的规则引擎,使用了上百台机器。我们花了很长时间优化它,最后却把它整个移除了。

这个结果并不是最初就能预见的。相比省下了多少机器,我更想回顾的是:为什么我们一开始关注的是如何把它做得更好,后来却开始判断它是否还有必要存在?

这段经历让我意识到,理解问题并不是开发之前完成的一项准备工作。系统开始运行之后,我们才有机会检验当初的假设,看到原来没有看清的成本。

一个看似合理的技术方案

我加入团队时,这套规则引擎已经存在。它的背景很直接:MQTT Gateway 接收海量设备消息,消息进入 Kafka 后,由上层消费者完成业务处理。但消费者的处理能力不足,无法承受全部消息带来的负担。

原来的方案是在 Gateway 与 Kafka 之间增加一层独立的规则处理系统。Gateway 通过 gRPC 将消息交给它,再由它根据规则过滤、分流、转换消息,把处理后的数据写入 Kafka。这样,消费者就能少接收一些消息,也能拿到更适合直接处理的数据。

这套系统使用 SQL DSL 描述规则。对于需要接入多种设备、服务不同业务的系统,这种设计很容易理解:不同业务关心的消息不同,数据格式也不同,用统一的规则表达处理逻辑,看起来既灵活,又能避免在 Gateway 中不断增加代码。

从当时要缓解的压力来看,它也有实际作用。减少下游接收的消息,提前完成一部分处理,可以让现有消费者继续工作,不必立刻大幅调整业务系统。独立部署也让这部分能力能够单独扩容,规则处理不会直接占用 Gateway 的全部资源。因此,早期方案看似有道理。面对眼前的吞吐压力,增加一个能够独立扩容的处理层,是可以理解的选择。只不过这个选择同时引入了另一套需要部署、监控、排障和持续优化的系统,也增加了一层可能发生调用失败和故障传播的边界。

消费者减轻了负担,也不代表整条链路的工作减少了。消息仍然要经过 Gateway,再被传给规则引擎处理。只看消费者这一端,很容易低估为了减轻它的压力,其他地方增加了多少工作。

随着规模扩大,规则引擎使用了上百台机器。对维护者来说,最自然的任务就是让它处理得更快、消耗得更少。我最初也沿着这个方向工作:既然消息需要经过这一层,就努力降低这一层的成本,换成 Rust 实现,优化它的吞吐和处理效率。至于这些工作为什么必须集中在这里,当时还不是我们最关注的问题。

运行之后,问题逐渐清晰

在长期维护和研究这条链路的过程中,我发现实际情况与通用规则引擎所暗示的需求有很大差别。

整套系统只有三四十条规则,其中不少内容重复。规则也长期固定,基本上不需要修改。原本用于应对灵活变化的 SQL DSL,实际承载的却是一组数量有限、相当稳定的处理逻辑。

再看规则做的事情,许多格式转换本来就属于设备业务。设备消息的结构由对应设备和业务定义,消费者本身也需要理解这些语义。把转换放到中间层,虽然让消费者暂时省了事,却让公共基础设施承担了不同设备的业务知识。

消息量本身也值得重新检查。一些历史 IoT 设备在网络波动或延迟时,会无脑地重复发送十多次,甚至更多。从后端看,这是需要处理的流量;沿着链路往前追,才发现其中一部分负担来自设备的发送行为。

当然重复不等于无意义。MQTT QoS 1 允许重复交付,设备无法确认发送结果时,重试也可能是必要的。需要区分的是保障可靠性的机制与不合理的重试条件、次数和频率,不能仅凭内容相同就丢弃消息。

发现源头问题,也不等于能够立即消除它。历史设备能否更新,仍然会限制实际选择。我们需要在这些约束下判断兼容处理的必要性,而不能为了降低负载牺牲消息应有的可靠性。

这些发现改变了我看待系统的方式。消费者的容量、设备业务的适配、源头的异常重发,原来都被归结成了规则处理需求。不同团队各自掌握一部分信息,缺少端到端视角时,这些差异很容易被一个中间层遮住。能够用同一种技术处理,并不意味着这些工作应该由同一个系统负责。

真正的优化,是重新定义问题

我们此前长期优化规则引擎,关注的是既定架构下的处理效率。这些工作有价值,但它们默认了一件事:这一层始终是必要的。

后来随着规则和链路逐渐清晰,我们开始重新检查这个前提。消费者真正需要哪些消息?哪些规则只是同一逻辑的重复表达?哪些转换应由消费者负责?剥离这些问题之后,Gateway 出口究竟还剩多少必须完成的工作?

最终,我们归并规则,收敛需要保留的处理,在 MQTT Gateway 出口用少量固定规则直接写入 Kafka,取消了独立的规则引擎。需要理解设备业务的适配,应回到相应消费者;消费者处理能力不足,也应该继续通过改善自身架构来解决。

这里有一条非常重要的约束:不能继续把新的业务规则塞进 Gateway。 否则,只是把原来的规则引擎搬进另一个进程,甚至让业务变化直接影响设备接入。少量稳定的出口规则可以留在这里,并不意味着 Gateway 应该成为新的通用业务处理平台。

改造后,我们移除了上百台机器,也省去了一层内部网络传输和相应的 CPU 消耗。少了一套独立系统,就少了一套部署维护工作,也减少了 gRPC 调用带来的超时、失败和排障边界。而 Gateway 并没有出现明显的新增负载。实际留下的工作,比原来那套系统呈现出来的规模小得多。

这个结果让我印象很深。此前我们一直在研究如何更高效地完成一批工作,后来才发现,其中一些没有必要以原来的方式存在,另一些则应该交给更理解业务的地方。

如果只以规则引擎的吞吐量和资源利用率评价工作,我们很难走到这个结论。把视线从自己维护的组件移到整条消息链路上,才可能看到另一种优化空间。

但我也不想把它概括成「组件越少越好」。如果规则确实复杂多变,需要独立的资源和故障隔离,规则引擎仍然可能是合理方案。这次能够删除它,是因为具体的运行事实已经不足以支持原来的复杂度。改变设计的依据,是对问题有了更清楚的认识。

工程判断贯穿整个生命周期

回头看,这次调整并不是某一次评审突然得出了更好的架构。它来自维护中的持续积累:研究规则、观察负载、追查消息来源,以及与其他团队核对业务语义。这些工作不断补全了我对系统的认识。

在企业里,理解一个系统所需要的信息,往往分散在不同团队和不同人的经验中。设备为什么重发、消费者为什么处理不过来、某条规则为什么存在,未必有人掌握完整的答案。有些背景没有文档化,有些随着人员离开已经难以追溯。代码和配置记录了系统现在怎么做,却未必说明当初为什么这样做。

已有设计也不能直接当成正确答案。早期决策可能建立在不完整的认识上,也可能曾经合理,但支撑它的需求和技术条件已经变化。接手一套运行中的系统,需要分清哪些是真实约束,哪些只是过去选择留下的坑。

这也是工程判断需要贯穿整个生命周期的原因。需求讨论时,要整合不同角色掌握的信息,确认目标;实施过程中,要通过实现发现遗漏的约束;上线以后,还要用真实运行的证据检验原来的假设。即使系统已经稳定运行,这些观察和判断仍然需要继续。

结语

AI 正在加快实现功能和构建系统的速度,也能帮助我们分析代码、探索方案。但散落在不同团队中的知识、没有被记录的历史背景,以及已经变化的现实条件,仍然需要工程师主动追查、核对和联系起来。实现得更快,并不自动意味着问题理解得更准确。

对我来说,这套被删除的规则引擎提醒了我:接手的既有系统,也有一组有待检验的历史判断。软件工程师的价值,在于从分散、不完整甚至相互矛盾的信息中形成对问题的理解,用证据做出判断并持续修正技术决策。过去的选择不一定是对的。


Share this post:

Previous Post
AI 越来越会写代码,软件工程师的价值在哪里?