1. 5.2 RendezvousHash 路由与粘性策略

5.1 提供远程 ActorConfig 候选;Gate 在 routeToRemoteActor 中用 zroute.RemoteRouteStrategy.PickOne首选下标,失败再 fallback。本节说明 HRW (hrwScore)DefaultRemoteRouteKey,实现见 zhenyi/zroute/remote_strategy.go

1.1. 5.2.1 路由选型:轮询 vs 哈希 vs 一致性哈希 vs HRW

先看几种常见路由策略的区别:

策略 粘性 扩缩容影响 实现复杂度
轮询(RoundRobin) 无(全打散)
随机(Random) 无(全打散) 极低
取模哈希(N % nodes) 大(全部重映射)
一致性哈希(Ketama) 小(约 1/N)
RendezvousHash(HRW) 与环哈希同量级、实现更简单

实时应用需要粘性——同一个用户的请求总是到同一个节点。所以轮询和随机被排除。

取模哈希的问题是扩缩容时全部重映射:3 个节点变 4 个,几乎所有用户的映射都变了。对有状态服务来说,这意味着大量用户的状态需要迁移。

一致性哈希解决了这个问题,但需要虚拟节点来平衡负载。

本框架默认在需要粘性候选规模有限(常几十个)时采用 HRW:线性扫描即可,不维护环与虚拟节点;是否与 Ketama「谁更优」取决于节点规模、均衡需求与运维习惯,此处只交代 zhenyi 的实现选择

1.2. 5.2.2 RendezvousHash 是什么?

RendezvousHash 的核心思想:把 key 和每个节点做一次哈希,选分数最高的节点。

用户 A 的请求:
  score(A, 节点1) = 0.72
  score(A, 节点2) = 0.85  ← 最高,选这个
  score(A, 节点3) = 0.31

用户 B 的请求:
  score(B, 节点1) = 0.91  ← 最高
  score(B, 节点2) = 0.44
  score(B, 节点3) = 0.67

同一个用户总是选到同一个节点(哈希函数确定性)。新增或删除节点时,只有分数比较受影响的用户会改变映射,大部分用户不受影响。

1.3. 5.2.3 与典型一致性哈希的对比(工程取舍)

1.3.1. 扩缩容对比

假设 3 个 IM Actor,加一个:

一致性哈希(Ketama):

原来:用户分布在 3 个节点的哈希环上
加了节点 4:
  - 节点 4 从相邻节点"抢走"一段哈希范围
  - 约 1/4 用户迁移到节点 4
  - 需要 100~200 个虚拟节点才能均匀分布

RendezvousHash:

原来:每个用户对所有节点算分数,选最高
加了节点 4:
  - 每个用户重新算一遍,节点 4 可能胜出也可能不
  - 约 1/4 用户迁移到节点 4
  - 不需要虚拟节点,天然均匀

两者在「约 1/N 迁移」量级上常接近;HRW 不引入虚拟节点,以 O(候选数) 扫描换数据结构简单。

1.3.2. 负载均衡

一致性哈希需要虚拟节点来保证均匀分布。如果虚拟节点不够,可能出现某些物理节点承担更多流量。

HRW 通过 对所有候选打分取最大 分散 key;是否比「少虚节点」的环哈希更匀,应靠压测与监控。

1.3.3. 实现复杂度

一致性哈希(Ketama):

  • 维护一个排序的哈希环(通常用红黑树或跳表)
  • 查找时在环上二分查找
  • 虚拟节点管理(每个物理节点 100~200 个虚拟节点)

RendezvousHash:

  • 线性扫描所有候选,选最高分
  • 不需要维护额外数据结构
  • 不需要虚拟节点

对于 zhenyi 的场景(通常几十个 Actor),线性扫描的开销可以忽略。如果有几百个节点,可以做分区优化,但目前的规模不需要。

1.4. 5.2.4 zhenyi 的实现

1.4.1. hash 函数

func hrwScore(key uint64, actorID uint64, process uint64) uint64 {
    const (
        c1 = 0x9e3779b185ebca87  // 黄金比例相关的乘子
        c2 = 0xc2b2ae3d27d4eb4f
    )
    x := key ^ (actorID + 0x100000001b3*process)
    x ^= x >> 33
    x *= c1
    x ^= x >> 29
    x *= c2
    x ^= x >> 32
    return x
}

实现为 64 位整型混合(乘子与位移见源码注释),输出 uint64 分数 只做大小比较,避免浮点。

三个输入参数:

  • key:路由键(DefaultRemoteRouteKey:优先 SessionId,否则 RpcId
  • actorID:Actor 的唯一 ID
  • process:进程 ID

为什么同时用 actorID 和 process?防止不同进程的 Actor ID 碰撞导致 hash 冲突。

1.4.2. 默认 key 的选择

func DefaultRemoteRouteKey(msg *zmsg.Message) uint64 {
    if msg.SessionId != 0 {
        return msg.SessionId   // 优先用 SessionId(会话粘性)
    }
    if msg.RpcId != 0 {
        return msg.RpcId       // 其次用 RpcId(请求粘性)
    }
    return 0                    // 无 key,退化为第一个候选
}

大部分消息都有 SessionId(客户端消息),所以默认就是会话粘性。

如果没有 SessionId 但有 RpcId(Actor 间的 RPC 调用),用 RpcId 做 key。

如果都没有,退化为"第一个候选优先",由 fallback 机制兜底。

1.4.3. 选址逻辑(PickOne)

func (s *RendezvousHashStrategy) PickOne(msg *zmsg.Message, candidates []zmodel.ActorConfig) int {
    key := s.KeyFunc(msg)  // 默认 DefaultRemoteRouteKey
    if key == 0 {
        return 0   // 无 key,退化为第一个候选
    }

    bestIdx := -1
    var bestScore uint64
    for i, c := range candidates {
        score := hrwScore(key, uint64(c.Id), uint64(c.Process))
        if bestIdx < 0 || score > bestScore { // 平局保留先出现的下标
            bestIdx = i
            bestScore = score
        }
    }
    return bestIdx
}

策略只返回一个首选下标;Gate 先尝试首选,失败再 fallback(见 4.3)。

1.5. 5.2.5 key 的选择对粘性的影响

key 选择不同,粘性的粒度不同:

key 粘性粒度 适用场景
SessionId 用户级 IM、游戏(同一用户状态在同一节点)
MsgId 消息类型级 无状态处理(同类消息到同一节点)
自定义 shardKey 业务级 多租户(同租户数据在同一节点)

zhenyi 默认优先用 SessionId,无则 RpcId,与大多数有状态实时场景一致;若仍无 key,则退化为“第一个候选”。

如果需要自定义,可以创建自己的 RendezvousHashStrategy

strategy := &RendezvousHashStrategy{
    KeyFunc: func(msg *zmsg.Message) uint64 {
        return msg.SessionId ^ uint64(msg.MsgId) // 自定义混合 key
    },
}
gate.SetRemoteRouteStrategy(strategy)

1.6. 5.2.6 无 key 时的降级

当消息没有 SessionId 也没有 RpcId 时,DefaultRemoteRouteKey 返回 0;RendezvousHashStrategy.PickOnekey == 0直接返回索引 0(首选候选),与“无粘性时固定打第一个候选”的语义一致,而不是对候选做全量重排。

1.7. 5.2.7 本节要点

  1. HRWhrwScore(key, Id, Process)Process 参与混合以降低跨进程 Id 碰撞风险。
  2. 默认 keySessionIdRpcId00PickOne 返回 0,即首选候选)。
  3. 自定义RendezvousHashStrategy{KeyFunc: ...} + gate.SetRemoteRouteStrategy
  4. 复杂度:对候选 线性扫描;候选数很大时需另做分域或分层。
  5. 与快照:远程候选列表来自 Group 跨进程路由快照5.4),与本节策略正交。

5.3 节TopicBus / znats 工程细节。

results matching ""

    No results matching ""