笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 接入与通信

    • 游戏基础架构与工具
    • 接入网关(长连接接入层)
    • 消息总线(共享内存 IPC)
    • 帧同步(Lockstep)
    • 全栈极限低延迟(物理层→应用层)
    • KCP 与 QUIC
  • 网络与服务网格

    • CNI 与 K8s 网络插件
    • 容器运行时:Docker 与 containerd
    • Istio 与 Cilium 服务网格
    • 服务网格:中心化 vs 去中心化
    • eBPF 原理与在网络/可观测/安全的落地
    • 自研 Mesh 服务网格 × K8s 部署
    • 自研 Mesh × K8s · 数据面深水区
    • Tco 协程框架 · 运行时深水区
    • K8s 异构 & 网络插件
  • 有状态服务

    • 分布式游戏存储(分布式 KV)
    • 有状态服务的数据迁移
    • 有状态服务的数据恢复与容灾
    • 有状态服务的 K8s 编排层治理(防漂移 + 节点崩溃恢复)
    • 内存配置热刷新与更新机制
  • 算法与协程

    • 一致性哈希四算法实现(Ring Hash / Ketama / Maglev / Jump Hash)
    • 蓄水池抽样(Reservoir Sampling)与加权扩展
    • C++ 协程:有栈 vs 无栈 vs C++20 原生(函数染色与 libco)
    • Raft 与 Gossip:CP 强一致共识 vs AP 最终一致传播
  • 流量与承载

    • 游戏秒杀场景承载
    • 令牌桶与漏桶
    • 限流与熔断
    • 限流:互联网 vs 游戏
    • 灰度发布 · Canary / BlueGreen / A-B / Shadow
    • 服务器日常系统开发与维护 SOP
    • 极限发压工具设计(多长连接 + 发压任务)
    • 性能分析与优化方法论(六条排查线)
  • 编译与排查

    • 编译优化与 LLVM/Clang/GCC
    • Sanitizer 工具链与内存泄漏定位

有状态服务的数据迁移

迁移是计划内变更:你主动把某台机器/某个分片上的玩家、房间、内存态搬到另一处(扩缩容、机器下线、机房搬迁、一致性哈希重分片)。难点不在"搬数据",而在"搬的过程中在线业务不能停、不能丢、不能双份"。与 stateful-recovery(面向故障的恢复)是两码事——本篇最后专门讲区别。

一句话结论

有状态在线迁移=快照+增量追平+短冻结+幂等回放+校验+原子切流留回滚点,零丢零重。

场景问题

无状态服务扩容很爽:起新 Pod、注册进 LB、流量自然打过来,旧 Pod 摘掉即可,因为每个请求自包含,状态在 DB。

有状态服务(战斗服、房间服、场景服)不行:

  • 内存态:玩家的战斗上下文、AOI 视野、房间内所有人的实时状态都在进程内存里,不落 DB 或落得很粗。
  • 连接态:玩家和这台服有长连接(经 接入网关/消息总线 路由),迁走后连接指向哪?
  • 不可重算:一局 MOBA 打到一半的血量/技能 CD/位置,无法从"初始状态 + 日志"廉价重放出来(即使能,也贵)。

于是一致性哈希扩容时的 slot 搬迁、机器计划下线时的房间腾挪,都要求:在线迁移(migration in flight),玩家几乎无感,数据零丢失零重复。

实现方案

迁移方式谱系(按停机时间递减)

方式停机适用
停机迁移长(冻结全服/分区)大版本、允许维护窗口
双写灰度无,但双份写压力存储层换引擎/分库
快照 + 增量追平 + 切换极短(仅切换瞬间冻结)房间/玩家在线迁移,主流
一致性哈希 slot 搬迁按 slot 短暂冻结扩缩容重分片

主流方案:快照 + 增量 + 追平 + 切流 + 校验

关键点:先冻结写、快照 + 增量、目标端回放、切流量、校验、留回滚点。冻结只发生在最后追平的一瞬(增量已很小),对玩家近似无感。

打个比方:在线迁移就像居民搬家 + 双钥匙交接。旧家先冻结门锁(freeze 写),不让新客人进;把家当打包发去新家(快照 + 增量);新家开门收拾好后,换发新地址给邮政和朋友(切路由)。中间"锁旧门 + 通知邮政"的空档窗口,是不可避免的"暂不接客"。类比失效边界:搬家可以让房子空半天,玩家却不接受"这几秒暂时不能打技能"。所以工程上要把冻结窗口压到毫秒级(靠增量先追到几乎为零再冻),并且让新连接优先落到新节点(客户端重连即命中新家),而不是死等旧节点把最后一批数据推完再放行——用"提前重路由 + 幂等回放"替代"等搬完再开门",才是玩家无感的真正工程含义。

快照 + 增量回放伪码

// ==== 源节点 ====
func (s *Source) StartMigration(target Node) {
    s.mode = MIGRATING
    s.deltaLog = NewDeltaQueue()      // 从此刻起, 所有写既作用于内存也追加进 deltaLog

    snap := s.Snapshot()               // 一致性快照: 记录 version=V0
    stream := target.OpenStream()
    stream.SendSnapshot(snap)          // 传全量

    // 持续推增量, 直到积压足够小
    for {
        batch := s.deltaLog.Drain()    // 取出自上次以来的增量
        stream.SendDelta(batch)
        if s.deltaLog.Backlog() < THRESHOLD {
            break
        }
    }

    // —— 极短冻结窗口 ——
    s.Freeze()                         // 拒绝新写(客户端会重试/被路由挡住)
    tail := s.deltaLog.Drain()
    stream.SendDelta(tail)             // 推最后残余增量
    if !target.Verify(s.StateDigest()) {   // 状态摘要比对
        s.Unfreeze(); return ROLLBACK      // 校验失败, 回滚到源
    }
    controlPlane.SwitchRoute(s.Shard, target)  // 原子切路由
    s.mode = DRAINED                   // 源转为回滚点, 灰度期后回收
}

// ==== 目标节点: 幂等回放 ====
func (t *Target) OnDelta(batch []Op) {
    for _, op := range batch {
        // 每个 op 带全局递增 seq; 已回放过的直接跳过 => 幂等, 可安全重传
        if op.Seq <= t.lastApplied {
            continue
        }
        t.Apply(op)                    // Apply 本身也要幂等(如"设为X"而非"加X"更安全)
        t.lastApplied = op.Seq
    }
}

提示

增量回放必须幂等:网络重传、协调器重试都可能让同一批 op 送达两次。做法:op 带全局单调 seq + 目标端记 lastApplied 去重;能设计成"幂等语义操作"(set 而非 incr)则更稳。

一致性哈希扩容下的 slot 搬迁

扩容加节点会导致约 1/N 的 key 需要重映射(见 consistent-hash-impl)。搬迁按 slot 粒度做:锁定待迁 slot → 该 slot 内的房间/玩家走上面的快照+增量流程 → 切路由表指向新节点 → 解锁。逐 slot 滚动,任一时刻只有一小撮 slot 短暂冻结,不影响其余流量。

为什么这么做

  • 为什么快照 + 增量而不是停机全搬:停机搬迁要冻结整段业务直到搬完,玩家掉线、体验崩。快照 + 增量把"长冻结"拆成"边跑边传的长过程 + 一瞬间的短冻结",在线无感。
  • 为什么先冻结写再切:切路由和最后追平之间若还有新写落在源上,就会丢。冻结保证"切换那一刻源与目标状态严格一致",这是零丢失的必要条件。
  • 为什么留回滚点:切流后新节点可能立刻暴露 bug(数据格式、性能)。保留源节点作为灰度期回滚点,发现问题能秒切回,是有状态迁移的安全网。
  • 为什么按 slot/分区滚动:把爆炸半径限制在一个 slot,失败只影响局部,且可暂停/续传。

为什么别的选择不行

  • 简单重建(在目标端从零拉起 + 客户端重连):直接丢掉内存态和会话——玩家半局战斗归零、房间解散。有状态服务的状态不可重算,重建 = 数据丢失,不可接受。
  • 纯双写灰度:要求所有写路径改造成双写,且两端引擎/结构兼容;对"内存态房间"这种非存储型服务几乎无法双写(写的是内存对象,不是可复制的 SQL)。双写更适合存储层换引擎,不适合内存态在线迁移。
  • 只传快照不传增量:快照期间源还在处理写,快照一到目标端就已经过时(stale),切过去必然丢掉快照后的变更。增量追平不可省。
  • 不冻结直接原子切:切换瞬间的 in-flight 写会落在源、丢在目标,产生不一致。短冻结是代价最小的一致性保证。

注意

迁移中要明确定义回滚点(rollback point)与一致性边界:哪个 version 之前的状态在两端一致、切换失败退回哪个 version。没有清晰回滚点的迁移,一旦中途失败就进退两难。

沉淀结论

  • 迁移是计划内主动搬迁,难在"在线不丢不重",不在搬数据本身。
  • 记住五步:冻结写 → 快照 + 增量 → 目标端幂等回放 → 校验一致 → 原子切流(留回滚点)。
  • 幂等(seq + lastApplied)和短冻结窗口是零丢失的两根支柱。
  • 有状态的核心约束:内存态 + 连接态不可重算,所以不能像无状态那样直接漂移或重建。
  • 一致性哈希扩容用 逐 slot 滚动搬迁,把爆炸半径关进单个 slot。

与 stateful-recovery 的区别(重要)

迁移面向计划内变更:源健在、你主动把状态从 A 搬到 B,可以慢慢追平、留回滚点、失败退回,追求"玩家无感"。
恢复面向故障:源已宕/失联,没有"源实时增量流"可用,只能靠已落盘的 checkpoint + WAL 回放/副本接管把状态重建到最近可用点,追求的是 RPO/RTO(能救回多少、多快救回)。
一句话:迁移有一个活着的源做增量追平;恢复没有活源,只有落盘的历史。

记忆口诀

五步流程:冻结写 / 快照+增量 / 幂等回放 / 校验一致 / 原子切流留回滚点
两根支柱:幂等(seq+lastApplied) / 短冻结窗口
核心约束:内存态不可重算 / 连接态需重路由 / 逐 slot 滚动关爆炸半径
迁移 vs 恢复:迁移有活源追平 / 恢复只有落盘历史

内容来源

综合整理。参考方向:分布式系统教材(《Designing Data-Intensive Applications》一致性/复制章节)、一致性哈希与在线重分片实践(Redis Cluster resharding、Vitess reshard)、有状态服务在线迁移工程经验,以及游戏后台房间/场景服迁移的通用做法。

扩展阅读

  • k8s-stateful-ops —— K8s 编排层视角下的有状态服务治理(防漂移 + 节点崩溃恢复)。本篇讲业务态的计划内搬迁;编排层的 Pod / Node / PVC 调度约束、descheduler 定期回收、NonGracefulShutdown 节点故障恢复由那篇负责。二者共同构成"业务层 × 编排层"两轴模型。

自测:合上资料能说清楚吗?

为什么有状态服务(战斗服/房间服)不能像无状态服务那样起新 Pod、摘旧 Pod 就完成扩缩容?

参考答案

无状态请求自包含、状态在 DB,漂移无损。有状态服务的内存态(战斗上下文、AOI、房间实时状态)不落 DB 或落得很粗,连接态是长连接需重路由,且半局状态不可重算。直接重建=数据丢失,必须走在线迁移。

主流方案"快照 + 增量 + 追平 + 切流"中,为什么先传快照还不够、必须再传增量?为什么切流前要冻结写?

参考答案

快照期间源仍在处理写,快照到达目标端时已过时(stale),不追平会丢掉快照后的变更,故增量不可省。切流与最后追平之间若有新写落源,就会丢;冻结写保证切换瞬间源与目标严格一致,是零丢失的必要条件。冻结只在增量已很小时的一瞬,玩家近似无感。

增量回放为什么必须幂等?工程上怎么做到?

参考答案

网络重传、协调器重试都可能让同一批 op 送达两次。做法:每个 op 带全局单调 seq,目标端记 lastApplied 去重,seq ≤ lastApplied 直接跳过;并尽量把操作设计成幂等语义(set 而非 incr),使重复应用无副作用。

对比"在线迁移"和"故障恢复(stateful-recovery)",它们面对的前提和追求的目标有何不同?

参考答案

迁移面向计划内变更:源健在,可慢慢增量追平、留回滚点、失败退回,追求玩家无感。恢复面向故障:源已宕/失联,没有实时增量流,只能靠落盘的 checkpoint + WAL 回放/副本接管重建到最近可用点,追求 RPO/RTO。一句话:迁移有活源,恢复只有落盘历史。

一致性哈希扩容时如何搬迁 slot,才能不影响其余流量?为什么这样做?

参考答案

逐 slot 滚动:锁定待迁 slot → 该 slot 内房间/玩家走快照+增量流程 → 切路由表 → 解锁,任一时刻只有一小撮 slot 短暂冻结。好处是把爆炸半径关进单个 slot,失败只影响局部,且可暂停/续传。

最近更新: 2026/9/10 11:38
Prev
分布式游戏存储(分布式 KV)
Next
有状态服务的数据恢复与容灾