本文介绍了 gossipsub v1.0 协议,这是一个针对 libp2p 的扩展型发布订阅协议。该协议通过建立随机主题网和使用 gossip 技术,旨在克服 floodsub 协议带来的带宽和可扩展性问题。文中详细阐述了协议的动机、工作原理、控制消息及其处理流程,并提供了相关参数和状态管理的信息。
生命周期阶段 | 成熟度 | 状态 | 最新修订 |
---|---|---|---|
3A | 推荐 | 活跃 | r2, 2020-03-12 |
作者: @vyzo
编辑: @yusefnapora
兴趣小组: @yusefnapora, @raulk, @whyrusleeping, @Stebalien, @jamesray1, @vasco-santos, @daviddias, @yiannisbot
请参阅 生命周期文档 以获取有关成熟度等级和规范状态的背景信息。
这是一个基于随机主题网格和 gossip 的可扩展 pubsub 协议的规范。它是一个通用的 pubsub 协议,具有适中的放大因子和良好的可扩展性。该协议被设计为可以通过更专业的路由器进行扩展,这些路由器可以添加协议消息和 gossip,以便提供针对特定应用程序配置优化的行为。
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> 目录
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
libp2p pubsub 接口规范 定义了对等方之间交换的 RPC 消息,但特意不定义路由语义、连接管理或其他有关对等方如何交互的具体细节。这留给具体的 pubsub 协议,从而使协议设计在支持不同用例方面具有很大的灵活性。
在 介绍 gossipsub 本身之前,我们首先来看一下 floodsub
的特性,这是最简单的 pubsub 实现。
pubsub 接口的初始实现是 floodsub
,它采用了一种非常简单的消息传播策略——它仅仅通过让每个对等方将所知的每个其他对等方广播来“淹没”网络中的消息。
通过 flooding,路由几乎是微不足道的:对于每个传入消息,转发给在该主题下所有已知的对等方。会有一些逻辑,因为路由器维护一个以前消息的定时缓存,以便已见消息不再被转发。它也绝不将消息转发回源或转发消息的对等方。
floodsub 路由策略具有以下高度理想的特性:
然而,问题是消息不仅沿着最小延迟路径传递;它们沿着所有边缘传播,从而造成淹没。网络的外发度是没有上限的,而我们希望它是有界的,以减少带宽需求并增加去中心化和可扩展性。
这种没有上限的外发度为个别密集连接节点带来了问题,因为它们可能有大量连接的对等方,无法承担转发所有这些 pubsub 消息的带宽。同样,放大因子仅受覆盖网络中所有节点度之和的限制,这在大规模密集连接的覆盖网络中造成了可扩展性问题。
gossipsub 通过对每个对等方的外发度施加上限,并全局控制放大因子,来解决 floodsub 的关键短板。
为此,gossipsub 对等方形成一个覆盖网格,在这个网格中,每个对等方将消息转发给其对等方的一个子集,而不是主题下的所有已知对等方。这个网格是随着对等方加入 pubsub 主题时构建的,并通过交换 控制消息 来维持。
网格的初始构建是随机的。当一个对等方加入一个新主题时,它将检查其 本地状态 以找出它已知的属于该主题的其他对等方。然后,它会选择一个主题成员的子集,最多为 D
,这是一个可配置的 参数,表示网络所需的度。这些将对于该主题添加到网格中,新添加的对等方将通过 GRAFT
控制消息 通知。
当离开某个主题时,对等方会通过 PRUNE
消息 通知其网格成员,并将网格从其 本地状态 中移除。通过 心跳过程 定期执行 进一步的维护,以保持网格大小在可接受的范围内,因为对等方的连接和断开。
网格链接是双向的——当一个对等方接收到通知他们已被添加到另一个对等方的网格的 GRAFT
消息时,它会假设自己仍然订阅该主题,在自己的网格中也添加该对等方。处于稳态(在 消息处理 之后),如果对等方 A
在对等方 B
的网格中,那么对等方 B
也在对等方 A
的网格中。
为了让对等方能够在关于主题的网格视图之外“连接”,我们使用 gossip 在网络中传播关于消息流的 元数据。这一 gossip 是发射给不在网格中的随机子集的对等方。我们可以将网格成员视为“完整消息”对等方,向他们传播在主题中所有接收到的消息的完整内容。我们所了解的主题中的其余对等方可以视为“仅元数据”对等方,我们在 定期间隔 发送 gossip。
元数据可以是任意的,但作为基础,我们发送 IHAVE
消息,它包括我们在最后几秒中看到的消息的消息 ID。这些消息被缓存,以便接收 gossip 的对等方可以使用 IWANT
消息 请求它们。
路由器可以使用此元数据改进其网格,例如,基于 gossipsub 构建的 episub 路由器可以创建适合少量发布者向更大受众广播的流行传播树。
gossip 的其他可能用途包括在覆盖网络的不同点重新启动消息传输,以纠正下游消息的丢失,或通过机会跳过跳跃加速对可能与网格中较远的对等方的消息传输。
Pubsub 被设计为适应 libp2p 的“生态系统”,其中的模块化组件提供互补的功能。因此,某些关键功能假定已经存在,并且未在 pubsub 本身中指定。
在对等方能够交换 pubsub 消息之前,它们必须首先意识到彼此的存在。可用几种对等发现机制,例如:用于局域网的 MulticastDNS、通过 libp2p-kad-dht 的随机游走、碰面协议,以及任何符合 libp2p 的对等发现接口的机制。
由于对等发现是广泛有用且与 pubsub 无关,因此 pubsub 接口规范 和本文档都不规定特定的发现机制。相反,这一功能假定由环境提供。一个启用了 pubsub 的 libp2p 应用程序还必须配置对等发现机制,该机制将发送环境连接事件,以通知其他 libp2p 子系统(如 pubsub)新连接的对等方。
每当新对等方连接时,gossipsub 实现会检查该对等方是否实现了 floodsub 和/或 gossipsub,如果是,便向其发送一个 hello 数据包,公告其当前订阅的主题。
本节列出了控制 gossipsub 行为的可配置参数,以及简短的描述和合理的默认值。每个参数的完整上下文在本文档的其他地方介绍。
参数 | 目的 | 合理的默认值 |
---|---|---|
D |
网络所需的外发度 | 6 |
D_low |
外发度的下限 | 4 |
D_high |
外发度的上限 | 12 |
D_lazy |
(可选)用于 gossip 发射的外发度 | D |
heartbeat_interval |
心跳 之间的时间 | 1 秒 |
fanout_ttl |
每个主题扇出状态的生存时间 | 60 秒 |
mcache_len |
消息缓存中的历史窗口数量 | 5 |
mcache_gossip |
发射 gossip 时使用的历史窗口数量 | 3 |
seen_ttl |
已看消息 ID 缓存的过期时间 | 2 分钟 |
请注意 D_lazy
被认为是可选的。它用于控制在 发射 gossip 时的外发度,此值可能与急切消息传播的度得单独调整。默认情况下,我们为两者使用相同的 D
。
路由器跟踪一些必要的状态,以维护稳定的主题网格并发出有用的 gossip。
该状态大致可分为两个类别:对等状态 和 消息缓存 相关状态。
对等状态是路由器如何跟踪其所知的支持 pubsub 的对等方及与每个对等方的关系。
对等状态的三大主要部分:
peers
是支持 gossipsub 或 floodsub 的所有已知对等方的 ID 集。
在本文档中,peers.gossipsub
将表示支持 gossipsub 的对等方,而 peers.floodsub
则表示 floodsub 对等方。
mesh
是订阅主题到我们所处网格中的对等方集合的映射。
fanout
,像 mesh
一样,是主题到对等方集合的映射,但 fanout
映射包含我们 未 订阅的主题。
除了上述 gossipsub 专用状态外,libp2p pubsub 框架也维护一些“路由器无关”的状态。这包括我们所订阅的主题集合,以及每个对等方所订阅的主题集合。我们在本文档中的其他地方会提到 peers.floodsub[topic]
和 peers.gossipsub[topic]
来表示特定主题中支持 floodsub 或 gossipsub 的对等方。
消息缓存(或 mcache
)是一个数据结构,用于存储消息 ID 及其对应消息,分段为“历史窗口”。每个窗口对应一个心跳间隔,而这些窗口在 心跳过程 中与 gossip 发射 之后进行移动。保留的历史窗口数量由 mcache_len
参数 决定,而在发送 gossip 时检查的窗口数量由 mcache_gossip
控制。
消息缓存支持以下操作:
mcache.put(m)
:将消息添加到当前窗口和缓存。mcache.get(id)
:通过 ID 从缓存中检索消息(如果仍然存在的话)。mcache.get_gossip_ids(topic)
:检索给定主题中最近的历史窗口的消息 ID,检查的窗口数由 mcache_gossip
参数控制。mcache.shift()
:移动当前窗口,丢弃旧于缓存的历史长度 (mcache_len
) 的消息。我们还保持一个 seen
缓存,这是一个定时的最近最少使用缓存,包含我们最近观察到的消息 ID。“最近”的值由 参数 seen_ttl
决定,合理的默认值为两分钟。该值应选择得靠近覆盖网络中的传播延迟,保持在健康的范围内。
seen
缓存有两个目的。在所有 pubsub 实现中,我们可以在转发消息之前,首先检查 seen
缓存,以避免重复发送相同的消息。特别是对于 gossipsub,seen
缓存用于处理另一个对等方发送的 IHAVE
消息,从而仅请求我们之前未见过的消息。
在 Go 实现中,seen
缓存由 pubsub 框架提供,且与 mcache
是分开的,但其他实现可能希望将其合并为一个数据结构。
pubsub 接口规范 定义了所有 libp2p pubsub 路由器使用的基线 RPC 消息格式。作为 RPC 消息的一部分,对等方可以包括关于它们希望订阅或退订的主题的公告。这些公告会发送给所有已知的 pubsub 能力的对等方,无论我们目前是否有共同的主题。
在本文档中,我们假设底层的 pubsub 框架负责发送公告订阅更改的 RPC 消息。一个不基于现有的 libp2p pubsub 框架的 gossipsub 实现需要实现这些控制 RPC 消息。
除了 pubsub 框架发送的 SUBSCRIBE
/ UNSUBSCRIBE
事件外,gossipsub 还必须进行额外工作,以维护它正在加入或离开的主题的网格。我们将下面两个主题成员资格操作称为 JOIN(topic)
和 LEAVE(topic)
。
当应用程序调用 JOIN(topic)
时,路由器将通过首先从其 本地对等状态 中检查 fanout
映射,选择最多 D
个对等方来形成主题网格。如果在 fanout[topic]
中有对等方,路由器将把这些对等方从 fanout
映射移动到 mesh[topic]
。如果主题不在 fanout
映射中,或者 fanout[topic]
中的对等方少于 D
,路由器将尝试使用属于该主题的所有 gossipsub 能力对等方填充 mesh[topic]
。
无论这些对等方来自 fanout
还是 peers.gossipsub
,路由器都会通过发送 GRAFT
控制消息 通知新成员 mesh[topic]
,他们已被添加到网格中。
应用程序可以调用 LEAVE(topic)
以退订主题。路由器会通过发送 PRUNE
控制消息 通知 mesh[topic]
中的对等方,以便他们可以从自己的主题网格中删除链接。在发送 PRUNE
消息后,路由器会忘记 mesh[topic]
并将其从本地状态中删除。
控制消息用于维护主题网格和发射 gossip。本节列出了核心 gossipsub 协议中的控制消息,值得注意的是,对 gossipsub 的扩展(如 [episub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/episub)可能会为其特定目的定义进一步的控制消息。
有关 gossipsub 路由器如何响应控制消息的详细信息,请参见 消息处理。
控制消息的 protobuf 架构在 Protobuf 部分有详细介绍。
GRAFT
消息在主题网格中 graft 一个新链接。GRAFT
通知一个对等方,它已被添加到本地路由器对于包含的主题 ID 的网格视图。
PRUNE
消息从主题网格中修剪网格链接。PRUNE
通知一个对等方,它已从本地路由器对于包含的主题 ID 的网格视图中删除。
IHAVE
消息作为 gossip 发射。它向远程对等方提供一个最近被本地路由器看到的消息列表。然后,远程对等方可以通过 IWANT
消息 请求完整的消息内容。
IWANT
消息请求完整的一条或多条消息的内容,这些消息的 ID 在远程对等方的 IHAVE
消息 中被公告。
接收到消息后,路由器将首先处理消息负载。负载处理将根据应用程序定义的规则验证消息,并检查 seen
缓存 以确定消息是否已被处理。它还将确保它不是消息的来源;如果路由器接收到自己发布的消息,将不会进一步转发。
如果消息有效、未由路由器自身发布,且以前未见过,该路由器将转发消息。首先,它将把消息转发给所有 peers.floodsub[topic]
中的对等方,以便向 floodsub 具备兼容性。其次,它将把消息转发给其本地 gossipsub 主题网格 mesh[topic]
中的每个对等方。
处理消息负载后,路由器将处理控制消息:
接收到 GRAFT(topic)
消息 时,路由器将检查自己是否确实订阅消息中标识的主题。如果是,路由器会将发送者添加至 mesh[topic]
。如果路由器不再订阅该主题,则将向发送者响应 PRUNE(topic)
消息,以通知他们应当移除其网格链接。
接收到 PRUNE(topic)
消息 时,路由器会将发送者从 mesh[topic]
中移除。
接收到 IHAVE(ids)
消息 时,路由器将检查其 seen
缓存。如果 IHAVE
消息包含未见过的消息 ID,路由器将用 IWANT
消息请求这些消息。
接收到 IWANT(ids)
消息 时,路由器将检查其 mcache
,并将 mcache
中存在的任何请求的消息转发给发送了 IWANT
消息的对等方。
除了转发接收到的消息外,路由器当然还可以代表自身发布消息,这些消息源自应用程序层。这与转发接收到的消息非常相似:
peers.floodsub[topic]
中的每个对等方。mesh[topic]
中的所有对等方。fanout[topic]
中的对等方集合。如果这个集合为空,路由器将从 peers.gossipsub[topic]
中选择最多 D
个对等方并将它们添加到 fanout[topic]
。假设此时 fanout[topic]
中有一些对等方,路由器将向每个发送消息。Gossip 和其他控制消息不必以其自己的消息形式传输。相反,它们可以集中在任何其他消息中进行搭载,以便于任何主题中的常规消息流。每当主题之间存在某种相关流时,这可以减少消息传输速率,这对于密集连接的对等方来说可以是显著的。
有关搭载实现细节,请参阅 Go 实现。
每个对等方运行一个周期性的稳定化过程,称为“心跳过程”,间隔时间是规律的。心跳的频率由 参数 heartbeat_interval
控制,合理的默认值为 1 秒。
心跳有三个功能:网格维护、扇出维护 和 gossip 发射。
主题网格通过以下稳定化算法进行维护:
for each topic in mesh:
if |mesh[topic]| < D_low:
select D - |mesh[topic]| peers from peers.gossipsub[topic] - mesh[topic]
; 即不包括已经在主题网格中的对等方。
for each new peer:
add peer to mesh[topic]
emit GRAFT(topic) control message to peer
if |mesh[topic]| > D_high:
select |mesh[topic]| - D peers from mesh[topic]
for each new peer:
remove peer from mesh[topic]
emit PRUNE(topic) control message to peer
算法的 参数 包括:
D
: 网络所需的外发度D_low
: D
的可接受下限。如在特定主题网格中对等方少于 D_low
,则尝试添加新对等方。D_high
: D
的可接受上限。如在特定主题网格中对等方多于 D_high
,则随机选择对等方进行移除。通过跟踪每个主题的最后发布时刻来维护 fanout
映射。如果我们在一个可配置的 TTL 内未向某个主题发布任何消息,则将丢弃该主题的扇出状态。
我们还尝试确保每个 fanout[topic]
集合至少有 D
个成员。
扇出维护算法如下:
for each topic in fanout:
if time since last published > fanout_ttl
remove topic from fanout
else if |fanout[topic]| < D
select D - |fanout[topic]| peers from peers.gossipsub[topic] - fanout[topic]
add the peers to fanout[topic]
算法的 参数 包括:
D
: 网络所需的外发度。fanout_ttl
: 我们为每个主题保留的扇出状态的时间。如果在 fanout_ttl
内未向某个主题发布,则将丢弃 fanout[topic]
集合。gossip 被发射到每个主题中随机选择的,对等方,它们尚未是主题网格的成员:
for each topic in mesh+fanout:
let mids be mcache.get_gossip_ids(topic)
if mids is not empty:
select D peers from peers.gossipsub[topic]
for each peer not in mesh[topic] or fanout[topic]
emit IHAVE(mids)
shift the mcache
请注意,我们使用相同的参数 D
作为 gossip 和网格成员资格的目标度,然而这不是规范性要求。可以使用一个单独的参数 D_lazy
来明确控制 gossip 传播因子,从而实现对消息的急切和懒散传输之间权衡的调整。
gossipsub 协议扩展了 现有的 RPC
消息结构,并添加了一个新字段 control
。这是一个 ControlMessage
的实例,可以包含一个或多个控制消息。
四个控制消息分别为 ControlIHave
(用于 IHAVE
消息)、ControlIWant
(用于 IWANT
消息)、ControlGraft
(用于 GRAFT
消息)和 ControlPrune
(用于 PRUNE
消息)。
Protobuf 如下所示:
syntax = "proto2";
message RPC {
// ... 请参见 pubsub 接口规范中的定义
optional ControlMessage control = 3;
}
message ControlMessage {
repeated ControlIHave ihave = 1;
repeated ControlIWant iwant = 2;
repeated ControlGraft graft = 3;
repeated ControlPrune prune = 4;
}
message ControlIHave {
optional string topicID = 1;
repeated bytes messageIDs = 2;
}
message ControlIWant {
repeated bytes messageIDs = 1;
}
message ControlGraft {
optional string topicID = 1;
}
message ControlPrune {
optional string topicID = 1;
}
- 原文链接: github.com/libp2p/specs/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!