mesh 圈子里只有八个邻居:GossipSub 靠心跳清单补齐传播 图 1
mesh 圈子里只有八个邻居:GossipSub 靠心跳清单补齐传播 · 图 1

节点把一条消息送出去之后,它是怎么在几秒内铺满几千台机器的?靠”逢人就发”会立刻把带宽烧光,靠”只发给固定的几个人”又会在网络边缘断流。GossipSub 给出的答案是把扩散拆成两件事:订阅者所在的小圈子负责真正把消息递过去,圈子之外只在心跳时刻发一份”我这里有什么”的清单。这个圈子在规范里叫 mesh。它既决定了消息的传播路径,也决定了某些节点能对网络看到多少,所以理解 mesh 就是理解这层传输的信任边界。

转发与清单是两条不同的路

一条消息抵达后,节点把它转发给该话题 mesh 内的全部邻居;这是唯一”真正把内容递出去”的动作。到了心跳时刻(参考实现的默认节拍是一秒一次),节点另外做一件事:挑一部分非 mesh 邻居,发一批 IHAVE 消息,内容是”我缓存里有哪些消息编号”。收到清单的邻居如果想要,用 IWANT 回指,对方再补发正文。也就是说,mesh 决定正文流向,清单决定谁有机会发现自己错过了东西。

能被发现的时间窗由缓存长度决定。规范用两个参数控制:HistoryLength 决定消息编号在缓存里存多少个心跳,HistoryGossip 决定其中多少个心跳内会被写进 IHAVE。这两个值一旦设小,“晚知道”的邻居就来不及问回来,传播就只能依赖随机图上的偶遇。

三个字母控制的圈子大小

订阅圈子与心跳清单共同完成消息扩散的机制示意

mesh 不是一成不变的拓扑,节点每轮心跳都会检查自己在这个话题里的邻居数量,并做增删。参数表里最关键的是三档:D 是理想度数,即节点希望每个话题维持多少 mesh 邻居;Dlo 是下限,低于它就在下一次心跳尝试 graft(拉人进圈);Dhi 是上限,超过它就要 prune(请一部分人出去)。参考实现把这三个默认值分别取作八、六、十二,实现不同可以改,但三者的相对顺序是规范约束。

被踢出圈子的一方不是被动等待。PRUNE 消息里可以带一个退避秒数,对方必须等满这段时间才能重新发起 graft,提前动手不仅会被拒,还会在自己的分数上留下记录。这条退避在 v1.1 里被明确写进规范,同版本还允许 PRUNE 附带一份对等名单(规范里的 Peer Exchange),用来缓解”必须先有一个可靠的节点发现服务才能组网”这个前置压力。至于显式对等(直连伙伴),它们被安排在所有 mesh 之外:新消息一律转发、来的 RPC 一律接受,对它们发起 graft 属于协议错误,应当回一条 PRUNE。

剪圈子的取舍同样有参数:Dscore 规定因子订阅过剩而修剪时,至少要保留多少个评分高的邻居。这类”按分数选人”的做法依赖 v1.1 引入的 peer scoring——把在圈子里待了多久、是否首次投递、圈内投递表现、是否发来无效消息这几项折算成一个分值。分值本身不由规范固定死,各实现自己调,官方文档给出的是调参指南和衰减规则。

边界在哪里

mesh 结构是效率工具,同时也是观察面。一个节点如果对”自己并未订阅的话题”照常收消息却从不转发,它能观察流量却不出力;规范用评分、 opportunistic grafting(在长期投递差的话题上试探性换邻居)和显式对等等机制削弱这类行为,但都不等于彻底消除。把 GossipSub 说成”抗审查""绝对安全”是越界描述:它能限制单点的破坏半径,不能把不可信邻居组成的网络变成可信网络。

对搭建节点的人来说,可核对的事实是这几条:某话题的 mesh 度是否长期贴近 Dlo(说明邻居不够或反复被 prune)、PRUNE 退避是否被频繁触发、心跳是否真的在跑。这些参数的当前默认值应查阅所用实现的文档与规范文件,不同版本会调整,本文只描述机制结构,不为任何具体数值作长期承诺。本文是协议机制解释,不构成任何投资建议。