Actor Addressing 设计文档¶
概述¶
本文档定义了 Pulsing Actor System 的地址设计方案。该方案提供统一的 URI 格式来标识和定位集群中的 Actor,支持具名 Actor 的多实例部署和自动负载均衡。
设计目标¶
- 统一的地址格式 - 使用 URI scheme 提供自描述、可扩展的地址表示
- 位置透明 - 具名 Actor 可在集群任意节点访问,无需知道具体位置
- 多实例支持 - 同一具名 Actor 可部署多个实例,支持负载均衡
- 高效的服务发现 - 只有具名 Actor 通过 Gossip 广播,减少网络开销
- 命名空间隔离 - 强制命名空间,提供逻辑分组和管理
地址格式¶
地址类型概览¶
graph TB
subgraph AddressScheme["Actor Address Scheme"]
subgraph Named["1. 具名 Actor 服务地址<br/>(位置透明,多实例)"]
N1["actor:///{namespace}/{path}/{name}"]
N1E["例: actor:///services/llm/router"]
end
subgraph Instance["2. 具名 Actor 实例地址<br/>(指定节点实例)"]
I1["actor:///{namespace}/{path}/{name}@{node_id}"]
I1E["例: actor:///services/llm/router@node_a"]
end
subgraph Global["3. 全局 Actor 地址<br/>(集群中任意 actor 的精确地址)"]
G1["actor://{node_id}/{actor_id}"]
G1E["例: actor://node_a/worker_123"]
end
subgraph Local["4. 本地快捷引用<br/>(当前节点上的 actor)"]
L1["actor://localhost/{actor_id}"]
L1E["例: actor://localhost/worker_123"]
end
end
style AddressScheme fill:#f5f5f5,stroke:#333,stroke-width:2px
style Named fill:#e3f2fd,stroke:#1976d2
style Instance fill:#fff3e0,stroke:#f57c00
style Global fill:#e8f5e9,stroke:#388e3c
style Local fill:#fce4ec,stroke:#c2185b
地址格式详解¶
1. 具名 Actor 服务地址¶
- 格式特征:三斜杠开头(
actor:///),表示省略 host 部分 - 命名空间:必须,路径的第一段
- 路径:可选的中间层级,用于逻辑分组
- 名称:路径的最后一段
示例:
actor:///services/llm/router # 命名空间: services, 名称: router
actor:///workers/inference/pool # 命名空间: workers, 名称: pool
actor:///system/cluster/monitor # 命名空间: system, 名称: monitor
2. 具名 Actor 实例地址¶
- 在服务地址后添加
@node_id后缀 - 直接路由到指定节点的实例
示例:
3. 全局 Actor 地址¶
- 格式特征:两斜杠后直接跟 node_id
- 集群中任意 Actor 的精确定位
- 不通过 Gossip 注册,需要预先知道地址
示例:
4. 本地快捷引用¶
- 使用保留字
localhost指代当前节点 - 运行时解析为实际的 node_id
- 适用于本地 Actor 间通信
示例:
地址分类对比¶
| 类型 | 格式 | Gossip 注册 | 多实例 | 使用场景 |
|---|---|---|---|---|
| 具名服务 | actor:///ns/name |
✅ | ✅ | 服务型 Actor,需要服务发现 |
| 具名实例 | actor:///ns/name@node |
✅ | - | 访问特定实例 |
| 全局地址 | actor://node/id |
❌ | ❌ | 临时/动态 Actor |
| 本地引用 | actor://localhost/id |
❌ | ❌ | 本地快捷访问 |
命名空间设计¶
强制命名空间¶
所有具名 Actor 必须属于一个命名空间。命名空间是路径的第一段,用于:
- 逻辑分组 - 按功能或模块组织 Actor
- 访问控制 - 未来可基于命名空间实现权限管理
- 监控隔离 - 按命名空间聚合监控指标
预留命名空间¶
| 命名空间 | 用途 | 示例 |
|---|---|---|
system |
系统内置 Actor | system/cluster/monitor |
services |
服务型 Actor | services/llm/router |
workers |
Worker 池 | workers/inference/pool |
user |
用户自定义 | user/myapp/handler |
路径层级¶
命名空间下可以有多级路径,用于更细粒度的组织:
actor:///services/llm/router # 2 级
actor:///services/llm/v2/router # 3 级
actor:///workers/inference/gpu/pool # 3 级
多实例支持¶
概念模型¶
┌──────────────────────────────────────────────────────────────────────────┐
│ │
│ 具名 Actor: actor:///services/api │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Instance │ │ Instance │ │ Instance │ │
│ │ @node_a │ │ @node_b │ │ @node_c │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ 访问 actor:///services/api 时自动负载均衡选择实例 │
│ 访问 actor:///services/api@node_b 时直接路由到 node_b │
│ │
└──────────────────────────────────────────────────────────────────────────┘
实例注册¶
同一路径的具名 Actor 可在多个节点创建实例:
// Node A
let path = ActorPath::new("services/llm/router")?;
system.spawn_named(path.clone(), LLMRouter::new()).await?;
// Node B (同一路径,不同实例)
system.spawn_named(path.clone(), LLMRouter::new()).await?;
// Node C
system.spawn_named(path.clone(), LLMRouter::new()).await?;
负载均衡¶
访问服务地址时,系统自动从可用实例中选择:
// 自动负载均衡
let addr = ActorAddress::parse("actor:///services/llm/router")?;
let resp = system.ask(&addr, request).await?; // 可能路由到 A、B 或 C
支持的负载均衡策略(可扩展): - Random(随机)- 默认 - RoundRobin(轮询) - LeastConnections(最少连接)
地址解析¶
解析流程¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ Address Resolution │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ actor:///services/api │
│ │ │
│ ├──→ 查询 Gossip Registry │
│ │ │ │
│ │ ↓ │
│ │ instances: [node_a, node_b, node_c] │
│ │ │ │
│ │ ↓ (负载均衡选择) │
│ │ selected: node_b │
│ │ │ │
│ └────────→ http://node_b_ip:port/named/services/api │
│ │
│ actor:///services/api@node_a │
│ │ │
│ ├──→ 查询 node_a 地址 │
│ │ │ │
│ └────────→ http://node_a_ip:port/named/services/api │
│ │
│ actor://node_a/worker_123 │
│ │ │
│ ├──→ 查询 node_a 地址 │
│ │ │ │
│ └────────→ http://node_a_ip:port/actors/worker_123 │
│ │
│ actor://localhost/worker_123 │
│ │ │
│ ├──→ 替换 localhost → current_node_id │
│ │ │ │
│ └────────→ 本地直接调用(不走网络) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
HTTP 映射¶
Actor 地址与 HTTP 端点的映射关系:
| Actor 地址 | HTTP 端点 |
|---|---|
actor:///services/llm/router |
POST http://{selected_node}/named/services/llm/router |
actor:///services/llm/router@node_a |
POST http://node_a/named/services/llm/router |
actor://node_a/worker_123 |
POST http://node_a/actors/worker_123 |
HTTP API 设计¶
路由规则¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ HTTP API Routes │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 具名 Actor 路由 │
│ ──────────────────────────────────────────────────────────────────────────│
│ POST /named/{namespace}/{path...} │
│ 发送消息给具名 Actor (ask/tell) │
│ Headers: │
│ X-Actor-Instance: node_id (可选,指定实例) │
│ X-Message-Type: string (消息类型标识) │
│ X-Response-Required: bool (是否需要响应,默认 true) │
│ Body: 序列化的消息内容 │
│ Response: 序列化的响应内容 │
│ │
│ GET /named/{namespace}/{path...} │
│ 查询具名 Actor 元信息 │
│ Response: { │
│ "path": "services/llm/router", │
│ "instances": ["node_a", "node_b"], │
│ "metadata": {...} │
│ } │
│ │
│ 普通 Actor 路由 │
│ ──────────────────────────────────────────────────────────────────────────│
│ POST /actors/{actor_id} │
│ 发送消息给普通 Actor │
│ Headers: 同上 │
│ │
│ GET /actors/{actor_id} │
│ 查询普通 Actor 元信息 │
│ │
│ 集群管理 │
│ ──────────────────────────────────────────────────────────────────────────│
│ POST /cluster/gossip │
│ Gossip 协议消息 │
│ │
│ GET /cluster/members │
│ 查看集群成员列表 │
│ │
│ GET /cluster/registry │
│ 查看具名 Actor 注册表 │
│ │
│ GET /cluster/registry/{namespace} │
│ 按命名空间过滤注册表 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
HTTP 语义¶
| 方法 | 语义 | 用途 |
|---|---|---|
POST |
发送消息 | ask/tell 消息传递 |
GET |
查询信息 | 获取 Actor 元信息、状态 |
Gossip 注册表¶
数据结构¶
/// 具名 Actor 注册信息
pub struct NamedActorInfo {
/// Actor 路径
pub path: ActorPath,
/// 所有实例所在的节点
pub instances: HashSet<NodeId>,
/// 版本号(用于冲突解决)
pub version: u64,
}
/// 全局注册表
pub struct GlobalRegistry {
/// 节点成员信息 (node_id -> MemberInfo)
pub members: HashMap<NodeId, MemberInfo>,
/// 具名 Actor 注册表 (path -> NamedActorInfo)
/// 只有具名 Actor 在此注册
pub named_actors: HashMap<String, NamedActorInfo>,
}
Gossip 消息¶
pub enum GossipMessage {
// ... 现有消息 ...
/// 具名 Actor 实例注册
NamedActorRegistered {
path: ActorPath,
node_id: NodeId,
},
/// 具名 Actor 实例注销
NamedActorUnregistered {
path: ActorPath,
node_id: NodeId,
},
/// 同步消息
Sync {
from: NodeId,
members: Vec<MemberInfo>,
named_actors: Vec<NamedActorInfo>,
},
}
注册流程¶
- 创建具名 Actor:调用
spawn_named(path, actor) - 本地注册:Actor 创建成功后注册到本地
- Gossip 广播:发送
NamedActorRegistered消息 - 集群同步:其他节点更新注册表
注销流程¶
- 停止 Actor:Actor 优雅关闭
- 本地注销:从本地注册表移除
- Gossip 广播:发送
NamedActorUnregistered消息 - 节点故障:SWIM 检测到节点故障时,自动清理该节点的所有实例
数据结构定义¶
ActorPath¶
/// Actor 路径(命名空间 + 层级路径 + 名称)
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct ActorPath {
/// 路径段,如 ["services", "llm", "router"]
segments: Vec<String>,
}
impl ActorPath {
/// 预留的系统命名空间
pub const SYSTEM_NAMESPACES: &[&str] = &["system"];
/// 创建新路径(至少需要 namespace/name 两段)
pub fn new(path: impl AsRef<str>) -> Result<Self, ParseError>;
/// 获取命名空间(第一段)
pub fn namespace(&self) -> &str;
/// 获取名称(最后一段)
pub fn name(&self) -> &str;
/// 获取完整路径字符串
pub fn as_str(&self) -> String;
/// 检查是否为系统命名空间
pub fn is_system(&self) -> bool;
}
ActorAddress¶
/// 保留的特殊节点标识
pub const LOCALHOST: &str = "localhost";
/// Actor 地址
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum ActorAddress {
/// 具名 Actor - actor:///namespace/path/name[@node]
Named {
path: ActorPath,
instance: Option<NodeId>,
},
/// 全局地址 - actor://node_id/actor_id
Global {
node_id: NodeId,
actor_id: String,
},
}
impl ActorAddress {
/// 解析 URI 格式地址
pub fn parse(uri: &str) -> Result<Self, ParseError>;
/// 转换为 URI 字符串
pub fn to_uri(&self) -> String;
/// 解析 localhost 为实际 node_id
pub fn resolve_localhost(self, current_node: &NodeId) -> Self;
/// 检查是否为本地引用
pub fn is_localhost(&self) -> bool;
/// 为具名地址添加实例定位
pub fn with_instance(self, node_id: NodeId) -> Self;
}
使用示例¶
创建和访问具名 Actor¶
// === 创建具名 Actor ===
// 在 Node A 上创建
let path = ActorPath::new("services/llm/router")?;
let router_a = system.spawn_named(path.clone(), LLMRouter::new()).await?;
// 在 Node B 上创建同名实例
let router_b = system.spawn_named(path.clone(), LLMRouter::new()).await?;
// === 访问具名 Actor ===
// 服务地址访问(自动负载均衡)
let addr = ActorAddress::parse("actor:///services/llm/router")?;
let response: Response = system.ask(&addr, request).await?;
// 指定实例访问
let addr = ActorAddress::parse("actor:///services/llm/router@node_a")?;
let response: Response = system.ask(&addr, request).await?;
// 查询所有实例
let info = system.lookup_named(&path).await?;
println!("Instances: {:?}", info.instances); // ["node_a", "node_b"]
创建和访问普通 Actor¶
// === 创建普通 Actor ===
// 创建(不注册到 Gossip)
let worker = system.spawn(TempWorker::new()).await?;
let worker_addr = worker.address(); // actor://node_a/worker_xyz123
// === 访问普通 Actor ===
// 通过完整地址访问
let addr = ActorAddress::parse("actor://node_a/worker_xyz123")?;
let result = system.ask(&addr, task).await?;
// 本地快捷访问
let local_addr = ActorAddress::parse("actor://localhost/worker_xyz123")?;
let result = system.ask(&local_addr, task).await?;
// === 地址传递 ===
// Worker 将自己的地址告诉 Manager
manager.tell(RegisterWorker {
addr: worker.address() // actor://node_a/worker_xyz123
}).await?;
// Manager 可以用这个地址访问 Worker
跨节点通信¶
// Node A 上创建 Worker
let worker = system.spawn(Worker::new()).await?;
let worker_addr = worker.address(); // actor://node_a/worker_123
// 将地址发送给 Node B 上的 Manager
let manager_addr = ActorAddress::parse("actor:///services/task/manager")?;
system.tell(&manager_addr, RegisterWorker { addr: worker_addr }).await?;
// Node B 上的 Manager 收到后,可以直接访问 Node A 的 Worker
impl Handler<Task> for Manager {
async fn handle(&mut self, task: Task, ctx: &mut ActorContext) {
// worker_addr 是 "actor://node_a/worker_123"
let worker_ref = ctx.actor_ref(&self.worker_addr).await?;
worker_ref.tell(task).await?;
}
}
总结¶
| 特性 | 设计决策 |
|---|---|
| 地址格式 | URI scheme:actor:// |
| 具名 vs 普通 | 具名通过 Gossip 注册,普通不注册 |
| 多实例 | 同一路径可在多节点部署,自动负载均衡 |
| 实例定位 | @node_id 后缀指定实例 |
| 本地引用 | localhost 保留字 |
| 命名空间 | 强制要求,提供逻辑隔离 |
| HTTP 语义 | POST=消息,GET=查询 |
附录:地址格式 BNF¶
actor-address = named-address | global-address
named-address = "actor:///" path [ "@" node-id ]
global-address = "actor://" node-id "/" actor-id
path = namespace "/" name
| namespace "/" sub-path "/" name
sub-path = segment
| segment "/" sub-path
namespace = segment
name = segment
segment = 1*( ALPHA | DIGIT | "_" | "-" )
node-id = "localhost" | identifier
actor-id = identifier
identifier = 1*( ALPHA | DIGIT | "_" | "-" )