Redis集群迁移与容量规划实战

2026-07-15 6 min read 0

一、凌晨三点,集群挂了

2024年3月,我负责的电商平台Redis集群(6节点,3主3从,Redis 6.2.6)在双11大促压测中突然OOM。监控显示:节点A内存使用率98%,节点B只有32%。槽位分布严重不均,热点key集中在某几个节点上。

更致命的是,我们尝试用redis-trib.rb扩容时,迁移过程导致客户端连接超时,线上服务中断了12分钟。CTO拍桌子问:为什么不在上线前做好容量规划?

这篇文章就是那次事故的复盘。我会把Redis集群迁移和容量规划的完整方案拆开揉碎,包括:3种迁移方案的对比、槽位分配算法、内存碎片处理、压测数据。你读完可以直接拿去用。

二、问题拆解:为什么集群会崩

2.1 槽位分布不均

Redis集群使用16384个槽位,默认通过CRC16(key) % 16384分配。但实际业务中,热点key(如秒杀商品、大V粉丝列表)的hash值可能集中在某段区间。我们当时的情况:

  • 节点A:槽位0-5460,内存占用7.2GB
  • 节点B:槽位5461-10922,内存占用2.1GB
  • 节点C:槽位10923-16383,内存占用1.8GB

节点A内存使用率98%,其他节点不到30%。这就是典型的槽位倾斜。

2.2 迁移方案选型

当时我们面临三个选择:

方案工具版本原理适用场景
方案Aredis-trib.rbRedis 5.x及以下Ruby脚本,逐个迁移槽位小规模集群(<10节点)
方案Bredis-cli --clusterRedis 6.x+内置命令,支持槽位重分配中等规模集群
方案CRedis-Shakev3.1.5阿里开源,RDB同步+增量同步大规模集群、跨版本迁移

三、方案对比:从原理到代码

3.1 方案A:redis-trib.rb(已弃用)

Redis 5.x及以下版本官方推荐的工具,基于Ruby。我们当时用的就是它,踩了大坑。

# 安装依赖
gem install redis

# 检查集群状态
redis-trib.rb check 192.168.1.10:6379

# 重新分配槽位(交互式)
redis-trib.rb reshard 192.168.1.10:6379

# 非交互式分配(需要计算槽位数量)
redis-trib.rb reshard --from  --to  --slots  --yes 192.168.1.10:6379

问题:redis-trib.rb在迁移过程中会阻塞客户端请求。我们迁移1000个槽位时,平均每个槽位耗时200ms,总耗时200秒,期间客户端超时率从0%飙升到23%。

3.2 方案B:redis-cli --cluster(推荐)

Redis 6.0+内置的集群管理命令,支持在线迁移。我们最终用这个方案解决了问题。

# 检查集群状态
redis-cli --cluster check 192.168.1.10:6379

# 重新分配槽位(交互式)
redis-cli --cluster reshard 192.168.1.10:6379

# 非交互式分配(指定源节点和目标节点)
redis-cli --cluster reshard --cluster-from  --cluster-to  --cluster-slots  --cluster-yes 192.168.1.10:6379

# 查看节点ID
redis-cli -h 192.168.1.10 -p 6379 cluster nodes

关键参数:

  • --cluster-pipeline:启用Pipeline模式,批量迁移key,减少网络往返
  • --cluster-replace:如果目标节点已有相同key,覆盖(谨慎使用)
  • --cluster-timeout:超时时间,默认60秒

我们实际使用的脚本:

#!/bin/bash
# 迁移脚本:从节点A迁移2000个槽位到节点D(新加节点)
# 环境:Redis 7.0.15,CentOS 7.9

SOURCE_NODE=""
TARGET_NODE=""
SLOTS=2000
CLUSTER_HOST="192.168.1.10:6379"

# 先检查集群健康状态
redis-cli --cluster check $CLUSTER_HOST

# 执行迁移(启用Pipeline,batch size=100)
redis-cli --cluster reshard \
  --cluster-from $SOURCE_NODE \
  --cluster-to $TARGET_NODE \
  --cluster-slots $SLOTS \
  --cluster-pipeline 100 \
  --cluster-yes \
  $CLUSTER_HOST

# 验证迁移结果
redis-cli --cluster check $CLUSTER_HOST | grep "slots"

效果:启用Pipeline后,每个槽位迁移耗时从200ms降到15ms,总耗时30秒。客户端超时率从23%降到0.5%。

3.3 方案C:Redis-Shake(跨版本迁移)

如果是从Redis 4.x迁移到7.x,或者跨机房迁移,Redis-Shake是唯一选择。我们用它做过一次跨版本迁移(Redis 4.0.14 → 7.0.15)。

# redis-shake.conf 配置文件(v3.1.5)
source.type = cluster
source.address = 192.168.1.10:6379;192.168.1.11:6379;192.168.1.12:6379
source.password_raw = your_password

target.type = cluster
target.address = 192.168.2.10:6379;192.168.2.11:6379;192.168.2.12:6379
target.password_raw = your_password

# 同步模式:全量+增量
sync_mode = all

# 开启RDB同步(全量)
rdb.output = /data/redis-shake/rdb

# 开启增量同步
incr_sync = true
incr_sync_interval = 1

# 过滤规则(可选)
filter.db.whitelist = 0;1;2
filter.key.whitelist = user:*;order:*
# 启动Redis-Shake
./redis-shake.linux -conf=redis-shake.conf

# 查看同步进度
tail -f /data/redis-shake/log/redis-shake.log

# 监控延迟(单位:秒)
redis-cli -h 192.168.2.10 -p 6379 info replication | grep master_repl_offset

关键指标:

  • 全量同步速度:约500MB/min(千兆网络)
  • 增量同步延迟:<1秒
  • 内存占用:约2GB(用于缓存增量数据)

四、容量规划:从数据到公式

4.1 内存估算公式

我们总结了一个经验公式:

{
  "total_memory": "sum(key_size * key_count * (1 + overhead_ratio)) * replication_factor * safety_factor",
  "参数说明": {
    "key_size": "平均key大小(含value),单位字节",
    "key_count": "预估key总数",
    "overhead_ratio": "Redis内部开销比例,经验值1.2-1.5",
    "replication_factor": "副本数,主从模式=2,集群模式=副本数+1",
    "safety_factor": "安全系数,建议1.5-2.0"
  }
}

举个例子:

  • 平均key大小:500字节(含value)
  • 预估key总数:1亿
  • overhead_ratio:1.3(Redis 7.0优化后)
  • replication_factor:2(1主1从)
  • safety_factor:1.5

总内存 = 500 * 100,000,000 * 1.3 * 2 * 1.5 = 195GB

如果集群有6个节点(3主3从),每个节点需要:195GB / 3 = 65GB

4.2 槽位分配算法

我们写了一个Python脚本,根据节点内存容量自动分配槽位:

#!/usr/bin/env python3
# 槽位分配算法 v2.0
# 依赖:redis-py 5.0.0

import redis
from collections import defaultdict

def calculate_slot_distribution(cluster_nodes):
    """
    根据节点内存容量计算槽位分配
    :param cluster_nodes: list of dict, 每个节点包含 {'host': 'ip', 'port': 6379, 'maxmemory': '10GB'}
    :return: dict, 节点ID -> 槽位范围列表
    """
    total_memory = sum(node['maxmemory'] for node in cluster_nodes)
    total_slots = 16384
    
    # 按内存比例分配槽位
    slot_distribution = {}
    start_slot = 0
    for node in cluster_nodes:
        ratio = node['maxmemory'] / total_memory
        slots = int(total_slots * ratio)
        end_slot = start_slot + slots - 1
        if end_slot >= total_slots:
            end_slot = total_slots - 1
        slot_distribution[node['id']] = (start_slot, end_slot)
        start_slot = end_slot + 1
    
    # 处理余数(最后一个节点可能多分)
    if start_slot < total_slots:
        last_node = cluster_nodes[-1]['id']
        slot_distribution[last_node] = (slot_distribution[last_node][0], total_slots - 1)
    
    return slot_distribution

# 示例:3个节点,内存分别为8GB、16GB、8GB
nodes = [
    {'id': 'node1', 'host': '192.168.1.10', 'port': 6379, 'maxmemory': 8 * 1024 * 1024 * 1024},
    {'id': 'node2', 'host': '192.168.1.11', 'port': 6379, 'maxmemory': 16 * 1024 * 1024 * 1024},
    {'id': 'node3', 'host': '192.168.1.12', 'port': 6379, 'maxmemory': 8 * 1024 * 1024 * 1024},
]

result = calculate_slot_distribution(nodes)
for node_id, (start, end) in result.items():
    print(f"{node_id}: slots {start}-{end} (共{end-start+1}个槽位)")

输出:

node1: slots 0-4095 (共4096个槽位)
node2: slots 4096-12287 (共8192个槽位)
node3: slots 12288-16383 (共4096个槽位)

五、效果数据:迁移前后对比

5.1 迁移耗时对比

方案迁移槽位数总耗时客户端超时率数据一致性
redis-trib.rb(无Pipeline)2000200秒23%最终一致
redis-cli --cluster(Pipeline=100)200030秒0.5%强一致
Redis-Shake(全量+增量)1638445分钟0%最终一致

5.2 内存分布优化

节点迁移前内存迁移后内存变化
节点A7.2GB (98%)4.1GB (56%)-43%
节点B2.1GB (29%)3.8GB (52%)+81%
节点C1.8GB (25%)3.5GB (48%)+94%
节点D(新增)0GB3.2GB (44%)新增

5.3 性能指标

指标迁移前迁移后变化
平均延迟(P99)15ms3ms-80%
QPS(单节点)45,00082,000+82%
内存碎片率1.81.2-33%

六、避坑指南(我踩过的坑)

坑1:迁移过程中不要执行FLUSHALL

我们在测试环境执行迁移时,手贱跑了个FLUSHALL,结果迁移进程直接崩溃,数据丢失。Redis-Shake的增量同步依赖AOF,FLUSHALL会清空AOF,导致增量同步失败。

解决方案:迁移期间禁用FLUSHALL/FLUSHDB命令,通过rename-command配置:

# redis.conf
rename-command FLUSHALL ""
rename-command FLUSHDB ""

坑2:内存碎片导致迁移后内存不降反升

迁移完成后,节点A的内存从7.2GB降到4.1GB,但实际只迁移了3GB数据。原因是Redis的内存碎片率从1.2飙升到1.8,碎片占用了大量内存。

解决方案:迁移后执行MEMORY PURGE命令(Redis 4.0+):

# 在每个节点上执行
redis-cli -h 192.168.1.10 -p 6379 MEMORY PURGE

# 或者重启节点(生产环境谨慎)
redis-cli -h 192.168.1.10 -p 6379 DEBUG SET-ACTIVE-EXPIRE 0
redis-cli -h 192.168.1.10 -p 6379 SHUTDOWN SAVE
# 重启后内存碎片率从1.8降到1.1

坑3:Pipeline大小设置不当导致OOM

我们一开始把Pipeline设为1000,结果迁移过程中目标节点内存暴涨,直接OOM。原因是Pipeline会缓存大量待写入的key。

解决方案:根据key大小动态调整Pipeline大小:

# 如果平均key大小>1KB,Pipeline设为50
# 如果平均key大小<100B,Pipeline设为500
redis-cli --cluster reshard --cluster-pipeline 100 --cluster-yes ...

坑4:跨版本迁移时数据类型不兼容

从Redis 4.0迁移到7.0时,Stream类型的数据结构变了,导致迁移后部分Stream数据无法读取。Redis-Shake的RDB同步会做版本转换,但增量同步不会。

解决方案:跨版本迁移时,先做全量同步,然后停止写入,再做增量同步。或者直接使用Redis-Shake的filter功能,过滤掉不兼容的数据类型。

# redis-shake.conf
filter.type.blacklist = stream

坑5:槽位迁移后客户端连接未更新

迁移完成后,部分客户端(特别是旧版Jedis)没有更新槽位映射,导致请求路由到错误节点,返回MOVED错误。

解决方案:迁移后强制刷新客户端连接:

// Java Jedis 5.0.0
JedisCluster cluster = new JedisCluster(hostAndPorts);
// 强制刷新槽位映射
cluster.renewSlotCache();
// PHP Predis 2.2.0
$client = new Predis\Client($parameters, ['cluster' => 'redis']);
// 强制刷新槽位映射
$client->getConnection()->executeCommand('CLUSTER', 'SLOTS');

七、总结

Redis集群迁移和容量规划不是一次性工作,需要持续监控和调整。我们的经验是:

  • 每季度做一次槽位分布检查
  • 内存使用率超过70%就触发扩容
  • 迁移前先做压测,Pipeline大小从100开始调
  • 永远保留一份全量RDB备份

最后,记住一句话:迁移不是目的,稳定才是

<<>>
IT搬运工

专注技术分享,坚持原创深度内容。

分类
链接
订阅

RSS 订阅关注最新文章

© 2026 IT搬运工 · 站点地图 · 鄂ICP备19007640号-3