Golang 的极速低延迟 Actor 框架
Hollywood 是一个为速度和低延迟应用而构建的超高速 Actor 引擎。适用于游戏服务器、广告代理、交易引擎等场景。它能在 1 秒内处理 1000 万条消息。
什么是 Actor 模型?
Actor 模型是一种用于构建高并发和分布式系统的计算模型。它由 Carl Hewitt 于 1973 年提出,旨在以更可扩展和容错的方式处理复杂系统。
在 Actor 模型中,基本构建块是 Actor(在 Hollywood 中有时称为接收者),它是一个独立的计算单元,通过交换消息与其他 Actor 通信。每个 Actor 都有自己的状态和行为,只能通过发送消息与其他 Actor 通信。这种消息传递范式允许系统高度去中心化和容错,因为即使其他 Actor 失败或不可用,Actor 也可以继续独立运行。
Actor 可以组织成层级结构,高级 Actor 监督和协调低级 Actor。这允许创建复杂的系统,能够以优雅和可预测的方式处理故障和错误。
通过在应用程序中使用 Actor 模型,您可以构建高度可扩展和容错的系统,能够处理大量并发用户和复杂的交互。
特性
- Actor 失败时保证消息传递(缓冲机制)
- 支持即发即忘或请求响应消息传递,或两者兼有
- 高性能 dRPC 作为传输层
- 优化的 proto buffers,无反射
- 轻量级且高度可定制
- 支持集群,用于编写分布式自发现 Actor
基准测试
make bench
生成了 10 个引擎
每个引擎生成 2000 个 Actor
开始发送风暴,将使用 20 个工作线程发送 10 秒
每秒发送的消息数 3244217
..
每秒发送的消息数 3387478
并发发送者:20 发送的消息 35116641,接收的消息 35116641 - 持续时间:10s
每秒消息数:3511664
死信:0
安装
go get github.com/anthdm/hollywood/...
Hollywood 需要 Golang 版本
1.21
快速入门
我们建议您从编写一些本地运行的示例开始。本地运行会更简单一些,因为编译器能够确定使用的类型。在远程运行时,您需要为编译器提供 protobuffer 定义。
Hello world
让我们来看一个 Hello world 消息的例子。完整示例可在 hello world 文件夹中找到。让我们从 main 开始:
engine, err := actor.NewEngine(actor.NewEngineConfig())
这创建了一个新的引擎。引擎是 Hollywood 的核心。它负责生成 Actor、发送消息和处理 Actor 的生命周期。如果 Hollywood 无法创建引擎,它会返回一个错误。对于开发来说,您不应该向引擎传递任何选项,所以可以传递 nil。我们稍后会看看这些选项。
接下来我们需要创建一个 Actor。这些有时被称为 Receivers
,因为它们必须实现接口。让我们创建一个新的 Actor,当它收到消息时会打印一条消息。
pid := engine.Spawn(newHelloer, "hello")
这将导致引擎生成一个 ID 为 "hello" 的 Actor。Actor 将由提供的函数 newHelloer
创建。ID 必须是唯一的。它将返回一个指向 PID 的指针。PID 是进程标识符。它是 Actor 的唯一标识符。大多数情况下,您会使用 PID 向 Actor 发送消息。对于远程系统,您将使用 ID 发送消息,但在本地系统上,您主要使用 PID。
让我们看看 newHelloer
函数和它返回的 Actor。
type helloer struct{}
func newHelloer() actor.Receiver {
return &helloer{}
}
很简单。newHelloer
函数返回一个新的 Actor。Actor 是一个实现 actor.Receiver 的结构体。让我们看看 Receive
方法。
type message struct {}
func (h *helloer) Receive(ctx *actor.Context) {
switch msg := ctx.Message().(type) {
case actor.Initialized:
fmt.Println("helloer 已初始化")
case actor.Started:
fmt.Println("helloer 已启动")
case actor.Stopped:
fmt.Println("helloer 已停止")
case *message:
fmt.Println("hello world", msg.data)
}
}
您可以看到我们定义了一个消息结构体。这是我们稍后会发送给 Actor 的消息。Receive 方法还处理了一些其他消息。这些生命周期消息由引擎发送给 Actor,您将使用这些来初始化您的 Actor。
引擎将 actor.Context 传递给 Receive
方法。这个上下文包含消息、发送者的 PID 和一些其他您可以使用的依赖项。
现在,让我们向 Actor 发送一条消息。我们将发送一个 message
,但您可以发送任何类型的消息。唯一的要求是 Actor 必须能够处理该消息。对于能够跨网络传输的消息,它们必须是可序列化的。对于 protobuf 能够序列化消息,它必须是一个指针。本地消息可以是任何类型。
最后,让我们向 Actor 发送一条消息。
engine.Send(pid, "hello world!")
这将向 Actor 发送一条消息。Hollywood 将把消息路由到正确的 Actor。然后 Actor 将向控制台打印一条消息。
examples 文件夹是学习和进一步探索 Hollywood 的最佳地方。
生成 Actor
当您生成一个 Actor 时,您需要提供一个返回新 Actor 的函数。在生成 Actor 时,有一些可调整的选项可以提供。
使用默认配置
e.Spawn(newFoo, "myactorname")
向构造函数传递参数
有时您可能想向 Actor 构造函数传递参数。这可以通过使用闭包来实现。在 request 示例 中有一个这样的例子。让我们看看代码。
默认构造函数看起来像这样:
func newNameResponder() actor.Receiver {
return &nameResponder{name: "noname"}
}
要构建一个带有名称的新 Actor,您可以这样做:
func newCustomNameResponder(name string) actor.Producer {
return func() actor.Receiver {
return &nameResponder{name}
}
}
然后您可以使用以下代码生成 Actor:
pid := engine.Spawn(newCustomNameResponder("anthony"), "name-responder")
使用自定义配置
e.Spawn(newFoo, "myactorname",
actor.WithMaxRestarts(4),
actor.WithInboxSize(1024 * 2),
actor.WithId("bar"),
)
)
这些选项应该很容易理解。您可以设置最大重启次数,这告诉引擎在发生崩溃时给定的 Actor 应该重启多少次,设置收件箱大小,这设置了收件箱在开始阻塞之前可以容纳多少未处理消息的限制。
作为无状态函数
没有状态的 Actor 可以作为函数生成,因为这样快速简单。
e.SpawnFunc(func(c *actor.Context) {
switch msg := c.Message().(type) {
case actor.Started:
fmt.Println("started")
_ = msg
}
}, "foo")
远程 Actor
Actor 可以通过 Remote 包在网络上相互通信。 这与本地 Actor 的工作方式相同,但是"通过网络"。Hollywood 支持使用 protobuf 进行序列化。
配置
remote.New() 接受一个监听地址和一个 remote.Config 结构体。
您可以使用以下代码实例化一个新的远程:
tlsConfig := TlsConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
}
config := remote.NewConfig().WithTLS(tlsConfig)
remote := remote.New("0.0.0.0:2222", config)
engine, err := actor.NewEngine(actor.NewEngineConfig().WithRemote(remote))
查看 远程 Actor 示例 和 聊天客户端和服务器 以获取更多信息。
事件流
在生产系统中,事情最终会出错。Actor 会崩溃,机器会失败,消息会最终进入死信队列。您可以构建能够以优雅和可预测的方式处理这些事件的软件,方法是使用事件流。
事件流是一个强大的抽象,允许您构建灵活和可插拔的系统,而无需依赖关系。
- 将任何 Actor 订阅到各种系统事件列表
- 向所有订阅者广播您的自定义事件
请注意,未被任何 Actor 处理的事件将被丢弃。您应该有一个 Actor 订阅事件流以接收事件。作为最低限度,您将需要处理 DeadLetterEvent
。如果 Hollywood 无法将消息传递给 Actor,它将向事件流发送 DeadLetterEvent
。
任何满足 actor.LogEvent
接口的事件都将被记录到默认日志记录器中,其严重性级别、消息和事件属性由 actor.LogEvent
的 log()
方法设置。
内部系统事件列表
actor.ActorInitializedEvent
,一个 Actor 已初始化但尚未处理其actor.Started
消息actor.ActorStartedEvent
,一个 Actor 已启动actor.ActorStoppedEvent
,一个 Actor 已停止actor.DeadLetterEvent
,一条消息未能传递给 Actoractor.ActorRestartedEvent
,一个 Actor 在崩溃/恐慌后重新启动actor.RemoteUnreachableEvent
,通过网络向不可达的远程发送消息cluster.MemberJoinEvent
,新成员加入集群cluster.MemberLeaveEvent
,成员离开集群cluster.ActivationEvent
,集群上激活了新的 Actorcluster.DeactivationEvent
,集群上停用了 Actor
事件流示例
有一个 事件流监控示例,展示了如何使用事件流。它包含两个 Actor,一个不稳定,每秒都会崩溃。另一个 Actor 订阅了事件流,并维护了几个不同事件的计数器,如崩溃等。
应用程序将运行几秒钟,然后毒化不稳定的 Actor。然后它会向监视器发送请求查询。由于 Actor 在引擎内部浮动,这是与它们交互的方式。main 将打印查询结果,然后应用程序退出。
定制引擎
我们使用函数选项模式。所有函数选项都在 actor 包中,其名称以 "EngineOpt" 开头。目前,唯一的选项是提供一个远程。这可以通过以下方式完成:
r := remote.New(remote.Config{ListenAddr: addr})
engine, err := actor.NewEngine(actor.EngineOptRemote(r))
addr 是一个格式为 "host:port" 的字符串。
中间件
您可以为您的接收者添加自定义中间件。这对于存储指标、在 actor.Started
和 actor.Stopped
时保存和加载接收者的数据很有用。
有关如何实现自定义中间件的示例,请查看 examples 文件夹中的中间件文件夹。
日志记录
Hollywood 有一些内置的日志记录功能。它将使用 log/slog
包中的默认日志记录器。您可以通过使用 slog.SetDefaultLogger()
设置默认日志记录器来配置您喜欢的日志记录器。这将允许您自定义日志级别、格式和输出。请参阅 slog
包以获取更多信息。
请注意,某些事件