一个让人抓狂的幽灵丢包前两年做一个无线自组网项目现场部署了二十多个节点拓扑是链状加部分网状冗余。设备跑起来之后日常业务数据基本正常但每次后台触发信道地图更新就零星出现丢包。丢包率不高大概千分之几但问题是它的分布毫无规律——有时候一整天没事有时候十分钟内连着丢几包。客户那边跑的是控制指令丢一包就是一次重传重传超时就是一次告警。最开始的排查方向是射频链路质量。我们挂了频谱仪看底噪换了几个频点调了发射功率甚至把天线重新做了匹配。结果丢包照旧。后来又怀疑是协议栈的缓冲区不够加大了队列深度反而更糟——延迟上去了丢包没下来。折腾了将近两周最后定位到问题出在信道地图更新的时序上。这个坑很深因为信道地图更新本身是低频操作大家默认它不影响业务恰恰是这个默认害死人。信道地图更新到底在做什么先把这个过程拆开说。所谓信道地图就是每个节点维护一张表记录自己和邻居之间在哪些信道、哪些时隙上通信质量好。这张表不是静态的环境变了、节点移动了、干扰源出现了都需要更新。更新的方式通常是触发一次全网或局部的探测过程节点在多个候选信道上发送探测帧收集应答统计成功率或信噪比然后重建地图。关键点在于探测过程本身要占用空口资源。很多实现里探测帧和业务帧共享同一个物理信道只是靠时隙或优先级区分。如果调度器设计得不够细致探测帧就会和业务帧抢时隙。抢赢了业务帧被推迟抢输了探测帧发不出去地图更新不完整。而丢包往往发生在推迟这个动作上——业务帧在队列里等等到超时被丢弃。更隐蔽的是探测帧的发送时机通常由定时器驱动。如果这个定时器和业务帧的发送定时器没有做好互斥就会出现两者同时触发的情况。这时候调度器如果只是简单地先到先服务那谁先排队谁先走后到的那个就可能错过当前时隙必须等下一轮。下一轮什么时候来取决于时隙周期。如果周期是几十毫秒业务帧的容忍延迟是十几毫秒那就直接超时了。现场怎么观察和判断这种问题在实验室很难复现因为实验室的流量模型太干净。现场有突发流量、有重传、有确认帧空口负载是动态的。所以排查必须去现场抓。第一步确认丢包和信道地图更新的时间相关性。做法很简单在后台记录每次地图更新的起始时间戳同时在前端记录每个丢包的时间戳。把两条时间线叠在一起看如果丢包集中在更新开始后的几十到几百毫秒内那基本就锁定了方向。注意不是每次更新都丢包因为还要看当时的业务负载。负载轻的时候探测帧挤一下也无所谓负载重的时候挤一下就崩。第二步看空口上的实际帧序列。用抓包工具抓一段空口数据过滤出探测帧和业务帧看它们的交错模式。重点观察探测帧是否连续占用了多个时隙业务帧被推迟了几个时隙推迟期间有没有确认帧超时这一步能直接看到抢时隙的过程比看计数器直观得多。第三步检查调度器的优先级配置。很多协议栈里探测帧的优先级是可配的。默认配置往往把探测帧设成高优先级理由是地图更新很重要不能延迟。但这个默认值在业务负载高的时候就是灾难。正确的做法是让探测帧在业务空闲时隙发送或者给探测帧设一个可延迟标志允许它在业务帧之后排队。几个容易踩的隐形陷阱第一个陷阱是探测帧的突发性。地图更新不是均匀发探测帧的它往往在短时间内集中发一批比如一次发十几个信道的探测。这批帧如果全部以高优先级插入就会形成一个优先级反转窗口——业务帧在这个窗口内全部被压制。窗口的长度取决于探测帧的数量和每个帧的发送时长。十几个探测帧每个几毫秒窗口就是几十毫秒。业务帧的容忍延迟如果小于这个窗口必然丢包。第二个陷阱是确认帧的连锁反应。业务帧被推迟后接收端可能已经收到了但确认帧回不来因为确认帧也要排队。发送端等不到确认触发重传。重传又遇到探测帧窗口再次被推迟。如此循环一个业务帧可能被重传多次每次都挤在探测帧后面。最终要么超时丢弃要么在某个窗口结束后突然发出去但此时上层已经判定失败了。第三个陷阱是地图更新的触发条件。有些实现里地图更新是周期性的比如每30秒一次。有些是事件驱动的比如误包率超过阈值就触发。事件驱动的更新更危险因为误包率升高往往意味着空口已经拥塞了这时候再插入探测帧等于火上浇油。正确的做法是在触发地图更新之前先检查当前空口负载如果负载超过某个门限就推迟更新或者降低探测帧的发送速率。怎么改以及改完的效果最直接的改法是给探测帧和业务帧分时隙。把时隙周期切成两部分一部分专门给业务帧一部分专门给探测帧。业务帧时隙内探测帧一律不发送探测帧时隙内业务帧可以发送但优先级低于探测帧。这样两者互不干扰代价是空口利用率下降因为探测帧时隙可能空着。如果探测帧很少这个浪费可以接受如果探测帧很多时隙划分就要动态调整。另一种改法是给探测帧加一个可抢占标志。探测帧在发送前先检查当前是否有业务帧在排队。如果有探测帧主动退避等业务帧发完再发。这个改法的好处是不需要划分时隙空口利用率高坏处是地图更新可能被无限期推迟如果业务流量一直很高的话。所以需要一个兜底机制探测帧等待超过一定时间后强制发送哪怕抢占业务帧。这个超时时间要设得比业务帧的容忍延迟大否则还是会丢包。我们最后采用的是混合方案正常负载下探测帧退避负载超过门限后切换到时隙划分模式同时给探测帧设一个最大等待时间超时后强制发送。改完之后连续跑了一周丢包率从千分之几降到了零。但这不是免费的——空口利用率在高峰时段下降了大约百分之五地图更新的完成时间从平均200毫秒延长到了400毫秒。这个取舍要看业务能不能接受。控制指令对延迟敏感对空口利用率不敏感所以这个取舍是划算的。如果是视频流业务可能就要反过来。调试的时候用什么工具看现场调试最有用的是时间戳对齐工具。把后台的更新日志、前端的丢包日志、空口抓包的时间戳全部对齐到同一个时钟基准上。很多团队忽略了时钟同步导致日志对不上排查效率极低。建议在系统里加一个轻量的时间同步机制精度到毫秒级就够了。其次是空口抓包工具。要能过滤出探测帧和业务帧并且能显示每个帧的发送时刻和持续时间。如果抓包工具只能看内容不能看时序那基本没用。时序信息比内容重要得多。最后是队列深度监控。在业务帧的发送队列上挂一个计数器记录队列长度的变化。如果发现队列长度在地图更新期间突然上升然后丢包那就确认了是队列溢出。如果队列长度没变但丢包了那可能是超时丢弃要看超时定时器的配置。一个反直觉的结论这个项目做完之后我最大的体会是低频操作往往比高频操作更危险。高频操作因为一直在跑问题暴露得快容易被发现和修复。低频操作因为不常发生问题被掩盖了一旦发生就是疑难杂症。信道地图更新就是一个典型的低频操作——它可能几分钟才跑一次但每次跑都可能丢包。而丢包的代价在控制类业务里比空口利用率下降百分之五要大得多。所以做设计的时候不要因为某个操作不常发生就忽略它的资源占用。恰恰相反低频操作要特别小心因为它没有足够的运行历史来暴露问题。你得在实验室里主动构造高频触发的场景把低频操作的压力放大才能提前发现这些坑。