Swift服务器生态系统的一个新的开源项目--Swift Cluster Membership

88 阅读7分钟

我很高兴地宣布Swift服务器生态系统的一个新的开源项目--Swift Cluster Membership。这个库旨在帮助Swift在服务器应用的新领域中成长:集群式多节点分布式系统。通过这个库,我们提供了可重用的、与运行时间无关的成员协议实现,可以在各种集群用例中采用。

背景介绍

集群成员协议是分布式系统的一个重要构件,如计算密集型集群、调度器、数据库、键值存储等。随着这个软件包的发布,我们的目标是使构建这样的系统更加简单,因为它们不再需要依赖外部服务来为它们处理服务成员。我们还想邀请社区合作,开发更多的会员协议。

成员协议的核心是为 "谁是我的(活的)同伴"这一问题提供答案。".在一个分布式系统中,这个看似简单的任务其实并不简单,在这个系统中,延迟或丢失的消息、网络分区、无反应但仍然 "活着 "的节点都是日常的面包和黄油。为这个问题提供一个可预测的、可靠的答案就是集群成员协议的作用。

在实施成员协议时,人们可以进行各种权衡,它仍然是一个有趣的研究和继续完善的领域。因此,集群成员协议包打算不专注于单一的实现,而是作为这一领域中各种分布式算法的协作空间。

今天,伴随着这个包的首次发布,我们开放了一个这样的成员协议的实现SWIM。

🏊🏾♀️🏊🏻♀️🏊🏾♂️🏊🏼♂️ SWIMming with Swift

我们正在开源的第一个成员协议是一个实现 可扩展的弱一致的感染式进程组成员制协议(或 "SWIM")的实现,以及2018年Lifeguard中记录的一些值得注意的协议扩展。用于更准确的故障检测的本地健康意识》的论文。

SWIM是一种八卦协议,其中对等体定期交换他们对其他节点状态的观察信息位,最终将信息传播给集群中的所有其他成员。这类分布式算法对任意的信息丢失、网络分区和类似问题有很强的弹性。

在高水平上,SWIM是这样工作的:

  • 一个成员周期性地ping一个它所知道的随机选择的对等体。它的做法是向该对等体发送一个.ping 消息,并期待一个.ack 被送回来。在下图中可以看到A 最初是如何探测B
    • 交换的消息也携带八卦有效载荷,这是关于消息发送者所知道的其他对等体的(部分)信息,以及他们的成员状态(.alive,.suspect, 等)。
  • 如果它收到一个.ack ,该对等体被认为仍然是.alive 。否则,目标对等体可能已经终止/崩溃或因其他原因而无响应。
    • 为了仔细检查对等体是否真的停机,原点通过向配置好的其他对等体发送.pingRequest 消息来询问其他对等体的状态,然后向该对等体发出直接ping(下图中的探测对等体E )。
  • 如果这些ping失败,原点对等体将收到.nack ("负确认")消息,由于缺乏.ack,导致该对等体被标记为.suspect

image

上述机制,不仅是一个故障检测机制,也是一个八卦机制,它承载着集群中已知成员的信息。这样一来,成员最终会了解到他们的同伴的状态,即使没有把他们全部列在前面。然而,值得指出的是,这种成员视图是*弱一致性的*,这意味着不能保证(或在没有额外信息的情况下,有办法知道)所有成员在任何给定时间点上都有相同的确切的成员视图。然而,对于更高级别的工具和系统来说,它是一个很好的构件,可以在上面建立更强的保证。

一旦故障检测机制检测到一个无响应的节点,它最终会被标记为.dead ,导致其不可逆转地从集群中移除。我们的实现提供了一个可选的扩展,在可能的状态中增加了一个.unreachable ,然而大多数用户不会发现它是必要的,它被默认禁用。关于合法状态转换的细节和规则,请参考 SWIM.Status或下面的图表。

image

Swift集群成员资格实现协议的方式是提供它们的 "实例"。例如,SWIM的实现被封装在与运行时无关的SWIM.Instance ,它需要由网络运行时和实例本身之间的一些胶合代码 "驱动 "或 "解释"。我们把实现中的这些胶水部分称为 "Shells",库中有一个使用SwiftNIODatagramChannel 实现的SWIMNIOShell ,它通过UDP异步执行所有消息传递。其他的实现可以使用完全不同的传输方式,或者在其他现有的流言系统上捎带SWIM消息等等。

SWIM 实例还内置了对排放指标的支持(使用swift-metrics),并可通过传递swift-log Logger 配置为记录内部细节。

例子:重用 SWIM 协议的逻辑实现

本库的主要目的是在需要某种形式的进程内成员服务的各种实现中共享SWIM.Instance 实现。在项目的README中深入记录了实现自定义运行时的情况,所以如果你对通过不同的传输方式实现SWIM感兴趣,请看看那里。

实现一个新的传输可以归结为一个 "填空 "练习:

首先,我们必须使用自己的目标传输实现对等协议

public protocol SWIMPeer: SWIMAddressablePeer {
    func ping(
        payload: SWIM.GossipPayload,
        from origin: SWIMAddressablePeer,
        timeout: DispatchTimeInterval,
        sequenceNumber: SWIM.SequenceNumber,
        onComplete: @escaping (Result<SWIM.PingResponse, Error>) -> Void
    )
    // ...
}

这通常意味着包裹一些连接、通道或其他身份,使其能够发送消息并在适用时调用适当的回调。

然后,在对等体的接收端,必须实现接收这些消息并调用SWIM.Instance上定义的所有相应的on<SomeMessage> 回调(归入SWIMProtocol)。这些调用在内部执行所有SWIM协议的具体任务,并返回指令,这些指令是对实现如何对消息做出反应的简单解释 "命令"。例如,在收到一个PingRequest 消息时,返回的指令可以指示一个shell到.sendPing(target:pingRequestOrigin:timeout:sequenceNumber) ,这可以通过在target 对等物上调用这个ping 来简单实现。

例子:使用SwiftNIO的SWIMming

存储库包含一个端到端示例和一个名为SWIMNIOExample的实施示例,它利用SWIM.Instance ,以实现一个简单的基于UDP的对等体监控系统。这使得对等体可以通过发送由SwiftNIO驱动的数据报,使用SWIM协议进行闲聊并相互通知节点故障情况。

SWIMNIOExample的实现只是作为一个例子,并没有考虑到在生产中使用,但只要花点力气,它肯定能很好地满足某些使用需求。如果你有兴趣了解更多关于集群成员算法、可扩展性基准和使用SwiftNIO本身的信息,这是一个很好的模块,可以让你一展身手,也许一旦这个模块足够成熟,我们可以考虑让它不仅仅是一个例子,而是一个基于SwiftNIO的集群应用程序的可重用组件。

在最简单的形式下,结合所提供的SWIM实例和SwiftNIO外壳来构建一个简单的服务器,人们可以像下面所示那样将所提供的处理程序嵌入到典型的SwiftNIO通道管道中:

let bootstrap = DatagramBootstrap(group: group)
    .channelInitializer { channel in
        channel.pipeline
            // first install the SWIM handler, which contains the SWIMNIOShell:
            .addHandler(SWIMNIOHandler(settings: settings)).flatMap {
                // then install some user handler, it will receive SWIM events:
                channel.pipeline.addHandler(SWIMNIOExampleHandler())
        }
    }

bootstrap.bind(host: host, port: port)

然后,这个例子的处理程序可以接收和处理SWIM集群成员变化事件:

final class SWIMNIOExampleHandler: ChannelInboundHandler {
    public typealias InboundIn = SWIM.MemberStatusChangedEvent

    let log = Logger(label: "SWIMNIOExampleHandler")

    public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        let change = self.unwrapInboundIn(data)
        self.log.info(
            """
            Membership status changed: [\(change.member.node)]\
             is now [\(change.status)]
            """,
            metadata: [
                "swim/member": "\(change.member.node)",
                "swim/member/status": "\(change.status)",
            ]
        )
    }
}

下一步是什么?

这个项目目前处于预发布状态,我们希望在发布稳定版之前给它一些时间进行烘烤。我们主要关注SWIM.Instance的质量和正确性,然而在Swift NIO实现的例子中还有一些小的清理工作要做。一旦我们确认一些用例对SWIM实现的API有信心,我们就想标记一个1.0版本。

从那时起,我们希望继续研究其他的成员实现,并尽量减少现有实现的开销。