第 79 篇里 redis-cli 一次搬 4096 个槽,把细节都藏起来了。这篇把一个槽的迁移拆成四步,亲手执行,同时让一个客户端持续读写这个槽,看每一阶段它遇到什么;然后做一件危险的事:搬到一半放弃,看看会留下什么,以及怎么找回。
> CLUSTER SETSLOT 8943 IMPORTING eeec82b846e33f8afdf15616137e0046a999fe44 "OK" > CLUSTER SETSLOT 8943 MIGRATING 6939912bef66ff9d366125af4d87c19b0323e3e7 "OK" GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 3.1 ms,源上槽里还剩 1801 个,目标上 200 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 2.7 ms,源上槽里还剩 1601 个,目标上 400 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 2.3 ms,源上槽里还剩 1401 个,目标上 600 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 1.0 ms,源上槽里还剩 1201 个,目标上 800 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 2.6 ms,源上槽里还剩 1001 个,目标上 1000 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 2.8 ms,源上槽里还剩 801 个,目标上 1200 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 2.0 ms,源上槽里还剩 601 个,目标上 1400 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 2.6 ms,源上槽里还剩 401 个,目标上 1600 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 2.7 ms,源上槽里还剩 201 个,目标上 1800 个 GETKEYSINSLOT → 200 个 key → MIGRATE OK 用时 3.6 ms,源上槽里还剩 1 个,目标上 2000 个 GETKEYSINSLOT → 1 个 key → MIGRATE OK 用时 0.5 ms,源上槽里还剩 0 个,目标上 2001 个 > CLUSTER SETSLOT 8943 NODE 6939912bef66ff9d366125af4d87c19b0323e3e7 "OK" > CLUSTER SETSLOT 8943 NODE 6939912bef66ff9d366125af4d87c19b0323e3e7 "OK" 计数器:客户端确认的 INCR 次数 = 2024,槽里最终的 counter = 2024(一致)
这是 redis-cli --cluster reshard 内部做的事:1) 目标 SETSLOT 槽 IMPORTING 源ID;2) 源 SETSLOT 槽 MIGRATING 目标ID;3) 循环 GETKEYSINSLOT 取一批 key,用 MIGRATE … KEYS 把它们搬到目标;4) 所有节点 SETSLOT 槽 NODE 目标ID 宣布易主。每批 200 个 key 只要 1–5 ms。
阶段 操作数 ASK MOVED 错误 p50 µs p99 µs max µs 前 309 0 0 0 328 791 981 设置状态 159 0 0 0 238 1196 1259 搬 key 中 965 807 0 0 511 1697 5206 收尾 136 271 1 0 871 1504 26781 后 455 0 0 0 339 619 6737
「前」和「设置状态」阶段没有 ASK:key 都还在源上。「搬 key 中」阶段有 807 次 ASK,p50 从 328 µs 涨到 511 µs(ASK 多一次往返,且要多发一个 ASKING)。「收尾」阶段:key 全搬完但槽还没易主,源对每个请求都回 ASK(271 次);最大延迟 26.8 ms 出现在这一阶段。SETSLOT NODE 之后,源改回 MOVED,客户端刷新槽位表,ASK 消失。整个过程 0 次错误。
对账:客户端确认过的 INCR 是 2024 次,槽里的最终计数器是 2024。(客户端用的是「收到 MOVED 就刷新整张槽位表」的策略,见第 71 篇。)
迁移到一半:源 7500 槽内 600 个,目标 7501 槽内 400 个
两端都 STABLE(放弃)之后,槽仍归 7500:通过集群读 1000 个 key,读不到的有 400 个
在目标节点 7501 上看:DBSIZE = 400;KEYS * 返回 0 个;SCAN 返回 0 个;CLUSTER COUNTKEYSINSLOT 4748 = 0;EXISTS {t2}:822 → MOVED 4748 127.0.0.1:7500放弃迁移后,槽仍归源节点,已经搬到目标的 400 个 key 对集群路由来说「不存在」:通过集群读 1000 个 key,400 个读不到。更隐蔽的是目标节点自己的视角:DBSIZE 还是 400(它们占着内存),但 KEYS * 返回 0 个、SCAN 返回 0 个、CLUSTER COUNTKEYSINSLOT 是 0,对它们做 EXISTS 得到 MOVED。这些 key 既不能被访问,也不能被枚举,只有 DBSIZE 能证明它们存在。(这个「按槽组织 key」的内部原因我没有验证,只是这次观察到的现象。)
check 的输出(关键行):
[OK] 3001 keys in 3 masters.
>>> Performing Cluster Check (using node 127.0.0.1:7500)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
fix --cluster-search-multiple-owners 的输出(关键行):
[OK] 3001 keys in 3 masters.
>>> Performing Cluster Check (using node 127.0.0.1:7500)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Check for multiple slot owners...
[OK] No multiple owners found.
fix 之后:读不到的 key 仍有 400 个--cluster check 只报 [OK] 3001 keys in 3 masters、槽位一致、没有 open slots;--cluster fix --cluster-search-multiple-owners 也说没有 multiple owners。fix 之后仍然读不到这 400 个 key。工具检查的是「槽位配置是否一致」,不是「key 是不是放对了节点」。
> CLUSTER SETSLOT 4748 IMPORTING 6939912bef66ff9d366125af4d87c19b0323e3e7
"OK"
再次 IMPORTING 后:目标 CLUSTER COUNTKEYSINSLOT = 400,KEYS * = 400,EXISTS {t2}:822(先 ASKING)→ 1
> CLUSTER SETSLOT 4748 MIGRATING eeec82b846e33f8afdf15616137e0046a999fe44
"OK"
源上还剩 600 个,MIGRATE → OK
SETSLOT NODE 之后:读不到的 key 0 个;槽 4748 归属 7501(目标 7501);各主库 DBSIZE {7500: 2001, 7501: 1000, 7502: 0}办法是让目标节点重新进入 IMPORTING 状态:这 400 个 key 立刻重新可见(KEYS * 又是 400 个,CLUSTER COUNTKEYSINSLOT 也是 400)。这时就可以像正常迁移一样继续:源设 MIGRATING,把剩下的 600 个 key 搬过去,所有节点 SETSLOT NODE 收尾——最终 1000 个 key 都能读到(0 个读不到),槽归目标。(理论上也可以反向把这 400 个搬回源,这一条我没有实测。)放弃一个迁移之前,要么先把搬走的 key 搬回去,要么做完它,不要直接 STABLE。