1. 1.3 服务级 Actor vs 对象级 Actor
在 Actor 粒度 上,业界常见两档:对象级(细、多)与 服务级(粗、少)。路由方式、监督形状与状态存放位置随之不同。本节讨论其差异与在 Go 环境下的常见取舍;与 zhenyi 的直接对应见 1.3.7。
1.1. 1.3.1 两种流派
1.1.1. 对象级 Actor(Erlang / Akka 等语境下常见)
往往一实体一 Actor,例如:
100 万在线用户 → 100 万个 UserActor
1 万个房间 → 1 万个 RoomActor
单个 Actor 很轻,调度、生命周期、迁移多由专用运行时承担。
特点概览:
- 数量可达百万、千万级
- 监督结构常较深
- 位置透明与迁移是体系的常规能力
- 路由、序列化等与平台紧耦合
1.1.2. 服务级 Actor(粗粒度 / 分片式)
一个可对外命名的服务或分片对应少量 Actor,例如:
IM 业务 → 少量进程内实例(按负载再分片)
匹配业务 → 少量实例
入口 → 少量统一接入实例
单进程内 Actor 数量常为数十量级;大量业务实体状态存放在各 Actor 内部的 map、切片等结构中。
特点概览:
- Actor 个数少
- 监督常扁平
- 实体状态集中在少数 Actor 的内部表结构
- 入口层 + 分片规则将请求导向正确的处理实例(网关、路由、目录等名称因项目而异)
后文将这一档简称为 服务级。
1.2. 1.3.2 在 Go 环境下,服务级为何更常出现
这通常不是单一「架构评审」的结论,而是 语言成本与工程范围 共同作用的结果。
Goroutine 与 Erlang process 的成本模型不同:数量极大时,栈与调度相关开销不可忽视;Go 也未内置「百万级轻进程 + 全局邮箱 + 内建分布」的 Actor 机。
若在 Go 上坚持对象级全套:海量 Actor 的调度、生命周期、迁移、深层监督与全局路由,接近重做一层运行时,工作量通常显著高于在服务级形态下把复杂度收进「少数处理主线 + 内部数据结构」。
服务级时:每条主线仍是「邮箱 + 顺序处理」,但实体态落在 map 等结构的条目上;调度由少量 goroutine 承担;监督常用单层策略即可;路由由统一入口与分片规则完成。对中小团队而言,更易在可维护性与性能之间取得平衡。
对照表(概括用,非严格定理):
| 维度 | 对象级 | 服务级 |
|---|---|---|
| 调度 | 强依赖平台 | 少量 goroutine + 进程内结构 |
| 生命周期 | 平台托管多 | Init → Run → Close 等常规模式 |
| 跨节点 | 常围绕单 Actor 迁移 | 常围绕分片或业务层搬迁 |
| 监督 | 深树常见 | 扁平常见 |
| 路由 | 全局 Actor 寻址 | 入口 → 分片 → 本地查表 |
许多 Go 系实时框架 偏向服务级,与「是否合乎某经典 Actor 定义」关系不大,更多与交付与维护成本有关。本书示例代码也采用这一路。
1.3. 1.3.3 同一业务的两条叙事(示意)
1.3.1. 对象级
用户 A → 发消息给用户 B:
1. 投递到 UserActor(B)
2. 在 UserActor(B) 内处理(B 的状态附着于该 Actor)
3. 再投递到 B 的下行路径(连接、推送等)
B 所在节点由全局目录等机制解析;Actor 迁移时更新目录,调用形态可保持稳定。
1.3.2. 服务级
用户 A → 发消息给用户 B:
1. 入口收到报文
2. 按规则选择承载 B 所在分片的业务 Actor
3. 在该 Actor 内的表中访问 B 的状态并处理
4. 下行经入口或约定出口送达 B 的连接
B 所在分片由 userId → 分片 等规则决定;语义上选择的是分片与服务实例,而非全局唯一的「UserActor 进程句柄」。
1.3.3. 差异小结
| 方面 | 对象级 | 服务级 |
|---|---|---|
| 用户态主要位置 | 独占的 UserActor | 分片 Actor 内表项 |
| 路由目标 | 具体 Actor | 分片 + 表查找 |
| 扩容时常见操作 | 迁移单个轻量 Actor | 调整分片映射与数据面搬迁 |
对象级在模型上强调一实体一隔离单元;服务级在工程中强调少量实例扛大规模实体。在 Go 生态中,后者更为常见。
1.4. 1.3.4 服务级的常见代价
1.4.1. 内部并发
在单线程顺序处理消息的前提下,进程内表结构往往无需外加锁:
// 示意:仅当消息始终在同一线程处理时成立
func (svc *imShard) onChat(msg Envelope) {
u := svc.users[msg.FromSession]
u.unread++
svc.users[msg.FromSession] = u
}
若使用后台 worker 池并行执行且并发触及同一张共享表,则需锁、分片、或将结果回填邮箱再合并等设计,否则会出现数据竞争。
1.4.2. 分片热点
单分片承载过多实体,或个别热点实体流量过大,会使该分片成为瓶颈。缓解手段包括:再分片、垂直拆分服务、或将重计算异步化等。
1.4.3. 状态搬迁
扩缩容改变分片映射时,状态多在内存表中;数据面(脏标记、拉取、双写等)需业务与运维设计。本书 第四章 4.5 从路由与粘性角度讨论映射变化时的一种现象;框架一般不提供通用的「整表搬家」编排。
1.5. 1.3.5 混合粒度
实践中可同时存在:主体用服务级扛量,局部用更细粒度的 Actor(如按房间、按回合单独一节流)。只要消息抽象不过度绑定某一种粒度,后续调整空间更大。
1.6. 1.3.6 本节要点(模型层)
对象级:Actor 数量大、平台能力强、监督与路由深度集成。服务级:Actor 少、状态在进程内表中、入口加分片、监督常扁平。在 Go 上完整复刻对象级体系成本高,服务级更普遍;代价是并行与共享表、热点、搬迁需自行设计。是否混合粒度依业务而定。
1.7. 1.3.7 附录:与 zhenyi 的对应
| 正文用语 | zhenyi 中常见落点 |
|---|---|
| 统一入口 | zgate.Server |
| 分片与选址 | 本地 LocalRouter,远程 RemoteRouteStrategy(如 HRW);第四、五章 |
| 进程内用户表等 | 各业务 Actor 内 map / 结构体 |
| 后台任务 | AsyncRun、协程池;第三章(注意与主线程共享数据) |
| 粘性 / 扩缩容路由 | 4.5 |
| 监督 | Group.superviseActor、MaxRestarts 等;第三章 |
阅读正文时区分 「调整分片映射」 与 「搬迁表内多行数据」,比记忆符号更能指导实现。