发生了什么
某设备升级后,把上报数据里的一部分配置信息从对象格式改成了数组格式,并从主体信息里拆出来并行上报。设备侧和中间平台看都正常。但下游有个消费方服务在拉数据时踩了坑:它一整包把设备核心数据(如用量、在线状态)和一堆不相干的配置信息全拉进来,然后整包一起反序列化。那个改成数组的配置字段格式不符预期,导致整包解析直接报错中断,本来完全正常的核心用量数据也跟着被丢弃,缓存停在故障前的旧值不再更新。更糟的是解析失败被静默吞掉,不报错不告警,接口照常返回旧数据。因为这条链路下游是计费,用量停更后每天账单都算成 0,系统只打了个异常标记却不告警、也没自动对账,直到多日后业务方人工发现费用偏少才上报,最终产生了需要事后补偿的资损。
本质
被破坏的约束有两条:一是消费方应只订阅自己真正需要的字段,并对不关心的字段解析失败保持隔离,这里却把无关配置和核心数据耦合进同一个整包、用零容错的方式整体反序列化,等于让一个不相干字段拥有了搞垮核心数据的权力;二是资损相关的数据链路必须监控数据本身的异常,而不只是系统报错。用量静默变 0、接口仍能返回旧值,这种看起来正常、其实数据早错了的状态比直接报错更危险,而现有监控只认系统异常、不认数据异常,于是错误状态长期无人察觉。
下次遇到这些场景要警惕
消费上游数据时只取自己需要的字段,别一整包全吃;反序列化要做到字段级容错,关心的字段解析成功就用,不关心的字段坏了就跳过,绝不能一个字段类型不符就整包全废。解析或消费失败绝不能静默,要有错误指标和告警,别拿着过期缓存假装正常对外服务。凡是资损或核心数据链路,除了系统异常告警,还要加数据异常告警和自动对账:比如用量连续为 0、账单金额环比骤降、数据长时间未更新都要能报出来。另外,上游做字段或格式变更前,别用历史版本这么改过没事当安全依据(下游消费方可能早已变化),每次变更都要走一遍下游消费方的兼容确认。
下面用代码把整包零容错和字段级容错加只取所需讲清楚。
问题版本:整包反序列化,任一字段格式不符就全废,且失败静默。
CabinetData parse(String bigJson) {
try {
// 把核心数据和一堆无关配置整包一起反序列化
// 只要 config 字段(本次从对象改成了数组)类型对不上,整包就抛异常
return objectMapper.readValue(bigJson, CabinetData.class);
} catch (Exception e) {
// 静默:解析失败不报错不告警,返回 null,上层继续用旧缓存
return null;
}
}
修复版本:只取需要的字段加字段级容错,坏字段不影响核心字段,且失败要告警。
CabinetCore parseCore(String bigJson) {
JsonNode root = objectMapper.readTree(bigJson); // 先转成树,按需取字段
CabinetCore core = new CabinetCore();
// 只解析真正需要的核心字段
core.setPower(root.path("power").asDouble());
core.setOnline(root.path("online").asBoolean());
// 不关心的配置字段单独、容错地解析:坏了只记这一块,不牵连核心数据
try {
core.setConfig(parseConfig(root.path("config")));
} catch (Exception e) {
metrics.increment("cabinet.config.parse.error"); // 有指标可告警
log.warn("配置字段解析失败,已跳过,不影响核心数据", e);
}
return core;
}
数据异常侧的兜底(把只打 log 升级成告警加对账):
// 旧:用量为 0 只打 info,没人看
// log.info("cabinet power is 0");
// 新:核心或资损链路,数据异常要能报出来
if (core.getPower() == 0 || dataStale(core)) {
metrics.increment("cabinet.power.zero"); // 连续为0或长时间未更新触发告警
alert("设备用量为0或长时间未更新,疑似数据停更", cabinetRef);
}
前后端边界:这条几乎全在后端或数据链路。上游是设备固件的上报格式变更,问题点在中间消费方服务的解析设计(整包、零容错、静默)和计费链路的监控缺失。用户前端功能其实没受影响,受影响的是后台结算数据。所以修复重心在后端:消费方按需取字段加字段级容错加失败告警,计费链路补数据异常告警和自动对账。前端或展示侧无需改动。