101、通知与指示机制在丢包场景下的可靠性差异
从一个凌晨三点的现场问题说起几年前做一个分布式采集系统几十个前端节点通过无线链路往中心汇聚数据。链路质量不稳定偶尔会丢包。系统运行了大半年没出过大问题直到某天凌晨三点值班同事打电话说监控大屏上有一半节点数据不刷新了但节点自己上报的在线状态还是正常的。赶到现场排查发现一个很诡异的现象中心侧看到的状态是节点在线、链路正常但实际数据就是不上来。重启中心服务之后数据立刻恢复但过几个小时又会重现。当时第一反应是内存泄漏或者句柄耗尽查了一轮没发现明显问题。最后抓包分析才定位到根因——状态同步用的那套机制在丢包场景下会进入一种双方认知不一致的僵局而且这个僵局不会自愈。那次之后我把系统里所有跨节点的交互都重新梳理了一遍发现一个之前完全没意识到的问题通知类机制和指示类机制在丢包场景下的可靠性表现根本不是一回事。混用、误用短时间看不出问题链路一抖动就暴露。这篇就把这个坑讲透。先分清两类机制的本质差异工程里跨节点传递信息粗分下来就两种模式。一种是通知No国外半导体厂商fica国外半导体厂商onA 告诉 B发生了一件事。比如数据已就绪“配置已更新”“该采集了”。通知的特点是——它描述的是一个事件。事件是有时效性的过了那个时间点通知本身就失去意义了。另一种是指示Indica国外半导体厂商onA 向 B 表达我当前处于某个状态。比如我在线“我忙”“我空闲”“我的缓冲区快满了”。指示的特点是——它描述的是一个状态。状态是持续的只要没被新的状态覆盖它就代表当前的事实。这两类东西在无丢包环境下用起来感觉差不多都是发一条消息过去。但一旦链路开始丢包行为差异就出来了通知丢了那件事就没发生过——接收方永远不会知道。除非发送方有重传机制。指示丢了接收方保留的是旧状态——它以为事情还是老样子。而旧状态可能已经和真实情况完全不符了。注意这里的关键区别通知丢失导致的是信息缺失接收方知道自己不知道指示丢失导致的是信息错误接收方以为自己知道但知道的是错的。后者更危险。那个凌晨故障的完整还原回到开头的项目。当时系统的设计是这样的前端节点每采集完一批数据向中心发一条数据就绪通知中心收到后回一条可以发送通知节点再把数据推上来。同时节点每隔一段时间向中心发一条我在线的指示中心维护一张在线状态表。问题出在两个地方。第一“数据就绪通知和可以发送通知都没有重传。无线链路丢一个包这条通知就没了。节点等不到可以发送”就一直等中心以为节点还没准备好也一直等。双方进入互相等待的死锁。这个死锁不会自己解开因为没有任何一方会主动重发。第二更隐蔽的是我在线这个指示。节点发出去之后就不管了中心收到就更新状态表没收到就保持旧值。链路丢包时中心的状态表里节点仍然是在线但实际上节点可能已经因为等待超时而进入了异常状态。中心看到的状态是错的而且没有任何机制能发现这个错误。这两个问题叠加就出现了凌晨那一幕中心以为节点在线指示的旧状态节点在等中心的回复通知丢了数据不上来状态显示正常。重启中心相当于强制清空了状态表双方重新握手暂时恢复。但只要再丢包又会重现。通知机制丢包后的典型异常现象通知丢包在现场通常表现为卡住或超时比较容易发现但排查时容易误判方向。常见的现象有这么几类单向等待。发送方发了通知就不管了接收方没收到发送方以为事情已经交代完了接收方一直在等。表现是某一方长期无动作日志里看不到后续流程。这种相对好查因为卡住的位置很明确。双向死锁。双方都在等对方的消息而各自的消息都丢了。表现是双方都停在某个等待点谁也不动。这种比单向等待难查因为两边日志都显示正在等待看起来都正常。重复触发。如果发送方有超时重传但接收方没有去重机制丢包后的重传会导致同一件事被处理多次。表现是数据重复、任务重复执行。这种最隐蔽因为系统看起来在正常工作只是结果不对。通知风暴。发送方检测到没收到确认就重传重传又丢再重传指数退避没做好就会把链路打满。表现是链路利用率突然飙升但有效数据没多少。排查通知丢包问题的核心思路是在通知的发送点和接收点都打日志对比两边的时间线。如果发送方记录了已发出接收方没有对应的已收到那就是丢了。然后看发送方有没有重传、重传了几次、接收方有没有去重。指示机制丢包后的典型异常现象指示丢包的问题比通知丢包难查得多因为它的表现是状态不对而不是流程卡住。状态陈旧。接收方保留的是上一次成功收到的指示而这个指示可能已经过时很久了。表现是监控显示的状态和实际不符但系统本身还在跑。开头那个项目就是这个问题。状态翻转。如果指示是周期性的丢几个包之后又收到一个接收方看到的状态会跳变。表现是状态在短时间内反复变化看起来像抖动。如果上层逻辑对状态变化做了触发动作就会导致误触发。状态丢失。如果接收方重启或者状态表被清空而发送方的下一个指示又丢了接收方就会处于未知状态。表现是状态显示为未知或默认值但如果默认值恰好是正常就会掩盖真实问题。指示与通知混淆导致的连锁反应。有些系统把指示当通知用比如我上线了这条指示接收方收到后触发一系列初始化动作。如果这条指示丢了接收方不会初始化如果后来又收到一条我上线了比如节点重连后重发接收方又会重复初始化。表现是初始化逻辑被执行多次或零次。排查指示丢包问题的核心思路是给状态加上时间戳和版本号。接收方不仅要知道状态是什么还要知道这个状态是什么时候的、是第几版。如果时间戳太旧就应该主动去查询或者标记为可疑而不是盲目相信。为什么很多系统会混用这两类机制混用的根源在于很多通信协议在语法层面不区分通知和指示都是一条消息发过去。开发的时候如果没想清楚这条消息到底是事件还是状态就会用同一种处理方式。常见的混用模式有把状态当事件发。比如缓冲区满了这条指示被当成一次性通知处理收到就触发一次流控。但如果这条指示是周期发的丢了几次之后又收到就会再次触发流控而实际上缓冲区可能早就恢复正常了。把事件当状态发。比如数据已就绪这条通知被当成状态维护接收方记录一个有数据待取的标志。如果这条通知丢了标志不会被置位数据就永远不被取走。而如果发送方后续又发了新的通知标志被置位但可能对应的是另一批数据导致取错。用同一种确认机制处理两者。通知需要确认重传指示需要确认版本超时失效。如果用同一套确认机制要么指示被过度重传浪费带宽要么通知没有重传丢失后无法恢复。工程上的取舍与判断思路搞清楚差异之后实际项目中怎么选、怎么设计有几个判断点。第一问自己这条消息丢了会怎样。如果丢了之后接收方只是不知道那它是通知需要重传或确认。如果丢了之后接收方会错误地以为那它是指示需要版本和时效。第二通知的重传要有边界。无限重传会把链路打满而且如果接收方真的挂了重传也没用。通常的做法是有限次重传超时上报让上层决定是放弃还是换路径。第三指示的时效要明确。每条指示都应该有一个有效期超过这个时间没收到新的接收方就应该把状态标记为未知或过期而不是继续沿用旧值。有效期的长度取决于状态变化的频率和业务能容忍的陈旧程度。第四指示的版本号比时间戳更可靠。时间戳依赖时钟同步分布式环境下时钟漂移是常态。版本号单调递增接收方只要发现版本号没变或者倒退了就知道指示丢了或者乱了。第五关键状态用查询代替等待。如果某个状态很重要不要被动等指示主动去查询。查询是请求-响应模式有超时和重试比单向的指示可靠。代价是增加交互次数所以只对关键状态用。第六通知和指示不要在同一个通道里混跑。如果条件允许把通知和指示分开处理用不同的优先级、不同的重传策略、不同的超时参数。混在一起的时候一方的重传会挤占另一方的带宽互相影响。一个可落地的检查清单每次设计跨节点交互的时候拿这张清单过一遍1. 这条消息是通知还是指示 - 事件型 → 通知 - 状态型 → 指示 2. 如果是通知 - 丢了接收方会怎样不知道 → 需要重传 - 重传几次超时多久 - 接收方如何去重 - 重传失败后如何上报 3. 如果是指示 - 丢了接收方会怎样保留旧状态 → 需要版本时效 - 版本号怎么生成单调递增 - 有效期多长超时后状态标记为什么 - 接收方重启后状态如何恢复 4. 通知和指示是否混在同一通道 - 是 → 考虑分离或至少区分优先级 5. 关键状态是否有主动查询通道 - 否 → 评估被动等待的风险最后说一个反直觉的点很多人觉得指示比通知可靠因为指示是周期发的丢一个还有下一个。但实际工程中指示丢包的危害往往比通知丢包更大。原因在于通知丢了流程会卡住卡住就会被发现。指示丢了流程照常跑只是状态是错的错误会被掩盖直到某个临界点才爆发。而爆发的时候现场往往已经和正常状态差得很远排查难度成倍增加。所以我的经验是通知可以丢但要有重传兜底指示不能丢丢了必须能被发现。发现的方式就是版本号时效主动查询。三者至少要有一样最好都有。那次凌晨故障之后我把所有指示类消息都加上了版本号和有效期中心侧发现版本号长时间不更新就主动去查询节点真实状态。改完之后同样的丢包率下再也没有出现过状态显示正常但数据不上来的情况。链路该丢还是丢但系统能自己发现不对劲这就够了。关键词核心关键词丢包、超时、重传、抓包精准长尾关键词先分清两类机制的本质差异、通知机制丢包后的典型异常现象、指示机制丢包后的典型异常现象、最后说一个反直觉的点、蓝牙BLE开发实战、BLE协议栈详解、低功耗蓝牙调试、BLE连接参数优化、蓝牙抓包分析、BLE广播数据解析、蓝牙连接稳定性优化、低功耗蓝牙功耗优化