介绍Swift的分布式行为者(附代码)

408 阅读18分钟

我们很高兴地宣布Swift on Server生态系统的一个新的开源包--Swift Distributed Actors,这是一个完整的面向服务器的集群库,用于即将推出的distributed actor 语言功能!

这个库为在服务器使用情况下使用分布式角色提供了完整的解决方案。通过尽早开放这个项目,与正在进行的语言功能工作同时进行,我们希望能收集到更多关于语言功能和相关传输实现形式的有用反馈。

分布式行为体提案

分布式演员是一个早期的、实验性的语言功能。 我们的目标是简化并推动Swift中分布式系统编程的最先进水平,就像我们用本地角色和Swift的结构化并发方法嵌入到语言中的并发编程那样。

目前,我们正在对分布式角色的设计进行迭代。我们希望在提案的投稿主题以及Swift论坛上的分布式角色类别中收集你的反馈、用例和一般想法。提案和这篇博文中描述的库和语言功能在夜间工具链中是可用的,所以请随时下载它们并感受一下该功能。我们会在论坛上发布更新的提案和其他讨论主题,如果你感兴趣,请关注Swift论坛上相应的分类和主题。

我们最感兴趣的是一般的反馈,关于用例的想法,以及你有兴趣承担的潜在传输实现。随着我们对语言功能的成熟和设计,这个库(下面介绍的)将作为这样一个先进而强大的行为体传输的参考实现。如果你对分布式系统感兴趣,我们也非常欢迎你对库本身的贡献,那里也有很多工作要做

很快,我们还将提供一个更完整的 "参考指南"、例子和文章式指南。 这些材料将使用最近开源的DocC文档编译器来编写,将教授这个库的具体模式和使用情况。

这些拟议的语言功能--就像所有的语言功能一样--在解除其实验状态之前,将经过适当的Swift进化过程。我们邀请社区参与,通过审查、贡献和分享经验来帮助我们塑造语言和API。非常感谢你的参与!

这个项目是作为 "早期预览 "发布的,它的所有API都有可能被改变,甚至被删除,没有任何事先警告。

该库依赖于未发布的、正在进行的、以及Swift Evolution审查中的语言功能。因此,我们还不能推荐在生产中使用它--该库可能依赖于工具链的特定夜间构建,等等。

提前开源这个库的主要目的是证明有能力使用distributed actor 语言特性实现功能完整、引人注目的聚类解决方案,并使两者同步发展。

分布式行为体概述

分布式行为体是Swift 并发模型的下一步发展方向。

通过在语言中内置行为体,Swift为开发者提供了一个安全、直观的并发模型,非常适合许多应用。由于先进的语义检查,编译器可以指导和帮助开发者编写没有低级数据竞赛的程序。不过,这些检查并不是行为者模型有用的地方:与其他并发模型不同,行为者模型对于分布式系统的建模也是非常有价值的。 由于位置透明的分布式行为体的概念,我们可以使用熟悉的行为体的概念来对分布式系统进行编程,然后随时将其转移到分布式环境中,例如,集群环境。

通过分布式角色,我们旨在简化和推动分布式系统编程的技术水平,就像我们用本地角色和Swift语言中嵌入的结构化并发模型进行并发编程一样。

不过,这种抽象并不打算完全掩盖分布式调用正在穿越网络的事实。在某种程度上,我们正在做相反的事情,在编程时假设调用可能是远程的。这个小而关键的观察使我们能够建立主要用于分布式的系统,并可在本地测试集群中进行测试,甚至可以有效地模拟各种错误情况。

分布式行为体与(本地)行为体相似,因为它们封装了自己的状态,完全通过异步调用进行通信。分布式方面为该等式增加了一些额外的隔离、类型系统和运行时考虑。然而,该功能的表面感觉与本地角色非常相似。下面是一个分布式角色声明的小例子:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****
// 1) Actors may be declared with the new 'distributed' modifier
distributed actor Worker {

  // 2) An actor's isolated state is only stored on the node where the actor lives.
  //    Actor Isolation rules ensure that programs only access isolated state in
  //    correct ways, i.e. in a thread-safe manner, and only when the state is
  //    known to exist.
  var data: SomeData

  // 3) Only functions (and computed properties) declared as 'distributed' may be accessed cross actor.
  //    Distributed function parameters and return types must be Codable,
  //    because they will be crossing network boundaries during remote calls.
  distributed func work(item: String) -> WorkItem.Result {
    // ...
  }
}

分布式角色带走了很多模板,我们通常在每次创建一些分布式RPC系统时都要建立和重新发明这些模板。毕竟,在这个片段中,我们并不关心确切的序列化和网络细节;我们声明了我们需要完成的事情,并在网络上发送了工作请求这种省略模板的做法是相当强大的,我们希望你会喜欢以这种身份使用行为体,除了它们的并发性方面。

为了让一个分布式角色参与到某个分布式系统中,我们必须为它提供一个ActorTransport ,一个用户可实现的库组件,负责执行所有必要的网络,以进行远程函数调用。开发者在分布式行为体的实例化过程中提供他们选择的传输方式,就像这样:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****

// 4) Distributed actors must have a transport associated with them at initialization
let someTransport: ActorTransport = ...
let worker = Worker(transport: someTransport)

// 5) Distributed function invocations are asynchronous and throwing, when performed cross-actor,
//    because of the potential network interactions of such call.
//
//    These effects are applied to such functions implicitly, only in contexts where necessary,
//    for example: when it is known that the target actor is local, the implicit-throwing effect
//    is not applied to such call.
_ = try await worker.work(item: "work-item-32")

// 6) Remote systems may obtain references to the actor by using the 'resolve' function.
//    It returns a special "proxy" object, that transforms all distributed function calls into messages.
let result = try await Worker.resolve(worker.id, using: otherTransport)

这篇文章在很高的层面上总结了分布式行为体的特性。我们鼓励有兴趣的人阅读Swift Evolution 中的完整提案,并在Swift 论坛的分布式角色类别中提供反馈或提出问题。

你可以在 Swift 论坛和Swift Evolution 上关注并提供关于distributed actor 语言提案的反馈。目前,完整的草案也可供查阅,不过我们预计不久后会对其进行重大修改。

我们希望听到你的反馈,并看到你参与到这个令人兴奋的新功能的 Swift Evolution 评审中来!

分布式角色传输实现

Swift 标准库本身并没有提供任何具体的传输。相反,它专注于定义语言模型和扩展点,传输实现可以用来实现分布式角色的特定传输。

我们打算启用新的、令人兴奋的传输实现。该标准库定义了一个ActorTransport 协议,任何人都可以实现该协议,以便在独特和引人注目的用例中利用分布式角色。潜在的传输实现的例子包括但不限于集群系统、基于web-socket的消息传递,甚至是分布式角色的进程间通信。

构建行为体传输不是一项简单的任务,我们只期望少数成熟的实现最终能登上舞台。

介绍一下:分布式行为体集群传输

今天,我们宣布开源发布Swift分布式角色库--一个用于构建分布式系统的全功能框架Swift。它是上述ActorTransport 协议的一个实现,可以作为其他传输作者的参考实现。

这个集群库专注于服务器端的点对点系统,通常用于需要与多方进行 "实时 "互动的系统,如:存在系统、游戏大厅、监控或物联网系统,以及经典的 "控制面 "系统,如协调器、调度器等。

该库利用SwiftNIO,即Swift的高性能服务器端重点网络库,来实现集群的网络层。该集群还提供了一个会员服务,基于去年早些时候开源的Swift集群会员库。这意味着你可以在独立的模式下使用这个集群,而不需要启动额外的服务发现或数据库服务。我们认为这是一项重要的能力,因为它简化了一些裸机场景的部署,并使利用这一集群技术在其他场景中成为可行,否则由于资源限制,这是不可能的。

该集群被设计成具有很强的可扩展性,并且可以带来你自己的大部分核心组件的实现,包括节点发现、故障检测等。

分布式行为体系统使行为体能够形成一个集群,相互发现并相互通信,而不需要像以前那样需要进行低级的网络编程。在接下来的章节中,我们将展示建立这种分布式行为体系统的一些基本步骤。

形成集群

为了使分布式行为体名副其实,让我们马上关注一个多节点的场景。我们将启动两个节点,让它们形成一个集群。这些代码片段在同一个过程中执行这个任务,但当然,这种系统的意图是最终在多个独立的机器上运行。这样做并没有什么不同,我们稍后会讨论这个问题。

在同一进程中创建多个集群节点的能力突出了集群的另一个有用的能力:可以在进程中编写你的分布式系统测试,让它们在内存中通信或通过实际网络通信--这两种情况的唯一区别是传递给每个角色的传输方式。这使得我们可以一次性开发分布式角色,然后测试、运行和部署相同的代码,但配置略有不同。我们可以在以下两种情况下运行同一组分布式角色:

  • 单节点集群,使用单个进程--只是假装是分布式的,这对早期和本地开发很有用。
  • 多个集群节点,但共享同一个进程--这在测试中很有用,因为我们可以为我们的分布式系统编写单元测试,并让它使用实际的网络,甚至是模拟消息丢失或延迟的传输。
  • 多个集群节点,在实际不同的物理机器上 - 这是生产中此类系统的通常部署策略。

形成集群需要一些关于在哪里可以找到集群的其他节点的知识。首先,让我们展示一下形成集群的相同进程但有许多节点的方式,因为这是在本地测试中经常使用的方式:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****

let first = ActorSystem("FirstNode") { settings in
  settings.cluster.enable(host: "127.0.0.1", port: 7337)
}
let second = ActorSystem("SecondNode") { settings in
  settings.cluster.enable(host: "127.0.0.1", port: 8228)
}

first.cluster.join(host: "127.0.0.1", port: 8228)
// or convenience API for local testing:
// first.cluster.join(node: second.settings.cluster.node)

行为者系统暴露了许多关于集群状态的有用功能,以及它可以通过.cluster 属性执行的行动,比如joining 其他节点进入集群。

如果集群已经有了多个节点,那么只需要一个节点加入一个新的节点,其他所有的节点就能最终了解这个新节点。成员信息会在整个集群中自动传播。

在一个生产系统中,我们不会像这样硬编码加入过程。生产部署通常有某种形式的节点服务发现,由于Swift服务发现,我们可以很容易地利用这些服务来发现和自动加入节点到我们的集群。Swift Service Discovery在发现机制上提供了一个抽象的API,并可以支持DNS记录或Kubernetes服务发现等后端。我们可以使用一个假想的DNS发现机制来发现节点:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****

let third = ActorSystem("Third") { settings in
  settings.cluster.enable()
  settings.cluster.discovery = ServiceDiscoverySettings(
    SomeExistingDNSBasedServiceDiscovery(), // or any other swift-service-discovery mechanism
    service: "my-actor-cluster" // `Service` type aligned with what DNSBasedServiceDiscovery expects
  )
}

// automatically joins all nodes that DNSBasedServiceDiscovery finds for "my-actor-cluster"

这种配置将导致系统定期查询DNS的服务记录,并尝试将任何新发现的节点加入到我们的集群中。

发现分布式行为体

在第一次学习分布式角色时,一个常见的问题是:"我如何找到一个远程角色?"因为为了获得一个远程引用,我们需要获得一个特定的ActorIdentity ,以提供给运行时,然而,不可能 "只是猜测 "一个远程角色的正确标识符。

值得庆幸的是,集群为这个问题提供了一个解决方案!我们称之为 模式。我们把它称为Receptionist 模式--因为类似于酒店,演员需要在前台登记(和离开),以便其他人能够找到他们。这种签到在设计上是可选的,不是自动的,因为并不是所有的分布式行为体都想向所有其他行为体宣传他们的存在,而只是向一些他们认识和信任的人宣传。

接待者从两方面进行交互,一是向其注册的行为体,二是对收听特定接待键的更新感兴趣的行为体。

首先,让我们看看一个分布式行为体如何在已知的接收密钥下在集群中宣传自己:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****
distributed actor FamousActor {
  init(transport: ActorSystem) async {
    await transport.receptionist.register(self, withKey: .famousActors)
  }
}

extension DistributedReception.Key {
  static var famousActors: Self<FamousActor> { "famous-actors" }
}

当我们在接收机上注册一个特定的行为体时,它将自动在网络上与集群中的其他节点闲谈这一信息,并确保所有节点都知道这个著名的行为体。

在集群的其他节点上,我们可以监听关于著名演员密钥的更新,当集群中出现新的演员时,我们会简单地得到通知。在这里,我们使用Swift的AsyncSequence 功能来消费这个潜在的无限的更新流:

// **** SYNTAX BASED ON CURRENT PROPOSAL TEXT AND LIBRARY -- NOT FINAL APIs ****
for try await famousActor in transport.receptionist.subscribe(.famousActors) {
  print("Oh, a new famous actor appeared: \(famousActor.id)")
  // we can use the famousActor right away and send messages to it
}

我们也可以向接待员询问某个特定键下已知的单个或所有演员,而不是订阅更新。

接待员模式为我们提供了一种类型安全的方式来宣传和发现角色,而不必担心我们如何实现这一目标的确切网络细节。

对集群和角色生命周期事件的反应

集群提供了推理角色生命周期的能力,不管他们是在同一地点,还是在某个远程计算节点上。这个功能被认为是允许分布式角色互相 "监视 "终止。

每当一个被监视的角色被取消初始化,或者它所运行的节点被确定为 "停机",就会向所有监视其生命周期的角色发出一个关于这个角色的终止信号。该集群使用去年早些时候开源的Swift集群成员库来检测失效的节点,并将其沿着生命周期图移动,如下图所示:

Cluster lifecycle diagram

集群事件以AsyncSequence<Cluster.Event> 。这种序列总是以集群当前状态的 "快照 "开始,然后是自那一刻起发生的任何变化。这可以用来实现一个函数,例如等待集群达到一定大小:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****

var membership: Membership = .empty

// "infinite" stream of cluster events
for try await event in system.cluster.events {
  print("Cluster event: \(event)")

  // events can be applied to membership to
  try membership.apply(event)

  if membership.count(atLeast: .up) > 3 { // membership has useful utility functions
    break
  }
}

如果需要,我们还可以检查具体的事件。请参考Cluster.Membership 的文档,以了解更多关于集群状态的所有事件类型和可用信息。

虽然这不是大多数开发者将与之互动的API的水平。行为体集群会自动将相关事件转化为行为体生命周期信号,所以我们不必每次都去监听集群事件,而是可以监听特定的行为体,万一该行为体所在的整个节点被终止,我们也会得到相关通知。这个功能被称为LifecycleWatch ,使用方法如下:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****

// distributed actor Person {}
let other: Person
let system: ActorSystem

watchTermination(of: other) { terminatedIdentity in
  system.log.info("Actor terminated: \(terminatedIdentity)"
}

重要的是,监视API不会保留行为体,因此也不会保持它的生命力--否则终究不会观察到终止。

分布式角色可能需要通过将它们的强引用存储在某种 "管理者 "角色、某种注册表中,或者让接收者保留它们(例如,直到它们取消注册,或者发生其他情况)来保持自己的活力。

例子:分布式工人池

最后,我们可以把所有这些功能放在一起,展示如何利用行为体集群建立一个分布式工作池样本。

由于集群的服务发现和故障检测机制,我们不需要实现任何特别的东西,以便在集群中添加新的节点,或在它们被终止时删除它们。相反,我们可以专注于行动者本身,因为集群机制将自动把集群事件转化为关于分布式行动者的相应事件。

首先,让我们准备一个WorkerPool 分布式行为体。它将用一个工作者密钥订阅接收者,并将集群中出现的所有工作者添加到池中。当他们终止时,它会将他们从它所维护的池中删除:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****

extension Reception.Key {
  static var workers: Self<Worker> { "workers" }
}

distributed actor WorkerPool {
  var workers: Set<Worker> = []

  init(transport: ActorSystem) async {
    Task {
      for try await worker in transport.receptionist.subscribe(.workers) {
        workers.insert(worker)
        watchTermination(of: worker) {
          workers.remove($0) // thread-safe!
        }
      }
    }
  }

  distributed func submit(work item: WorkItem) async throws -> Result {
    guard let worker = workers.shuffled.first else {
      throw NoWorkersAvailable()
    }
    try await worker.work(on: item)
  }
}

WorkerPool ,除了是一个分布式行为体之外,还收获了作为一个actor 的通常好处:我们可以安全地修改workers 变量,而不必关心或担心线程问题--由于行为体隔离,行为体保证了这种属性的并发安全。

工作者池使用了两个集群功能:接待员发现新的工作者,以及生命周期观察,以便在他们终止时移除他们。这足以实现一个完全管理的对等体集合,它将随着工人节点的加入和离开集群而被动态更新。

工作者的实现也是相当简短的。我们需要确保所有Worker 行为体在初始化时向接待员注册,所以我们将在行为体的异步初始化器中完成这一工作。我们不需要做任何其他事情,就可以让接待员在集群中自动提供对该工作者的引用。当工作者解除初始化,或者它所运行的整个节点崩溃时,其他系统上的接待员会自动将其转化为各自系统上的终止信号:

// **** APIS AND SYNTAX ARE WORK IN PROGRESS / PENDING SWIFT EVOLUTION ****

distributed actor Worker {
  init(transport: ActorSystem) async {
    await transport.receptionist.register(self, withKey: .workers)
  }

  distributed func work(on item: WorkItem) async -> Result {
    // do the work
  }
}

这就是了!接待员将自动与集群中的其他对等体闲谈,介绍在工作者键下加入接待的新工作者实例。集群中任何订阅了接待员更新的其他角色都可以发现这些角色并与之联系。

你也可能已经注意到,我们没有深入地实现任何网络、请求/回复匹配,甚至没有对一些电线格式进行编码/解码。所有这些都是由语言功能与ActorSystem传输实现协作处理的。有一些方法可以定制其中的许多方面,然而在简单的情况下--我们不需要担心这个问题!

因此,除了系统初始化(配置节点发现)外,这确实是你为准备一个分布式工人池所需要编写的所有代码。我们希望这个小例子能给你带来启发,让你大致了解这个功能能带来什么样的用例。关于分布式行为体,还有很多东西需要学习和发现,但现在我们就不多说了。