首页 / 资讯中心 / 文章详情

WuKongIM 动态节点接入 Stage 3:用有界 staged slot_replica_move 任务安全迁移 Slot 副本

WuKongIM 动态节点接入 Stage 3:用有界 staged slot_replica_move 任务安全迁移 Slot 副本 ★ FEATURED ARTICLE
即时通讯后端【免费下载链接】WuKongIMMore than just IM 不只是即时通讯(IM)项目地址https://gitcode.com/gh_mirrors/wu/WuKongIM点击查看免费下载本文围绕 WuKongIM 动态节点生命周期系列的 Stage 3 实现计划展开已激活的动态数据节点如何安全地接管既有物理 Slot 副本。读完你可以理解 WuKongIM 如何用「保持DesiredPeers不变的任务态暂存目标」替代直接修改成员集掌握slot_replica_move任务的阶段围栏phase fence设计、open_learner → add_learner → promote_learner → remove_voter → commit_assignment的完整执行状态机以及 manager 侧有界 onboarding API 的规划/启动/推进/查询全链路并能用仓库中的测试与 e2e 场景验证「迁移期间消息收发不中断」这一核心目标。1. Stage 3 要解决什么问题在 WuKongIM 的动态节点生命周期中Stage 2 让第四台数据节点完成了「加入并激活」joining → ready → active但激活后的节点最初并不承载任何物理 Slot 副本而 Stage 4 及之后的阶段才会处理节点下线、gateway 连接排空与 Channel 副本迁移。Stage 3 专门填补中间这块空白把物理 Slot 副本从已有节点搬迁到已激活的新节点上且搬迁全程保持消息收发可用。Stage 3 实现计划 给出的目标Goal与架构Architecture要点如下GoalAdd bounded Slot replica onboarding so activated dynamic data nodes can receive existing physical Slot replicas safely为已激活的动态数据节点添加有界的 Slot 副本接入使其能安全地接收既有物理 Slot 副本。ArchitectureStage 3 保持DesiredPeers作为已提交的 voter 集合把迁移目标表示为任务本地的暂存状态task-local staged state直到 learner 追赶完成并被证明可以晋升。manager 创建有界的 onboarding 任务tasks层执行器通过 Slot leader 执行 Slot Raft 配置变更只有当观察到的 voter 集合与目标集合完全一致时才提交最终 assignment。目标节点在成为 voter 之前会先通过专门的OpenLearner路径在本地幂等地打开 learner Slot 运行时——这一步不改变DesiredPeers。Tech StackGo、pkg/slot/multiraft配置变更、slots运行时、ControllerV2 任务 FSM、clusterv2 任务执行器、internalv2 manager onboarding API、e2e SEND 冒烟测试。计划文档中明确列出了它在整个系列中的位置四个关联文档分别是总体设计docs/superpowers/specs/2026-06-24-internalv2-dynamic-node-lifecycle-design.md主索引docs/superpowers/plans/2026-06-24-internalv2-dynamic-node-lifecycle.md上一阶段Stage 2节点加入与激活docs/superpowers/plans/2026-06-24-internalv2-dynamic-node-stage2.md下一阶段Stage 4docs/superpowers/plans/2026-06-24-internalv2-dynamic-node-stage4.md边界同样重要Stage 3 不标记节点为leaving不排空 gateway 连接也不迁移历史 Channel 副本——它只移动物理 Slot 副本。这保证了每个 Stage 的职责单一、可独立验证。需要说明的一点计划文本中的包路径pkg/clusterv2、pkg/controllerv2、internalv2、test/e2ev2是文档撰写时的 v2 包命名在当前仓库中对应实现已落在 pkg/cluster、pkg/controller、internal 与 test/e2e 等路径下。本文同时给出计划原文路径与当前仓库中可查证的实际文件位置。2. 入口门槛与 Review 修正六条先于编码的设计约束2.1 Entry Gate进入 Stage 3 的前提计划要求先确认 Stage 2 已实现并提交并且动态加入的 e2e 测试通过GOWORKoff go test -tagse2e ./test/e2ev2/cluster/dynamic_node_join -run TestDynamicJoinFourthDataNode -count1这保证「第四台数据节点可以加入并激活」是 Stage 3 复制搬迁的地基。2.2 Review Corrections六条强制修正计划在编码前列出了六条必须在生产代码中体现的修正这些修正直接决定了后面的数据结构和命令设计目标 learner 的打开是执行的一部分。尚未进入DesiredPeers的目标节点必须通过专门的OpenLearner路径打开本地 Slot Raft 存储/状态机否则到来的 learner Raft 消息会命中ErrSlotNotFound。ConfigAppliedIndex的语义是「最近一次配置变更被应用/观察到的确切 Raft 条目索引」绝不能被无关的普通条目更新污染。slot_replica_move的校验必须允许任务活跃期间TargetPeers与当前DesiredPeers不同但仅限TargetPeers等于「当前 assignment 中用TargetNode替换SourceNode」的情形。remove-voter 拆分在多次 reconcile 尝试之间完成若源节点仍是 Slot leader执行器先把 leadership 转移走持久化/保持remove_voter阶段后返回下一次在观察到的 Slot leader 上执行时才真正移除源 voter。为副本搬迁的创建、阶段推进、最终提交、取消使用显式的 ControllerV2 命令不能藏在一个无定语的通用 task upsert 里。manager 的advance/cancel路由需要精确的 writer 面若取消落到 Stage 3它必须是一个被围栏fenced的 Controller 命令而不是 HTTP-only 的胶水代码。从这些约束可以看出 Stage 3 的核心工程哲学Raft 成员集变更voters/learners与集群控制面的 assignment 记录是两件事前者先行、后者最后提交中间所有状态都必须可持久化、可恢复、可拒绝过期写入。3. Task 1扩展 Slot 运行时状态与 ChangeConfig 契约Stage 3 执行器需要「看见」learner 集合与每个 peer 的复制进度才能做追赶判定因此第一步是扩展pkg/slot/multiraft的Status。计划原文要求扩展后的Status为type PeerProgress struct { // Match is the highest log index known replicated on the peer. Match uint64 // Next is the next log index the leader will send to the peer. Next uint64 // State is the Raft progress state name for operator diagnostics. State string } type Status struct { SlotID SlotID NodeID NodeID LeaderID NodeID CurrentVoters []NodeID // CurrentLearners is the Raft learner set currently observed by this runtime. CurrentLearners []NodeID // ConfState is the latest Raft configuration state observed by storage or Ready processing. ConfState raftpb.ConfState Term uint64 CommitIndex uint64 AppliedIndex uint64 // ConfigAppliedIndex is the applied index at which the latest conf state was observed. ConfigAppliedIndex uint64 // Progress stores leader-observed replication progress by peer. Progress map[NodeID]PeerProgress Role Role }在当前仓库中这一扩展已完整落地pkg/slot/multiraft/types.go 定义了Status与PeerProgress并且针对ConfigAppliedIndex的持久化还专门定义了 ConfigAppliedIndexStorage 接口——注释说明存储实现如果在压缩已应用日志条目时实现了该接口Status.ConfigAppliedIndex就能在原始条目消失后被恢复这恰好呼应了 Review 修正第 2 条对语义纯净性的要求。计划中「在slot.go刷新g.status处填充这些字段、发布前克隆切片/map/raftpb.ConfState」的做法保证对外发布的是不可变快照。配套的失败测试TDD 的 RED 步骤验证ChangeConfig(AddLearner)之后Status中能读到 learner、ConfState.Learners、Progress[2]与非零的ConfigAppliedIndexfunc TestRuntimeStatusIncludesLearnersConfStateAndProgress(t *testing.T) { rt, slotID : newSingleNodeRuntime(t) fut, err : rt.ChangeConfig(context.Background(), slotID, ConfigChange{Type: AddLearner, NodeID: 2}) // ... 省略等待 future 完成的断言 ... st, err : rt.Status(slotID) // 断言 CurrentLearners 包含 2、ConfState.Learners 非空、Progress[2] 存在、ConfigAppliedIndex 非 0 }运行验证命令GOWORKoff go test ./pkg/slot/multiraft -run TestRuntimeStatusIncludesLearnersConfStateAndProgress -count13.1 clusterv2 运行时契约加入 ChangeConfig计划要求在slots包当前为 pkg/cluster/slots的Runtime接口中加入// ChangeConfig submits a Slot Raft membership change through the local runtime. ChangeConfig(context.Context, multiraft.SlotID, multiraft.ConfigChange) (multiraft.Future, error)并由runtime.go中的 Adapter 直接委托给multiraft.Runtime。3.2 OpenLearner目标节点先「开门」再「入群」这是 Stage 3 最关键的新增契约Manager需要OpenLearner(ctx, assignment)约束是校验 manager 装配与正数SlotID本地 Slot 运行时已打开时直接返回 nil幂等用Runtime.OpenSlot打开既有本地存储存储尚无 hard state 时打开一个空的本地 Slot 运行时供后续 learner Raft 流量使用绝不调用BootstrapSlot绝不要求本地节点在assignment.DesiredPeers中。当前仓库中的实现 pkg/cluster/slots/manager.go#L87-L114 与上述约束逐条对应先通过m.runtime.Status(...)探测是否已打开ErrSlotNotFound视为未打开再取存储与状态机后经m.runtime.OpenSlot打开全程不触碰BootstrapSlot。计划要求的聚焦测试也已存在TestManagerOpenLearnerOpensNonDesiredTargetWithoutBootstrap 专门证明「为非 desired 目标打开本地存储且不 bootstrap voters」。4. Task 2ControllerV2 的 staged 任务意图与围栏命令4.1 任务类型、步骤与持久化围栏字段在 pkg/controller/state/types.go 中新增了任务类型与步骤常量// TaskKindSlotReplicaMove moves one physical Slot voter from SourceNode to TargetNode. TaskKindSlotReplicaMove TaskKind slot_replica_move步骤step集合比计划原文还多了一个transfer_leader——这是把 Review 修正第 4 条「remove-voter 跨 reconcile 拆分」落为显式阶段的结果// TaskStepTransferLeader asks Slot Raft to move leadership away from the observed source. TaskStepTransferLeader TaskStep transfer_leader // TaskStepOpenLearner opens the target replica as a non-voting Slot learner. TaskStepOpenLearner TaskStep open_learner // TaskStepAddLearner adds the target node to the Slot Raft learner set. TaskStepAddLearner TaskStep add_learner // TaskStepPromoteLearner promotes the target learner into the Slot Raft voter set. TaskStepPromoteLearner TaskStep promote_learner // TaskStepRemoveVoter removes the source node from the Slot Raft voter set. TaskStepRemoveVoter TaskStep remove_voter // TaskStepCommitAssignment commits the durable Slot assignment after Slot Raft converges. TaskStepCommitAssignment TaskStep commit_assignmentReconcileTask被扩展出四个持久化阶段围栏字段见 pkg/controller/state/types.go#L325-L332// PhaseIndex advances after each externally observed Slot Raft config step. PhaseIndex uint32 // ObservedConfigIndex is the Slot Raft applied index that proved the current phase. ObservedConfigIndex uint64 // ObservedVoters stores the voter set observed for the current phase. ObservedVoters []uint64 // ObservedLearners stores the learner set observed for the current phase. ObservedLearners []uint64这些字段的意义在于崩溃恢复执行器重启后不依赖内存只凭task.Steptask.PhaseIndex 观察到的 Raft 状态就能续跑。4.2 创建任务RequestSlotReplicaMove计划要求新建runtime_slot_replica_move.go当前为 pkg/controller/runtime_slot_replica_move.go其请求/结果类型与实现要点// SlotReplicaMoveRequest describes a staged physical Slot replica move. type SlotReplicaMoveRequest struct { SlotID uint32 SourceNode uint64 TargetNode uint64 TargetPeers []uint64 ConfigEpoch uint64 StateRevision uint64 }RequestSlotReplicaMove的完整逻辑pkg/controller/runtime_slot_replica_move.go#L36-L77取本地集群状态定位 Slot assignment通过validateSlotReplicaMoveRequest做前置校验用确定性规则生成 TaskIDslot-%d-replica-move-%d-to-%d-r%dSlotID、SourceNode、TargetNode、StateRevision——同一状态修订下的同一搬迁会得到同一 TaskID天然幂等构造Step: TaskStepOpenLearner、CompletionPolicy: single_observer的ReconcileTask以KindUpsertSlotReplicaMoveTask命令携带ExpectedRevision req.StateRevision走 Raft 提议路径。校验规则validateSlotReplicaMoveRequest与计划逐条一致当前 Slot assignment 存在且ConfigEpoch匹配SourceNode在当前DesiredPeers中TargetNode是 active 数据节点且尚不在当前DesiredPeers中TargetPeers恰等于「当前DesiredPeers中用TargetNode替换SourceNode」同一 Slot 只允许一个活跃任务冲突时返回ErrSlotActiveTaskConflict。配套的失败测试断言创建任务后DesiredPeers保持不变而任务出现在st.Tasks中——这正是「保持DesiredPeers作为已提交 voter 集合」这一架构原则的可执行证明。4.3 三个显式命令upsert / advance / commit计划明确要求用显式命令替代「无定语的通用 task upsert」。当前仓库 pkg/controller/command/command.go#L23-L28 中// KindUpsertSlotReplicaMoveTask writes a staged Slot replica move task without changing assignment peers. KindUpsertSlotReplicaMoveTask Kind upsert_slot_replica_move_task // KindAdvanceSlotReplicaMovePhase records the next safe Slot replica move phase. KindAdvanceSlotReplicaMovePhase Kind advance_slot_replica_move_phase // KindCommitSlotReplicaMove atomically replaces the Slot assignment and completes the move task. KindCommitSlotReplicaMove Kind commit_slot_replica_move阶段推进命令的负载为type SlotReplicaMovePhaseAdvance struct { TaskID string SlotID uint32 ConfigEpoch uint64 Attempt uint32 ExpectedPhaseIndex uint32 NextStep state.TaskStep ObservedConfigIndex uint64 ObservedVoters []uint64 ObservedLearners []uint64 }FSM handler 必须拒绝过期推进任务 ID、attempt、config epoch、expected phase index、任务类型任一不匹配即Rejected通过则递增PhaseIndex、设置Step、持久化观察到的 config index/voters/learners并重新检查 assignment 守卫防止阶段写入「活过」冲突的 assignment 变更。最终提交命令KindCommitSlotReplicaMove的 handler 要求计划原文要求活跃任务 ID、kind 为slot_replica_move、attempt 匹配观察到的 voters 必须等于TargetPeers任务步骤必须为commit_assignmentObservedConfigIndex非零当前 assignment 的ConfigEpoch等于任务的ConfigEpoch当前DesiredPeers仍包含SourceNode且不包含TargetNodeTargetPeers仍等于当前DesiredPeers中SourceNode被TargetNode替换的结果然后原子地用TargetPeers替换DesiredPeers、递增ConfigEpoch、移除已完成任务。当前实现见 pkg/controller/fsm/mutation_handlers.go#L168-L220applyUpsertSlotReplicaMoveTask/applyAdvanceSlotReplicaMovePhase/applyCommitSlotReplicaMove与围栏校验validateSlotReplicaMovePhaseAdvance。upsert 命令的 handler 只写Tasks段、绝不动Slots段保证「创建意图」与「改变 assignment」在状态机层面严格分离。取消命令KindCancelSlotReplicaMoveTask按计划「仅当本阶段实现 manager cancel 路由时才添加」它必须以任务 ID、Slot ID、任务类型、config epoch、attempt 与期望 revision 围栏并以有界原因operator_cancelled失败化任务。当前仓库的 manager 路由中未注册 cancel 路由见第 6 节与计划「未实现就省略路由而非加 HTTP-only 行为」的取舍一致。4.4 崩溃恢复 FSM 测试计划给出的两个崩溃恢复测试在 pkg/controller/fsm/fsm_test.go 中均已落地func TestApplyAdvanceSlotReplicaMovePhasePersistsFence(t *testing.T) { // upsert 一个 staged 任务后Apply 一个 ExpectedPhaseIndex0 的推进命令 // 断言任务 Steppromote_learner、PhaseIndex1、ObservedConfigIndex33 已持久化 } func TestApplyAdvanceSlotReplicaMovePhaseRejectsStalePhase(t *testing.T) { // 任务 PhaseIndex 已为 1 时携带 ExpectedPhaseIndex0 的推进必须被 Rejected }前者证明围栏可持久化崩溃后状态不丢后者证明过期写入可被拒绝旧 leader/旧执行器无法回滚新阶段。4.5 control 门面与转发计划要求镜像 Stage 2 的转发模式pkg/cluster/control/slot_replica_move.go 定义控制面请求/结果类型并用与 Controller 完全相同的确定性规则生成任务slotReplicaMoveTaskFromRequest两端 TaskID 生成保持一致pkg/cluster/control/codec.go#L165-L166 添加ControlWriteActionSlotReplicaMove slot_replica_move及对应的 wire 字段pkg/cluster/control/control_write.go#L25-L26 在ControlWriteApplier接口中加入RequestSlotReplicaMovepkg/cluster/control/runtime.go#L418-L428 的Runtime.RequestSlotReplicaMove在本地节点不是 Controller leader 时经forwardControlWrite转发。计划同时划清边界不要把副本搬迁创建塞进TaskApplier它只保留给任务结果/进度写入phase advance、final commit、cancel 只有在 applier 方法被显式类型化并围栏时才允许走 task-result RPC否则走通用 control-write 路径。5. Task 3SlotReplicaMoveExecutor 执行引擎执行器是 Stage 3 的心脏。计划要求新建pkg/clusterv2/tasks/slot_replica_move.go当前实现为 pkg/cluster/tasks/slot_replica_move.go。5.1 依赖接口执行器只依赖三个小接口便于单测注入 faketype SlotReplicaMoveRuntime interface { Status(multiraft.SlotID) (multiraft.Status, error) ChangeConfig(context.Context, multiraft.SlotID, multiraft.ConfigChange) (multiraft.Future, error) TransferLeadership(context.Context, multiraft.SlotID, multiraft.NodeID) error } type SlotLearnerOpener interface { OpenLearner(context.Context, slots.Assignment) error } type SlotReplicaMoveWriter interface { AdvanceSlotReplicaMovePhase(context.Context, control.SlotReplicaMovePhaseAdvance) error CommitSlotReplicaMove(context.Context, control.SlotReplicaMoveCommit) error FailTask(context.Context, controller.TaskResult) error }配置结构带两个默认值NewSlotReplicaMoveExecutorPollMax默认 30 次、PollInterval默认 10ms共同限定 learner 追赶观察窗口约 0.3 秒超窗则失败化任务而不是无限等待。5.2 阶段状态机Reconcile只处理TaskKindSlotReplicaMove且CompletionPolicy为single_observer、非 failed、对应 Slot 仍存在的任务。各阶段行为Reconcile 主循环 与各阶段函数阶段执行节点行为open_learner目标节点幂等调用Learners.OpenLearner成功后推进到add_learner不改变 DesiredPeersadd_learnerSlot leader若目标已在 voters 则跳remove_voter已在 learners 则跳promote_learner否则ChangeConfig(AddLearner)观察到目标进入 learners/voters 后推进到promote_learnerpromote_learnerSlot leader等待learnerCaughtUp目标在CurrentLearners且Progress.Match CommitIndex后再ChangeConfig(PromoteLearner)观察到目标进入 voters 后推进到remove_voter未追上时返回deferred等待下一轮 reconcileremove_voterSlot leader源节点仍被 voter 观察到时先确认收敛若LeaderID SourceNode则TransferLeadership到任一非源 voter对应transfer_leader观测标签转移是异步的——未确认时保持任务活跃、下一轮再续leader 已迁走后执行ChangeConfig(RemoveVoter)观察到目标 voter 集合后推进到commit_assignmentcommit_assignmentSlot leader仅在ConfigAppliedIndex ! 0且观察到的 voters 与TargetPeers集合相等时调用MoveWriter.CommitSlotReplicaMove完成控制面提交几个关键细节追赶判定learnerCaughtUp 用progress.Match status.CommitIndex判定 learner 是否已追上这正是 Task 1 扩展Progress字段的用武之地提交观察validMoveCommitObservation 同时要求ConfigAppliedIndex ! 0、源节点不在 voters、voters 集合与TargetPeers完全一致三者缺一则继续 defer有界错误failTask经boundedTaskError截断truncateTaskErrorUTF8上限 1024 字节且保持 UTF-8 完整避免把任意长错误信息写进持久化任务状态可观测性每个阶段经SlotReplicaMoveObserver上报step/result/durationresult 为ok/timeout/conflict/deferred/fail等低基数取值见 slotReplicaMovePhaseResultForError。执行器在 pkg/cluster/default_slots.go 中注册进组合执行器composite executor与计划中的 wiring 代码一致LocalNode来自节点配置、Runtime是 Slot 运行时、Learners是 slots manager、MoveWriter是控制面。5.3 计划中的失败测试TDD 契约计划给出的三个执行器测试当前见 pkg/cluster/tasks/slot_replica_move_test.go分别锁定三条不变量TestSlotReplicaMoveExecutorAddsLearnerBeforeDesiredPeersChange——第一个变更必须是AddLearner target 4且在 learner 追上之前绝不允许completed即不得先改 assignmentTestSlotReplicaMoveExecutorOpensTargetLearnerBeforeDesiredPeersChange——目标节点上open_learner阶段必须实际调用OpenLearner打开 Slot 1TestSlotReplicaMoveExecutorResumesFromPersistedPromotePhase——从持久化的promote_learner阶段PhaseIndex: 1恢复后首个变更是PromoteLearner target 4并携带ExpectedPhaseIndex1、NextStepremove_voter的阶段围栏——直接验证崩溃恢复路径。6. Task 4manager 的有界 Onboarding API6.1 有界性参数与规划器internal/usecase/management/slot_onboarding.go 定义了有界性常量// DefaultMaxSlotMoves is the default number of Slot replica moves created per onboarding request. DefaultMaxSlotMoves uint32 1 // MaxSlotMoves is the hard cap for Slot replica moves created per onboarding request. MaxSlotMoves uint32 5即每次 onboarding 请求默认只创建 1 个搬迁任务上限 5 个——「bounded」二字的具体含义。规划器PlanNodeOnboarding的约束拒绝非 active 目标、把MaxSlotMoves归一化到1..5、跳过已有活跃任务的 Slot、按稳定 Slot ID 顺序选候选且候选选择使用投影副本计数PlanNodeOnboarding 中的projectedReplicas先选中的候选会预扣源节点副本数、预加目标节点副本数避免把源节点一次抽干。跳过原因skip reason是一组稳定的低基数常量slot_onboarding.go#L22-L33active_task、target_already_peer、no_source_peer、max_slot_moves_reached、control_conflict。6.2 五个 API 与路由计划要求的五个路由POST /manager/nodes/:node_id/onboarding/plan POST /manager/nodes/:node_id/onboarding/start GET /manager/nodes/:node_id/onboarding/status POST /manager/nodes/:node_id/onboarding/advance POST /manager/nodes/:node_id/onboarding/cancel当前仓库 internal/access/manager/server.go#L475-L485 注册了其中四个plan、start、advance挂在写权限作用域cluster.slot:w的nodeWrites路由组status挂在读权限作用域cluster.slot:r的nodes路由组cancel按计划取舍被省略因为对应的 fenced cancel 命令未在本阶段落地——宁可缺路由也不加 HTTP-only 行为。行为约定计划原文start与advance走同一条有界规划路径每次请求最多创建max_slot_moves个新任务实现中的executeNodeOnboarding用「每轮规划 1 个候选 最多2×maxMoves次重试预算」的循环逐个提交遇到并发控制状态变更revision 冲突时以control_conflict停止而不是盲目重试status汇总目标节点上的活跃slot_replica_move任务给出TotalActive/Pending/Running/Failed聚合计数与按 Slot ID 排序的任务列表。6.3 usecase 规划测试计划给出的规划测试锁定两个核心断言4 台 active 数据节点、2 个 Slot 各由{1,2,3}承载时PlanNodeOnboarding{TargetNodeID: 4, MaxSlotMoves: 1}必须恰好产生 1 个以节点 4 为目标、StateRevision回显 12 的候选。这验证了「有界」与「快照围栏」两个性质。7. Task 5e2e 验证「搬迁期间 SEND 不中断」计划要求在test/e2ev2/cluster/dynamic_node_join/slot_onboarding_test.go当前位于 test/e2e/cluster/dynamic_node_join/slot_onboarding_test.go中新增场景func TestSlotReplicaMoveKeepsSendAvailable(t *testing.T) { cluster : suite.StartCluster(t, suite.ClusterConfig{Nodes: 3}) manager : cluster.ManagerClient(t, 1) sender : cluster.Client(t, sender) receiver : cluster.Client(t, receiver) channel : suite.NewTestChannel(t, sender, receiver) // 第 4 台种子加入节点joining → ready → activate → active node4 : cluster.StartSeedJoinNode(t, suite.SeedJoinNodeConfig{NodeID: 4, Seeds: cluster.SeedAddrs(), JoinAddr: cluster.NodeAddr(4)}) defer node4.Stop(t) manager.EventuallyNodeJoinState(t, 4, joining, 20*time.Second) manager.EventuallyNodeReadiness(t, 4, true, 20*time.Second) manager.MustActivateNode(t, 4) manager.EventuallyNodeJoinState(t, 4, active, 20*time.Second) // 有界 onboarding规划 1 个搬迁并启动 plan : manager.MustPlanOnboarding(t, 4, 1) if len(plan.Candidates) ! 1 { t.Fatalf(candidates %#v, want one, plan.Candidates) } manager.MustStartOnboarding(t, 4, plan.StateRevision, 1) // 搬迁全程持续收发 100 条消息 for i : 0; i 100; i { suite.MustSendAndReceive(t, sender, receiver, channel, fmt.Sprintf(move-%03d, i)) } manager.EventuallyOnboardingSafe(t, 4, 30*time.Second) }这个测试把 Stage 3 的承诺压成一条端到端断言在某个 Slot 的 learner→voter→移除源节点全过程进行中SEND 路径始终可用100 条消息无丢失、无失败最终以EventuallyOnboardingSafe确认 onboarding 收敛。计划要求的验证命令GOWORKoff go test -tagse2e ./test/e2ev2/cluster/dynamic_node_join -run TestSlotReplicaMoveKeepsSendAvailable -count1按当前仓库布局对应./test/e2e/cluster/dynamic_node_join。同时计划要求更新三处 FLOW 文档internalv2/infra/cluster/FLOW.md、internalv2/usecase/management/FLOW.md、internalv2/access/manager/FLOW.md对应当前 pkg/cluster/FLOW.md 与 internal 下各包 FLOW 文档记录如下调用链并声明「目标 learner 在晋升前不在DesiredPeers中」manager onboarding route - management onboarding planner - SlotReplicaMoveWriter - ControllerV2 slot_replica_move task - clusterv2 task executor - Slot Raft ChangeConfig - final ControllerV2 assignment commit8. 产出物路径与验证门禁8.1 文件结构总览计划 vs 当前仓库层计划路径当前仓库落点Slot 状态扩展pkg/slot/multiraft/types.go、slot.gopkg/slot/multiraft/types.goslots 契约pkg/clusterv2/slots/{types,runtime,manager}.gopkg/cluster/slots含 OpenLearner控制面类型/转发pkg/clusterv2/control/{snapshot,slot_replica_move,codec,runtime,transport}.gopkg/cluster/control含 slot_replica_move.goController 状态/FSMpkg/controllerv2/{state,command,fsm} 新建runtime_slot_replica_move.gopkg/controller含 runtime_slot_replica_move.go任务执行器pkg/clusterv2/tasks/slot_replica_move.gopkg/cluster/tasks/slot_replica_move.go执行器注册pkg/clusterv2/default_slots.gopkg/cluster/default_slots.gomanager onboardinginternalv2/usecase/management/slot_onboarding.go、internalv2/access/manager/*internal/usecase/management/slot_onboarding.go、internal/access/manager/server.goe2e 场景test/e2ev2/cluster/dynamic_node_join/slot_onboarding_test.gotest/e2e/cluster/dynamic_node_join/slot_onboarding_test.go8.2 Exit GateStage 3 全量验证GOWORKoff go test ./pkg/slot/multiraft ./pkg/clusterv2/slots ./pkg/controllerv2 ./pkg/clusterv2/control ./pkg/clusterv2/tasks ./pkg/clusterv2 GOWORKoff go test ./internalv2/usecase/management ./internalv2/access/manager ./internalv2/app GOWORKoff go test -tagse2e ./test/e2ev2/cluster/dynamic_node_join -run TestDynamicJoinFourthDataNode|TestSlotReplicaMoveKeepsSendAvailable -count1 git diff --check按当前仓库布局第一条对应./pkg/slot/multiraft ./pkg/cluster/slots ./pkg/controller ./pkg/cluster/control ./pkg/cluster/tasks ./pkg/cluster第二条对应./internal/usecase/management ./internal/access/manager ./internal/app。全部通过后用 grep 确认 Stage 4 前置条件——副本搬迁是任务化task-backed、分阶段staged、有界bounded、可观察observable的rg -n slot_replica_move|RequestSlotReplicaMove|TaskKindSlotReplicaMove|ChangeConfig|CurrentLearners|ConfigAppliedIndex pkg internalv2 test/e2ev29. 小结Stage 3 的设计取舍Stage 3 用三个层次的「延迟提交」换来了搬迁全程的可用性控制面延迟提交DesiredPeers在 learner 追赶并晋升、源 voter 被移除、观察到的 voter 集合与目标集合完全一致之前始终维持旧值中间状态全部落在任务围栏字段PhaseIndex、ObservedConfigIndex、ObservedVoters、ObservedLearners里可持久化、可拒绝过期写入、可在崩溃后从Step PhaseIndex续跑。Raft 分步收敛open_learner → add_learner → promote_learner → (transfer_leader) → remove_voter → commit_assignment每步只做一个可回退的最小动作leader 转移这种异步操作被显式拆成跨 reconcile 的两步避免单节点同时承担转移与移除。入口有界manager 每次请求至多创建 1默认到 5上限个搬迁任务且每个 Slot 同一时刻至多一个活跃任务把并发搬迁引入的 Raft 配置变更风暴限制在可诊断范围内。结合 总体设计 阅读Stage 3 是整个动态节点生命周期中「扩容真正生效」的一环Stage 2 给出节点Stage 3 给出数据Stage 4 及以后才开始处理收缩与下线。赞分享即时通讯后端【免费下载链接】WuKongIMMore than just IM 不只是即时通讯(IM)项目地址https://gitcode.com/gh_mirrors/wu/WuKongIM点击查看免费下载相关推荐WuKongIM 手动单 Slot 领导权转移Slot Leader Transfer任务机制详解WuKongIM 手动单 Slot 领导权转移Slot Leader Transfer任务机制详解 本文围绕 WuKongIM 中 leader_trans即时通讯后端WuKongIM 发送权限准入的失败关闭两副本 Slot 丢失 voter 后的有界 503 与恢复证据WuKongIM 发送权限准入的失败关闭两副本 Slot 丢失 voter 后的有界 503 与恢复证据 本文基于 WuKongIM 仓库内的 E2E 报告即时通讯后端WuKongIM MQTT 回放协调回合有界复制、锚点准入与副本恢复的协调实现剖析WuKongIM MQTT 回放协调回合有界复制、锚点准入与副本恢复的协调实现剖析 在 WuKongIM 的 MQTT 离线消息回放replay体系中即时通讯后端上一篇OpenLayers 10.2.0 版本解读Flow 粒子流图层、WMS 1.1.1 解析与瓦片缓存优化下一篇learn-harness-engineering Project 02 徹底解説Agent-Readable Workspace と永続化状態ファイルによる「中断地点からの再開」创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站