为什么节点一多,BLE Mesh 就开始现原形
做 BLE Mesh 的人,大概率都经历过这样一个阶段:几十个节点的时候一切正常,群组控制、状态同步都很快;一旦节点数堆到几百上千,网络就突然变得不可控——指令延迟从几百毫秒涨到几秒,偶尔还有节点收不到消息,重试又进一步加剧拥塞。

问题通常不在协议本身,而在于我们对“节点数量、消息转发、网络收敛”这三者之间的关系理解得不够工程化。BLE Mesh 的地址空间理论上能容纳三万个节点,但理论容量和实际可用的组网密度是两回事。这篇文章主要聊泛洪机制在规模变大之后卡在哪里,以及哪些优化手段能真正落地。
先理解转发模型:受控泛洪不是路由
BLE Mesh 不是路由网络。它没有路由表,不维护端到端路径。一条消息发出后,所有使能了中继(relay)的节点都会在 TTL 耗尽之前把它重新广播出去。转发成本因此不是点对点的线性成本,而是和中继节点数量直接相关的乘法成本。
一个 500 节点的网络,如果 400 个节点开着 relay,一条只有几十字节的指令,理想情况下会被复制成上千次空中广播事件。单条消息无所谓,但群组指令、状态上报、心跳、OTA 这些流量叠在一起,2.4GHz ISM 频段很快就会被打满。真正先出问题的往往不是“能不能到达”,而是“信道里同时飘着多少条重复消息”。
# 受控泛洪的转发逻辑(简化)
def on_mesh_packet(pkt):
key = (pkt.src, pkt.seq, pkt.dst)
if key in message_cache: # 已经转发过,直接丢弃
return
message_cache.add(key)
if pkt.ttl <= 1: # TTL 只剩一跳
deliver_to_upper_layer(pkt)
return
if relay_enabled and in_same_subnet(pkt):
pkt.ttl -= 1
for _ in range(relay_retransmits):
send_on_adv_channels(pkt) # ch37 / ch38 / ch39
deliver_to_upper_layer(pkt)
这段转发逻辑里有四个直接影响大规模组网的变量:relay 是否使能、TTL 设定、relay 重发次数、消息缓存大小。前两个在工程里最常见,也最容易想当然;缓存则通常在出问题之后才被想起来。
不少人拿 ZigBee 和 BLE Mesh 类比。两者虽然都面向物联网自组网,转发模型完全不同,网络规划思路不能直接搬。
| 对比项 | ZigBee 自组网 | BLE Mesh |
|---|---|---|
| 转发方式 | 路由发现,按路径转发 | 受控泛洪,逐跳广播 |
| 路由维护 | 拓扑变化需要重新收敛 | 没有路由表 |
| 参与转发的节点 | 路径上的节点 | TTL 范围内所有 relay |
| 主要瓶颈 | 路由表规模和路由收敛开销 | 信道竞争与消息风暴 |
把 ZigBee 的组网经验直接搬到 BLE Mesh 上通常不好用。ZigBee 靠“少而精”的路径规划来省流量,BLE Mesh 必须直接面对广播域里的每一个中继节点。这也是大规模组网里一系列问题的最底层原因。
大规模网络里常见的四个误区
在项目里见过比较多的错误假设有这四个:
- BLE Mesh 会像路由网络一样自动选路。它连路由表都没有,消息是朝 TTL 允许的所有方向扩散。按路由思维去规划,会高估它的可预测性。
- relay 开得越多越可靠。密集部署时,中继越多意味着同一份消息的副本越多,冲突和缓存淘汰越严重,最后表现为丢消息和收敛变慢。
- TTL 调大、重传调多就能解决丢包。这两者增加的是冗余覆盖,但每一份冗余都要占用信道。负载不高的网络里有效,负载高的网络里反而会加速拥塞。
- 分段消息有逐段确认,大包可以放心发。组播场景下分段消息没有逐段确认,任何一段丢失,整个消息就要重传。这是大规模 OTA 很难做的直接原因。
工程优化:把泛洪成本从“默认全员”压到“够用”
优化本质是控制两类变量:参与复制的节点数量,以及消息扩散的范围。下面六条是从实际项目里反复验证过的方向。
1. 只保留骨干中继节点
设备默认开 relay 在产品上最省事,但大规模组网必须把转发名额收回来。按物理分布保留部分骨干节点转发,比如每个分区、每 5 到 8 米保留一两个,其他节点关闭 relay 只做收发。1000 节点的网络,relay 从 1000 降到 200,单条消息的空中广播量会下降一个量级。代价是网络对骨干节点的依赖增加,骨干离线可能造成覆盖空洞。因此骨干要保留冗余,验收阶段最好做一次骨干失效测试。
2. TTL 按区域和目标来设
默认 TTL=7 是协议栈的缺省值,适合小规模验证,不适合直接搬到大网。同一房间用 TTL=1 就能覆盖,同一楼层通常 TTL=2 足够,跨楼层、跨区域才需要更大的值。工程上可以把 TTL 和发布目标绑定:本地小场景用低 TTL,全局场景才放开。收益不仅是减少转发次数,还能减少不同区域之间的流量互相污染。
3. 消息缓存要按消息速率设计
每个中继节点维护一个消息缓存,用来判断“这条消息我是不是已经转发过”。缓存条目数有限,消息速率一高,旧条目会被提前淘汰。如果一条消息的副本在缓存淘汰之后又被另一个邻居转发过来,节点会再次转发,形成隐性的重复放大。缓存容量应按“每秒需要去重的消息数 × 消息在网内的存活时长”来估算,转发负载高的节点可以分配更大的缓存。上线后要观察缓存命中率,而不是只看内存够不够。
4. OTA 必须分批、限速
OTA 是泛洪网络最苛刻的考验。一份常见大小的固件会拆成成百上千条组播分段消息,每一条都在相关范围内泛洪。几百个节点同时升级,信道会瞬间饱和,然后整批节点一起失败。实践上只能分批推进:每批二三十个节点进入升级,批次之间留间隔,避免上一批的尾部流量和下一批的头部流量叠加。失败节点单独重试,不要整组重试。BLE Mesh 1.1 的 DFU 模型同样围绕分批分波次设计,1.0 时代的项目也要按这个节奏来控制。
5. 心跳和周期上报也要计入网络预算
周期上报是最容易被忽略的流量源。每个节点每周期一条组播消息,几百个节点按 10 秒周期上报,平均每秒就有几十条消息在全网泛洪,峰值更高。工程上可以三件事同时做:把周期拉到业务能接受的极限;改成变化上报或按需查询;让周期消息只在小范围传播,不经过整个广播域。
6. 协议选型没锁定的话,认真评估 Mesh 1.1
BLE Mesh 1.1 引入了子网(Subnet),可以把一个物理大网划分成多个逻辑转发域,消息只在相关子网内中继,相当于从协议层面收窄泛洪范围。远程配置(Remote Provisioning)允许通过已入网节点去配置高处或难以触达的设备,能省掉大量部署人力。子网需要更仔细的地址和密钥规划,但大型多区域场景下,它的收益远大于在 1.0 上硬撑。
几类常见转发策略的取舍可以这样对比:
| 策略 | 适用规模 | 收敛表现 | 主要风险 |
|---|---|---|---|
| 全员转发(默认配置) | 原型验证、百节点以内 | 节点增多后迅速恶化 | 消息风暴、缓存失效 |
| 骨干中继(稀疏泛洪) | 几百到几千节点 | 可预测,p95 可控 | 骨干失效产生覆盖空洞 |
| 子网隔离(Mesh 1.1) | 大型多区域网络 | 分区收敛,互不干扰 | 子网间交互需要额外设计 |
| Mesh 加集中网关混合 | 高时延敏感业务 | 时延低,网关成单点 | 部署复杂、成本高 |
怎样验证网络收敛是否合格
优化有没有效果,不能靠感觉,要靠可重复的收敛测试。做法是周期性向目标组播一条探测消息,节点收到后立刻上报接收时间戳,网关侧统计分布。这里的收敛既指单条消息到达目标节点的速度,也指网络在负载下的稳定性。
# 收敛测试伪代码
for i in range(20):
start = now_ms()
publish_group(0xC101, 'PING', ttl=optimized_ttl)
latencies = collect_acks(timeout=5000) # 节点上报各自收到时间
p95 = percentile(latencies.values(), 95)
print('round %d: p95=%d ms, reach=%d/300' % (i, p95, len(latencies)))
判断标准可以很简单:固定负载下,p95 是否随测试轮次持续升高。升高说明网络在超负荷边缘,需要继续降中继、降重传、收 TTL 或者拆子网。规模压测尽量放在真实射频环境里做。实验室里节点挨着节点的密集分布,和现场隔墙、跨楼层的分布,表现完全不一样。
大规模 BLE Mesh 组网,最划算的投资是在部署图阶段就把转发边界画清楚:哪些节点转发、转发给谁、TTL 到哪里为止。这些决策越早定,后面踩的坑越少。
写到最后
BLE Mesh 的协议机制并不复杂,难的是在真实规模下控制泛洪的扩散。节点数量决定了网络的宽度,消息转发策略决定了网络的成本,网络收敛是对这两者的最终检验。
回到开头的问题:整楼几千个节点还想保持秒级收敛,能做,但前提是别让所有节点都当中继,别用默认 TTL 走全网,别把大包消息一次性灌进组播。把这三件事落地,再配上一套可重复的收敛验证方法,大规模组网就会从一个“玄学问题”变成一个可量化、可优化的工程问题。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/994/