介绍 Swift 分布式 Actor
我们非常激动地宣布为 Swift on Server 生态系统推出一个新的开源软件包 Swift Distributed Actors,这是一个完整的面向服务器的集群库,用于即将推出的 distributed actor
语言特性!
该库为在服务器用例中使用分布式 actor 提供了完整的解决方案。通过尽早开源这个项目,并配合正在进行的语言特性开发工作,我们希望收集更多关于语言特性形状和相关传输实现的有用反馈。
分布式 Actor 提案
分布式 actor 是一项早期且实验性的语言特性。我们的目标是像我们使用本地 actor 的并发编程和 Swift 语言中嵌入的结构化并发方法一样,简化和推动 Swift 中分布式系统编程的先进水平。
目前,我们正在迭代分布式 actor 的设计。我们希望在提案的 pitch 线程 以及 Swift 论坛上的 Distributed Actors 类别 中收集您的反馈、用例和一般想法。提案和这篇博文中描述的库和语言特性在 nightly toolchains 中可用,所以请随时下载并体验该特性。我们将在论坛上发布更新的提案和其他讨论主题,如果您有兴趣,请关注 Swift 论坛上相应的类别和主题。
我们最感兴趣的是一般性反馈、关于用例的想法以及您可能有兴趣承担的潜在传输实现。随着我们语言特性的成熟和设计,该库(如下介绍)将作为一种先进且强大的 actor 传输的参考实现。如果您对分布式系统感兴趣,也非常欢迎为该库本身做出贡献,并且那里还有很多工作要做!
很快,我们还将提供更完整的“参考指南”、示例和文章风格的指南。这些材料将使用最近开源的 DocC 文档编译器编写,将教授该库启用的特定模式和用例。
这些提议的语言特性——与所有语言特性一样——在解除其实验性状态之前,将经历适当的 Swift 演进过程。我们邀请社区参与并帮助我们通过审查、贡献和分享经验来塑造语言和 API。非常感谢您的提前参与!
本项目以“早期预览”形式发布,其所有 API 都可能发生更改,甚至在没有任何事先警告的情况下被删除。
该库依赖于未发布、正在开发中和 Swift 演进审查待定的语言特性。因此,我们目前不建议在生产环境中使用它——该库可能依赖于 toolchains 的特定 nightly 构建版本等。
尽早开源该库的主要目的是证明有能力使用 distributed actor
语言特性实现一个功能完整、引人注目的集群解决方案,并同时发展两者。
分布式 Actor 概览
分布式 actor 是 Swift 并发模型演进的下一步。
借助语言内置的 actor,Swift 为开发者提供了一种安全且直观的并发模型,非常适合许多应用程序。得益于先进的语义检查,编译器可以引导和帮助开发者编写没有底层数据竞争的程序。然而,actor 模型的用处不仅限于这些检查:与其他并发模型不同,actor 模型对于建模分布式系统也非常有价值。得益于位置透明分布式 actor 的概念,我们可以使用熟悉的 actor 概念来编程分布式系统,然后轻松地将其迁移到分布式环境(例如,集群环境)。
通过分布式 actor,我们的目标是简化和推动分布式系统编程的先进水平,就像我们使用本地 actor 的并发编程和 Swift 语言中嵌入的结构化并发模型所做的那样。
然而,这种抽象并不打算完全隐藏分布式调用正在跨越网络的事实。在某种程度上,我们正在做相反的事情,并假设调用可能是远程的进行编程。这个虽小但至关重要的观察结果使我们能够构建主要用于分布式的系统,并在本地测试集群中进行测试,这些集群甚至可以有效地模拟各种错误场景。
分布式 actor 与(本地)actor 类似,因为它们通过异步调用专门封装其状态进行通信。分布式方面为该等式增加了一些额外的隔离、类型系统和运行时考虑因素。但是,该特性的表面感觉与本地 actor 非常相似。这是一个分布式 actor 声明的小例子
// **** 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 {
// ...
}
}
分布式 actor 消除了我们在每次创建一些分布式 RPC 系统时通常必须构建和重新发明的许多样板代码。毕竟,在代码片段中,我们并不关心确切的序列化和网络细节;我们声明了我们需要完成的工作,并在网络上发送了工作请求!这种样板代码的省略非常强大,我们希望您除了并发方面之外,还会喜欢在这种能力中使用 actor。
为了让分布式 actor 参与到某些分布式系统中,我们必须为其提供一个 ActorTransport
,这是一个用户可实现的库组件,负责执行所有必要的网络操作以进行远程函数调用。开发者在实例化分布式 actor 期间提供他们选择的传输方式,如下所示
// **** 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)
这篇文章总结了分布式 actor 特性的一个非常高的层次。我们鼓励有兴趣的人阅读 Swift Evolution 中提供的完整提案,并在 Swift 论坛上的 Distributed Actors 类别 中提供反馈或提出问题。
您可以在 Swift 论坛和 Swift Evolution 上关注并提供关于 distributed actor
语言提案的反馈。当前的完整草案也已可供审查,尽管我们预计很快会对其进行重大更改。
我们很乐意听取您的反馈,并期待您参与这项激动人心的新特性的 Swift Evolution 审查!
分布式 Actor 传输实现
Swift 标准库本身不提供任何特定的传输方式。相反,它专注于定义语言模型和扩展点,传输实现可以使用这些扩展点来实现分布式 actor 的特定传输。
我们旨在启用新的和令人兴奋的传输实现。标准库定义了一个 ActorTransport
协议,任何人都可以实现该协议,以便在独特且引人注目的用例中利用分布式 actor。潜在的传输实现示例包括但不限于:集群系统、基于 web-socket 的消息传递,甚至用于分布式 actor 的进程间通信。
构建 actor 传输并非易事,我们预计最终只有少数成熟的实现会登上舞台。
介绍:分布式 Actor 集群传输
今天,我们宣布开源发布 Swift Distributed Actors 库 - 一个用于构建分布式 Swift 系统的功能齐全的框架。它是上述 ActorTransport
协议的实现,可以作为其他传输作者的参考实现。
这个集群库专注于服务器端对等系统,这些系统通常用于需要与多方进行“实时”交互的系统中,例如:在线状态系统、游戏大厅、监控或物联网系统,以及经典的“控制平面”系统,例如编排器、调度器等。
该库使用 SwiftNIO,Swift 的高性能服务器端专用网络库,来实现集群的网络层。该集群还提供了一个成员服务,该服务基于去年早些时候开源的 Swift Cluster Membership 库。这意味着您可以在独立模式下使用此集群,而无需启动额外的服务发现或数据库服务。我们认为这是一项重要的能力,因为它简化了某些裸机场景中的部署,并使在其他情况下利用此集群技术成为可能,否则由于资源限制可能无法实现。
该集群被设计为非常可扩展的,并且可以引入您自己实现的绝大多数核心组件,包括节点发现、故障检测等。
分布式 actor 系统使 actor 能够形成集群,彼此发现并相互通信,而无需进行原本必要的底层网络编程。在接下来的部分中,我们将展示构建此类分布式 actor 系统所需的一些基本步骤。
形成集群
为了让分布式 actor 名副其实,让我们立即关注多节点场景。我们将启动两个节点并使它们形成一个集群。代码片段在一个相同的进程中执行此任务,但当然,这种系统的目的是最终跨多个独立的机器运行。这样做并没有什么不同,我们稍后会讨论这一点。
在同一进程中创建多个集群节点的能力突出了集群的另一个有用功能:可以进程内编写分布式系统测试,并让它们在内存中通信或通过实际网络通信 - 这两种情况之间的唯一区别是传递给每个 actor 的传输方式。这允许我们一次开发分布式 actor,然后在略微不同的配置中测试、运行和部署相同的代码。我们可以在以下任一环境中运行同一组分布式 actor:
- 单节点集群,使用单个进程 - 仅假装是分布式的,这对于早期和本地开发可能很有用,
- 多集群节点,但共享同一进程 - 这在测试中很有用,因为我们可以为分布式系统编写单元测试,并让它使用实际的网络,甚至使用模拟消息丢失或延迟的传输方式,
- 多个集群节点,在实际不同的物理机器上 - 这是此类系统在生产中的常用部署策略。
形成集群需要一些关于集群中其他节点位置的知识。首先,让我们展示形成集群的同进程但多节点方式,因为这是本地测试中经常使用的方式
// **** 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)
actor 系统通过 .cluster
属性公开了许多关于集群状态和它可以执行的操作的有用功能,例如将其他节点 joining
到集群中。
如果集群已经有多个节点,则只需一个节点加入新节点,所有其他节点最终都会了解这个新节点。成员信息会在整个集群中自动传播。
在生产系统中,我们不会像这样硬编码加入过程。生产部署通常具有某种形式的可用节点服务发现,并且由于 Swift Service Discovery,我们可以轻松地利用这些服务来发现节点并自动将节点加入到我们的集群中。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 以获取服务记录,并尝试将任何新发现的节点加入到我们的集群中。
发现分布式 Actor
首次了解分布式 actor 时,一个常见的问题是“我如何找到一个远程 actor?” 因为为了获得远程引用,我们需要获得特定的 ActorIdentity
以提供给运行时,但“仅仅猜测”远程 actor 的正确标识符是不可能的。
值得庆幸的是,集群为这个问题提供了一个解决方案!我们称之为 接待员
模式 - 因为类似于酒店,actor 需要在接待处登记入住(和退房),以便其他人能够找到它们。此登记是可选的且非自动的,这是经过设计的,因为并非所有分布式 actor 都一定希望向所有其他 actor 公告它们的存在,而只向少数几个它们认识和信任的 actor 公告。
接待员从双方进行交互,actor 向其注册,以及对监听特定接待键更新感兴趣的 actor。
首先,让我们看看分布式 actor 如何在集群中以已知的接待键公告自己
// **** 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" }
}
当我们在接待员处注册特定 actor 时,它将自动在网络上与其他集群节点传播此信息,并确保所有节点都知道这个著名的 actor。
在集群的其他节点上,我们可以监听关于著名 actor 键的更新,当集群中出现新的已知 actor 时,我们会收到通知。在这里,我们使用 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
}
也可以向接待员询问单个或所有在特定键下已知的 actor,而不是订阅更新。
接待员模式为我们提供了一种类型安全的方式来公告和发现 actor,而无需担心我们如何实现此目的的确切网络细节。
对集群和 Actor 生命周期事件做出反应
集群提供了推理 actor 生命周期能力,无论它们是位于同一计算节点上还是位于某些远程计算节点上。此功能表现为允许分布式 actor “监视”彼此的终止。
每当被监视的 actor 被反初始化,或者运行它的节点被确定为“宕机”时,就会发出关于该 actor 的终止信号,发送给任何正在监视其生命周期的 actor。集群使用去年早些时候开源的 Swift Cluster Membership 库来检测节点故障,并按照下图所示的生命周期图移动它们
集群事件作为 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 级别。actor 集群自动将相关事件转换为 actor 生命周期信号,因此,我们不必每次想要监视 actor 的生命周期时都监听集群事件,而是可以监视特定 actor,并且如果 actor 所在的整个节点终止,我们也会收到通知。此功能称为 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)"
}
重要的是,watch API 不会保留 actor,因此不会使其保持活动状态 - 否则,将永远不会观察到终止。
分布式 actor 可能需要通过在某种“管理器”actor、某些注册表或让接待员保留它们(例如,直到它们注销或发生某些其他条件)中存储对它们的强引用来保持自身活动状态。
Example: Distributed Worker Pool
最后,我们可以将所有这些特性结合在一起,展示如何构建一个利用 Actor 集群的分布式工作池示例。
得益于集群的服务发现和故障检测机制,我们无需实现任何特殊功能来添加新节点(当新节点添加到集群时)或移除节点(当节点终止时)。相反,我们可以专注于 Actor 本身,因为集群机制会自动将集群事件转换为关于分布式 Actor 的相应事件。
首先,让我们准备一个 WorkerPool
分布式 Actor。它将使用 worker 键订阅接待员,并将集群中出现的所有 worker 添加到池中。当这些 worker 终止时,它会将它们从其维护的池中移除。
// **** 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)
}
}
除了作为分布式 Actor 之外,WorkerPool
还受益于作为 actor
的通常优势:我们可以安全地修改 workers
变量,而无需关心或担心线程问题——由于 Actor 隔离,Actor 保证了此属性的并发安全性。
工作池使用两个集群功能:接待员来发现新的 worker,以及生命周期监控,以便在 worker 终止时将其移除。这足以实现一组完全托管的对等节点,这些节点将随着 worker 节点的加入和离开集群而动态更新。
worker 的实现也很简短。我们需要确保所有 Worker
Actor 在初始化时向接待员注册自己,因此我们将在 Actor 的异步初始化程序中执行此操作。我们无需执行任何其他操作,接待员即可自动使对该 worker 的引用在集群中可用。当 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
}
}
就是这样!接待员将自动与集群中的其他对等节点传播关于新的 worker 实例加入 workers 键下的接待的信息。集群中任何其他订阅接待员更新的 Actor 随后都可以发现这些 Actor 并联系它们。
您可能还注意到,我们不必深入研究实现任何网络、请求/回复匹配,甚至从某种线路格式进行任何编码/解码。所有这些都由语言特性与 ActorSystem 传输实现协同处理。有很多方法可以自定义这些方面的许多内容,但是在简单的情况下——我们无需担心它!
因此,除了系统初始化(配置节点发现)之外,这真的是您需要编写的所有代码来准备分布式工作池。我们希望这个小例子能够启发您,并让您大致了解此功能实现的用例类型。关于分布式 Actor 还有很多东西需要学习和发现,但现在我们先到此为止。