现场反馈「报警太吵」,我们从头到尾查了一遍,结论是传感器没问题,规则表的设计有问题。这篇把当时改的东西和还没改的东西一起写下来。
一、规则表上为什么必须有一个唯一键
farm_env_threshold_rule这张表最早只有设备、指标、取值几列,页面允许多人从不同入口配置,交接期间同一件事被建两遍的概率很高。设备报一次异常,列表里出两条告警,现场看起来就是系统在抽风。
改法是加一个source_unique_key列,把设备、指标、方向这几个维度拼起来做唯一约束,约束范围限定在「在役」的规则上:
ALTERTABLEfarm_env_threshold_ruleADDCOLUMNsource_unique_keyVARCHAR(191)NULL,ADDUNIQUEKEYuk_env_threshold_in_force(source_unique_key,in_force_flag);CREATEINDEXidx_env_threshold_queryONfarm_env_threshold_rule(device_id,metric_code,in_force_flag);这里有个取舍容易被忽略:业务上确实存在「同一设备同一指标要两套阈值」的正当需求,比如白天和夜间的温度区间不一样。所以唯一键必须带上在役状态这一维,停用的规则让位,新的才能进来。如果图省事做成全表唯一,等于把合法的多档位配置一刀切掉,现场会绕过系统,在别处再记一份。
二、匹配要能逐级往下退
气象灾害类的规则比环境阈值更难落:它要绑到具体的棚、具体的地块上。实际项目里配置粒度是不齐的,有的农场把棚和地块登记得很细,有的只有几行粗名字。只做精确匹配的结果就是,规则明明存在,一条提醒都出不来,然后被判定为「系统没反应」。
我们采用的是逐级回退:先按精确对象找,找不到退到同一台设备,再找不到按语义匹配。这个顺序不能颠倒,精确的那一级优先,回落只是兜底。
回退带来一个新问题:命中范围会悄悄变大。所以告警记录里我们额外写了「本次命中的是哪一级」,不然事后没人解释得清这条提醒为什么会落到这个棚头上。这属于自己给自己留的排查线索。
三、规则绑在物模型字段上,不绑原始报文
设备接入这层是分开做的:协议驱动可插拔,Modbus 有 Modbus 的解析器,JSON 通用报文有通用的解析器,MQTT 直连还有一套,各自负责把字节流翻译成结构化数据。再往上由物模型模板统一语义,阈值规则绑在物模型的字段上,不绑在某个厂家报文的第几个字节上。
这个选择在换设备的时候有回报。同一台网关下挂不同厂家的温湿度探头,只要都按物模型登记过,阈值规则不用改;反过来,如果规则是照着某一份报文地址表写死的,换一批设备就得重配一遍,而且很难有人记得清上一版为什么那么写。
留一个要说清的地方:原始报文解析失败时,阈值判断不允许沿用上一条读数顶上去。宁可这一段是空的,也不能拿旧值假装新值。这条纪律听起来理所当然,实际排查故障时,它是真值和新值之间的分界线。
四、判断和执行拆开之后,账才记得清
环境阈值这一层只做判断。它产出的是告警,以及一句「建议开风机」这样的控制意图,不会直接给设备下指令。下发动作本身归设备管控那侧,走远程控制和指令下发通道。下发成功之后自动写台账,页面上没有独立的人工录入入口。
这个「没有独立入口」是故意的。如果判断侧和执行侧都能往同一张台账里写一行,重复记账几乎必然发生,后面统计执行率就是错的。代价也有:现场想手工补一条执行记录时没地方补,只能走下发链路把记录带出来。我们判断这个代价可以接受,因为它换来的是账面上没有两套真相。
五、去重做在写入侧,而不是事后清洗
去重我们分两层看。
规则层靠上面的唯一键,再加上设备维度的收敛,同一个原因不会因为有两条在役规则而被放大。
明细层参考了虫情识别模块的做法:识别明细表的写入侧做了 60 秒的失败原因去重,同一原因在窗口内复用同一行记录。原因很朴素,现场人员遇到卡顿会连续点几次按钮,如果每次都老实落一行,明细表会被刷成一片一模一样的失败行,事后根本分不出是四次故障还是一次点击。写入侧去重的成本远低于事后清洗。
要说清的是,这套东西没有解决所有问题。告警的时间窗聚合我们到现在也没做,目前只靠规则唯一键加设备维度收敛,同一条规则在短时间内仍然可能重复告警。另外这套做法在五十个点位以内的农场是够用的,点位上了千,采集架构和聚合层都得换,我们还在试。
六、失败必须显式,不能回落成「看起来正常」
虫情 AI 那套错误码体系我们后面照搬到了其他模块,核心只有一句话:任何一步不成立,结果就是 null。
没有图片是PEST_IMAGE_MISSING,图片基地址没配是PEST_AI_IMAGE_BASE_URL_NOT_CONFIGURED,没有配置行是PEST_AI_NOT_CONFIGURED,提供方没有实现是PEST_AI_PROVIDER_UNSUPPORTED,调用失败和响应解析不了各有一个码。规则配了但被停用,走的是和未配置一样的码,也就是说停用等于未配置,不回落、不假装成功,界面上一律显示不出识别结果。
超时默认 5000ms,可配范围 500ms 到 60000ms。危害等级分 light / moderate / severe,其中 moderate 和 severe 强制关联处置,防治回看默认 60 天,上限 365 天。
这套思路在阈值这层同样适用。把「没配置」渲染成「一切正常」,是这类系统里最贵的 bug,因为它在很长一段时间里不会被人发现。
七、告警列表的查询开销
告警列表是页面上被打开次数最多的一个视图,也是最先出问题的一个。它天然带时间范围条件,如果 SQL 只会写WHERE device_id = ?,时间过滤靠扫描,点位一多就慢。
我们按设备、指标、在役状态建了复合索引,列表查询走游标分页,不再用大的 offset。理由很简单:现场看告警的顺序永远是「最新的先看」,翻到第十几页的情况基本不存在,为深分页做优化是白花的力气。
时序数据走的是单独的存储通道,业务库只存规则、告警和处理状态。把高频读写的时序点从业务表里拆出去之后,规则表在高峰期的锁等待明显少了。这一块取舍的关键在于,别让两个写放大不同的负载压在同一张表上。
八、接口返回 200 不等于配置生效
还有个坑记一下,发生在自研网关那边。设备侧的 Web 请求线程里直接写 flash,会触发看门狗把设备打掉线,表现为前端点了保存、提示成功,设备随即离线。后来的改法是引入 pending-save,请求只挂一个待落盘的标记,由主循环串行写入。
这次改动反过来影响了外部接口的设计:保存类接口不能只回一个 200 就完事,状态回读要落到设备侧确认之后才算生效。前端那句「已保存」的措辞也跟着改成了「已提交」。接口语义上的诚实,比提示文案好看要紧。
规则引擎能做的,是让噪音少一点,账目清楚一点。参数本身准不准,还是得回到现场去问。
延伸阅读:这一层的完整能力清单与口径边界(哪些能保证、哪些必须现场确认),我们整理在《农业环境监测》:https://www.hoyotech.top/solutions/environment-monitoring