命令发送成功,数据却没回来:一次设备同步故障与补录设计

平台返回发送成功,并不代表设备数据已经同步成功。沿着命令、回程消息和时间窗口拆开排查,才能解释为什么定时任务恢复之后,历史记录仍然没有回来。

🎧 文章导读

先把“成功”拆成三层

这次故障最容易误判的地方,是日志里同时存在两个看起来矛盾的结果:平台返回 SENDMESSAGE_SUCCESS,定时同步却不断失败,业务层最后只剩下一句“接口调用失败![null]”。

命令被平台受理、设备执行后返回、业务数据成功入库,是三个不同的结果。 如果把第一层当成第三层,后面的排查方向就会全部偏掉。

实际链路是:调度任务每 2 分钟调用业务服务;集成服务向平台发命令,拿到 commandId;平台将命令下发设备,再把设备回复通过消息通道推回来;原请求线程按命令编号等待,最长约 15 秒。

命令去程与消息回程
图 1:去程受理成功,只覆盖了链路前半段。故障需要沿回程消息继续定位。

信号 能说明什么 还不能说明什么
SENDMESSAGE_SUCCESS 平台受理了命令 设备已回复、数据已入库
RECEIVE_FAIL 超时 等待期内未取得回复 一定是设备故障
收到成功回复、列表为空 本次查询正常且无记录 历史窗口之外也无记录
业务入库成功 数据通过了本系统校验 所有历史缺口均已补齐

这里的 15 秒等待还是自旋实现。它值得另行优化,但这次记录没有完成该项改造,不能把性能债务和本次回程故障混为一个根因。

沿证据找断点,而不是沿报错文案猜

业务日志中的 [null] 实际上丢失了集成服务返回的错误信封。继续向下查,才能看到“命令已发出,但没有在预期时间内收到回复”。

设备是否有问题,还要换一个观察位置。平台管理台里,设备在线,查询能够很快返回。这将怀疑范围收敛到平台向业务系统投递回复的环节。另有网络不可达日志,结合故障时间,指向平台与系统之间的网络或投递异常;这些证据支持排查方向,但不足以进一步断言某台网络设备就是根因。

随后按时间压缩连续成功/失败状态,规律变得清楚:大部分夜间中断都与设备下班关机、次日开机一致,只有一个工作日白天出现异常长时间中断。规律比单条 ERROR 更有解释力。

下面是适合记录在排障笔记里的脱敏时间线格式:

1
2
3
4
5
08:00—17:00  正常同步
17:30 设备本地新增登记记录
17:32—19:30 命令受理成功,等待回复超时
次日 08:00 同步恢复,但只查询最近两小时
结论 恢复之后的成功请求,没有覆盖前一天的登记

这次还发现另一类设备业务的无限重试造成大量日志。它是独立问题,应保留线索单独处理;不能因为日志数量最多,就把它当成访客同步故障的直接原因。

两分钟执行一次,为什么仍然漏数据

定时任务每次拉取过去 2 小时的记录。正常情况下,一条登记大约有 60 次被查询到的机会,因此短暂网络抖动通常不会造成永久漏拉。

但这个冗余只存在于有限窗口内。一旦失败持续超过窗口宽度,恢复后的任务会继续向前走,不会自动回头。 “任务恢复”与“历史数据完整”因此没有必然联系。

滚动窗口与历史缺口
图 2:窗口不断前移;故障恢复时,旧登记已经落在窗口之外。

可以用一个简单条件理解风险:

1
2
3
4
5
# 这是风险判断示意,不是实际调度实现。
needs_recovery = (
outage_duration > lookback_window
and source_has_records_during_outage
)

其中第二个条件很重要。设备夜间关机但无人登记,和白天有人登记却回程断链,补录价值完全不同。最有力的确认方法,是直接向设备查询故障时段,先证明源数据还在,再讨论如何恢复。

补录入口必须同时考虑兼容、幂等和校验

这次采用的修复比较克制:给既有同步接口增加可选的 startTime、endTime。不传参数时仍然查询过去 2 小时;传参时必须成对提供,格式错误直接拒绝。

以下是脱敏后的调用示意;路径和端口需替换为实际环境,接口仍应置于现有认证与运维访问控制下:

1
2
curl --request POST \
'http://localhost:8080/internal/device-sync?startTime=2026-09-21%2000:00:00&endTime=2026-09-21%2023:59:59'

宽窗口会重复拉到已入库记录,因此必须核对已有幂等逻辑。本次实现按设备记录标识判重,重复拉取会跳过已同步记录。推广到多设备系统时,还要检查这个标识是否只在单设备内唯一;如果是,就需要带上设备维度,不能照搬一个全局唯一假设。

补录还有第二道门槛:设备允许保存的数据,不一定通过业务系统校验。本次就遇到设备侧证件字段宽松、系统侧格式与校验位严格的问题。记录拉到了,但被业务校验拒绝,仍然不能算恢复成功。

因此,补录结果至少要分开记录:

结果 后续处理
已存在 幂等跳过,计入重复数
新增成功 核对业务列表及关联字段
字段不合法 保留失败原因,按业务规则修正源数据后重试
请求或回程失败 保留窗口与命令编号,重新确认链路

“校验失败就清空字段继续入库”属于数据语义变更,本次没有擅自采用。自动降级是否可接受,应由业务规则决定。

日志和发布过程也是修复的一部分

这次代码同时修复了错误透传。下层没有业务响应时,保留信封中的错误码和说明,避免在上层只剩下 null。理想的日志结构可以参考下面的字段;它是建议格式,不代表本次已全部实现:

1
2
3
4
5
6
7
{
"stage": "WAIT_DEVICE_REPLY",
"commandId": "example-command-id",
"windowStart": "2026-09-21 00:00:00",
"windowEnd": "2026-09-21 23:59:59",
"result": "TIMEOUT"
}

修复过程还暴露了一个发布陷阱:本地代码打包部署后,没有及时进入团队统一构建来源,随后被另一轮正常发布覆盖。新接口一度又回到旧行为,宽窗口参数没有产生预期效果。

遇到“刚修好又不生效”,应该先核对部署包对应提交、文件摘要和实际版本,再怀疑数据被删除。堆栈行号可以辅助识别,但不应替代版本标识。

据原始记录,本次新增的控制器、错误透传和提供方相关测试共 6 条通过;补录曾完成一条合法样本的端到端验证。这些是当时的工程记录,本篇整理没有重新操作设备,也没有把后续待发布事项写成已完成。

这次最值得留下的经验是:同步故障要同时回答“链路恢复了吗”和“数据补齐了吗”。 前者靠分层观测,后者靠明确窗口、幂等恢复以及逐条业务结果。

延伸阅读


本文根据 2026 年 9 月 22 日至 10 月 22 日的工程笔记整理。账号、凭证、地址和业务样本已脱敏;通用示例已在正文标明,历史验证结果以原记录为准。