1. 4.3 路由策略:本地与远程

4.2 的映射之上,本节细化 gateHandler 三条分支与 跨进程候选Group 路由表视图、zroute.RemoteRouteStrategy,以及 sendNoRouteError/OnNoRoute。锚点 zhenyi/zgate/gate.gozhenyi/zroute/remote_strategy.gozhenyi/zactor/group.go

1.1. 4.3.1 路由的三条路径

Gate 收到客户端消息后,按优先级尝试三条路径:

Gate 收到消息
    ↓
路径 1:Gate 自己能处理吗?
    ↓ 不能
路径 2:本进程有其他 Actor 能处理吗?
    ↓ 没有
路径 3:其他进程有 Actor 能处理吗?
    ↓ 没有
回复错误:无路由

4.1 节已经讲了这个优先级。本节深入每条路径的实现。

1.2. 4.3.2 路径 1:Gate 自身处理

有些消息不需要到达业务 Actor,Gate 自己处理:

func (s *Server) gateHandler(ctx context.Context, channel IChannel, msg *zmsg.Message) {
    // 检查 Gate 自己有没有注册这个 msgId 的 handler
    if _, ok := s.GetMsgList()[msg.MsgId]; ok {
        s.GetHandleMgr().HandleClientMessage(ctx, msg)
        return  // 处理完直接返回
    }
    // 继续尝试其他路径...
}

典型场景:心跳包、认证请求等 Gate 层面的协议。

1.3. 4.3.3 路径 2:本地路由

消息不在 Gate 的处理范围内,尝试转发给同进程的其他 Actor。

1.3.1. 路由表

Group 维护一个路由表:msgId → []IActor。每个 Actor 注册时,声明自己能处理哪些 msgId:

// Actor 注册时声明支持的 msgId
group.RegisterRoutes(actor, []int32{MsgChat, MsgJoinRoom, MsgLeaveRoom})

路由表用 atomic.Value 存储,Copy-On-Write 模式:

  • 读路径:无锁原子读(atomic.Value.Load());默认接口返回副本,热路径可选走只读视图(零分配)
  • 写路径:复制 map,只对本次修改的 key 生成新 slice,然后原子替换
func (g *Group) LookupActorsByMsgID(msgID int32) []ziface.IActor {
    table := g.msgRouteTable.Load()  // 无锁读
    list := table[msgID]
    out := make([]ziface.IActor, len(list))
    copy(out, list)  // 返回副本,调用方安全修改
    return out
}

为什么用 Copy-On-Write 而不是读写锁?因为路由注册是低频操作(进程启动时注册一次),而路由查询是高频操作(每条消息都要查)。Copy-On-Write 让读路径零开销。

1.3.2. DefaultLocalRouter

默认的本地路由实现优先走只读视图快路径(若 Group 实现了 IGroupRouteTableView),否则回退到副本接口:

func (r *DefaultLocalRouter) RouteLocal(group IGroup, msg *zmsg.Message) (IActor, error) {
    if fast, ok := any(group).(ziface.IGroupRouteTableView); ok {
        actors := fast.LookupActorsByMsgIDView(msg.MsgId) // 只读视图,零分配
        if len(actors) == 0 { return nil, ErrNotFound }
        return actors[0], nil
    }
    actors := group.LookupActorsByMsgID(msg.MsgId) // 兼容副本语义
    if len(actors) == 0 {
        return nil, ErrNotFound
    }
    return actors[0], nil  // 多个候选时返回第一个
}

如果只有一个 Actor 注册了某个 msgId,直接返回它。如果有多个(比如启动了多个 IM Actor 实例做分片),当前默认返回第一个,后续可以扩展为基于消息内容的分片策略。

1.4. 4.3.4 路径 3:远程路由

本进程没有能处理的 Actor,尝试转发给其他进程

1.4.1. 构建候选列表

Gate 通过 Group.otherActors 获取其他进程的 Actor 配置。这个数据来自服务发现(Etcd),由 watchActor goroutine 维护:

func (g *Group) watchActor(ctx context.Context) {
    // 启动时加载存量
    items := g.discoverer.FindAllByPrefix("/servers")
    for _, item := range items {
        g.otherActors.Store(item.ActorConfig.Id, item.ActorConfig)
    }
    // 监听变化
    ch := g.discoverer.Watch()
    for cfg := range ch {
        if cfg.ActorType <= 0 {
            g.otherActors.Delete(cfg.Id)  // 下线
        } else {
            g.otherActors.Store(cfg.Id, cfg)  // 上线或更新
        }
    }
}

默认先尝试使用跨进程快表只读视图(IGroupRemoteRouteTableView),未实现时再回退线性扫描:

if fast, ok := any(group).(ziface.IGroupRemoteRouteTableView); ok {
    candidates = fast.LookupOtherActorConfigsByMsgIDView(msg.MsgId) // 只读视图,零分配
} else {
    // 回退:线性扫描
    for _, cfg := range configs {
        for _, id := range cfg.SupportedMsgIDs {
            if id == msg.MsgId {
                candidates = append(candidates, cfg)
                break
            }
        }
    }
}

1.4.2. 路由策略(PickOne)

选出一组候选后,由路由策略选出“首选候选索引”(PickOne)。Gate 先尝试首选,失败后再按原顺序 fallback,不再构造 ordered 新切片。zhenyi 提供了三种策略:

FirstCandidate(默认)

// 直接选择第一个候选(签名见 zroute)
func (FirstCandidateStrategy) PickOne(_ *zmsg.Message, candidates []zmodel.ActorConfig) int {
    if len(candidates) == 0 { return -1 }
    return 0
}

最简单的策略,适合单进程或固定部署。

RoundRobin(轮询)

// 按顺序轮询,无粘性(示意:Add(1) 起算,与首包落点以源码为准)
func (s *RoundRobinStrategy) PickOne(_ *zmsg.Message, candidates []zmodel.ActorConfig) int {
    if len(candidates) == 0 { return -1 }
    return int(s.seq.Add(1) % uint64(len(candidates)))
}

适合无状态的消息处理。但同一条消息可能被路由到不同的 Actor,不适合有状态的场景。

RendezvousHash(HRW 一致性哈希)

func (s *RendezvousHashStrategy) PickOne(msg *zmsg.Message, candidates []zmodel.ActorConfig) int {
    key := DefaultRemoteRouteKey(msg)  // 默认用 SessionId
    if key == 0 { return 0 }
    // 对每个候选计算 HRW 分数,选择分数最高的下标
    for i, c := range candidates {
        score := hrwScore(key, c.Id, c.Process)
        if score > bestScore {
            bestIdx = i
        }
    }
    return bestIdx
}

默认 key 由 zroute.DefaultRemoteRouteKey 选择:优先 SessionId,否则 RpcId,均为 0 时退化为“第一个候选”(见 zhenyi/zroute/remote_strategy.go)。在常见客户端协议里 SessionId 非 0,因此表现为会话粘性。

1.4.3. 三种策略的对比

策略 粘性 扩缩容影响 适用场景
FirstCandidate 重新部署会变 单进程、固定部署
RoundRobin 无影响 无状态处理
RendezvousHash 有(按 key) 最小影响 有状态、需要粘性

为什么 RendezvousHash 扩缩容影响最小?

普通一致性哈希在节点增减时,大约 1/N 的 key 会重新映射。RendezvousHash(HRW)的效果类似,但实现更简单,不需要虚拟节点。对实时应用来说,扩缩容时只有少量用户的请求需要迁移,大部分用户不受影响。

1.4.4. 跨进程转发

选定目标后,通过 NATS 消息总线转发:

msg.TarActor = target.Id
topic := target.GetTopic()
// 发送侧可复用编码缓冲,示意为直接广播已编码字节
encoded, _ := msg.Marshal()
zbus.DefaultBus.Broadcast(topic, encoded)

目标进程的 Actor 订阅了这个 topic,收到消息后进入自己的 Mailbox 处理。

1.4.5. Fallback

如果首选候选失败了(比如 NATS 发送失败),会尝试下一个候选:

for i := 0; i < len(candidates); i++ {
    var target ActorConfig
    if i == 0 {
        target = candidates[pickIdx]
    } else {
        idx := i - 1
        if idx >= pickIdx {
            idx++
        }
        target = candidates[idx]
    }
    if i > 0 {
        zmetrics.GateRouteRemoteFallback.Inc()  // 记录 fallback 次数
    }
    msg.TarActor = target.Id
    topic := target.GetTopic()
    encoded, _ := msg.Marshal() // 示例:每轮按当前 TarActor 重新编码
    err := zbus.DefaultBus.Broadcast(topic, encoded)
    if err != nil {
        continue  // 失败,尝试下一个
    }
    return true
}

1.5. 4.3.5 无路由处理

三条路径都失败时,gateHandler 调用 sendNoRouteErrorgate.go):内部先尝试 OnNoRoute 注册的 noRouteHandler,若 handled 则按需 sendClient;否则 仅打 Warn 日志,客户端未必收到应用层错误包——需业务在协议层约定超时或注册钩子。

1.6. 4.3.6 本节要点

  1. :自身 handler → RouteLocalCmdTypeClient + Retain)→ routeToRemoteActorsendNoRouteError
  2. 本地表LookupActorsByMsgID / LookupActorsByMsgIDView(无分配快路径视 Group 能力而定)。
  3. 远程候选otherActors + SupportedMsgIDs,快表或线性扫描;PickOne 仅选首选下标,失败则 fallback。
  4. 策略FirstCandidate / RoundRobin / RendezvousHashDefaultRemoteRouteKey:SessionId 优先)。
  5. 无路由:默认日志;Server.OnNoRoute 可自定义回包。

4.4 节PushSetReplyIToClientFastPath 的顺序与约束。

results matching ""

    No results matching ""