Redis(开发与运维):49---集群之(集群伸缩:伸缩原理、集群扩容、集群收缩)

一、伸缩原理

  • Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容,如下图所示:

  • 从上图看出,Redis集群可以实现对节点的灵活上下线控制。其中原理可抽象为槽和对应数据在不同节点之间灵活移动。首先来看我们之前搭建的集群槽和数据与节点的对应关系,如下图所示:

  • 三个主节点分别维护自己负责的槽和对应的数据,如果希望加入1个节点实现集群扩容时,需要通过相关命令把一部分槽和数据迁移给新节点,如下图所示:

  • 图中每个节点把一部分槽和数据迁移到新的节点6385,每个节点负责的槽和数据相比之前变少了从而达到了集群扩容的目的。这里我们故意忽略了槽和数据在节点之间迁移的细节,目的是想让读者重点关注在上层槽和节点分配上来,理解集群的水平伸缩的上层原理:集群伸缩=槽和数据在节点之间的移动
  • 下面将介绍集群扩容和收缩的细节

二、集群扩容

  • 扩容是分布式存储最常见的需求,Redis集群扩容操作可分为如下步骤:
    • 准备新节点
    • 加入集群
    • 迁移槽和数据

①开启集群

  • 现在我们先开启一个集群,集群中有6个节点,其中前3个为主节点,后3个为复制节点
sudo redis-server /opt/redis/conf/redis-6379.conf
sudo redis-server /opt/redis/conf/redis-6380.conf
sudo redis-server /opt/redis/conf/redis-6381.conf
sudo redis-server /opt/redis/conf/redis-6382.conf
sudo redis-server /opt/redis/conf/redis-6383.conf
sudo redis-server /opt/redis/conf/redis-6384.conf

  • 由于输入命令的时候复制关系有点不同,可能与上面的图片不一致,但是都是3对3的复制
  • 然后查看集群信息,可以看到:
    • 槽0-5460属于6379,6383复制6379
    • 槽5461-10922属于6380,6384复制6380
    • 槽10923-16383属于6381,6382复制6381

②准备新节点

  • 下面我们新增两个新节点,一个为6385,一个为6386,其中6385作为主节点,6386复制6385
  • 新节点建议跟集群内的节点配置保持一致,便于管理统一,准备好配置后启动两个节点命令如下:
sudo redis-server /opt/redis/conf/redis-6385.conf
sudo redis-server /opt/redis/conf/redis-6386.conf

  • 启动后的新节点作为孤儿节点运行,并没有其他节点与之通信,集群结构如下图所示:

③加入集群

  • 新节点依然采用cluster meet命令加入到现有集群中。在集群内任意节点执行cluster meet命令让6385和6386节点加入进来,命令如下:
redis-cli -p 6379 cluster meet 127.0.0.1 6385
redis-cli -p 6379 cluster meet 127.0.0.1 6386

 

  • 新节点加入后集群结构如下图所示:

  • 集群内新旧节点经过一段时间的ping/pong消息通信之后,所有节点会发现新节点并将它们的状态保存到本地。例如我们在6380节点上执行cluster nodes命令可以看到新节点信息,如下所示:
redis-cli -p 6380 cluster nodes

  • 新节点刚开始都是主节点状态,但是由于没有负责的槽,所以不能接受任何读写操作。对于新节点的后续操作我们一般有两种选择:
    • 为它迁移槽和数据实现扩容
    • 作为其他主节点的从节点负责故障转移

使用redis-cli --cluster命令实现加入集群

  • 上面我们使用cluster meet命令将两个新节点加入集群,我们还可以使用redis-cli --cluster命令来加入新节点,还实现了直接 添加为从节点的支持,内部同样采用cluster meet命令实现加入集群功能
  • 命令如下:
    • --cluster-slave和--cluster-master-id是可选的,在设置从节点的时候才会用。如果不指定--cluster-master-id会随机分配到任意一个主节点
redis-cli --cluster add-node new_host:new_port existing_host:existing_port --cluster-slave --cluster-master-id <arg>

# 例如下面将6385加入到6379所属的集群中,并且作为117457eab5071954faab5e81c3170600d5192270的从节点
redis-cli --cluster add-node 127.0.0.1:6385 127.0.0.1:6379 --cluster-slave --cluster-master-id 117457eab5071954faab5e81c3170600d5192270
  • 例如对于上面加入集群的操作,我们还可以采取下面的命令:
# 将6385添加到6379所在的集群中
redis-cli --cluster add-node 127.0.0.1:6385 127.0.0.1:6379

# 将6386添加到6379所在的集群中
redis-cli --cluster add-node 127.0.0.1:6386 127.0.0.1:6379
  • 备注:正式环境建议使用redis-cli cluster命令加入新节点,该命令内部会执行新节点状态检查,如果新节点已经加入其他集群或者包含数据,则放弃集群加入操作并打印如下信息:

  • 如果我们手动执行cluster meet命令加入已经存在于其他集群的节点,会造成被加入节点的集群合并到现有集群的情况,从而造成数据丢失和错乱, 后果非常严重,线上谨慎操作

④迁移槽和数据

  • 上面我们添加了两个新节点:6385、6386。其中6385作为主节点存储数据,6386作为从节点复制6385。下面我们要把其他节点的槽和数据迁移到6386这个节点中

槽迁移计划

  • 槽是Redis集群管理数据的基本单位,首先需要为新节点制定槽的迁移 计划,确定原有节点的哪些槽需要迁移到新节点。迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀
  • 例如,在集群中加入 6385节点,如下图所示,加入6385节点后,原有节点负责的槽数量从6380变为4096个

  • 槽迁移计划确定后开始逐个把槽内数据从源节点迁移到目标节点,如下图所示

迁移数据

  • 数据迁移过程是逐个槽进行的,每个槽数据迁移的流程如下图所示。流程说明:
    • 1)对目标节点发送cluster setslot {slot} importing {sourceNodeId}命令,让 目标节点准备导入槽的数据
    • 2)对源节点发送cluster setslot {slot} migrating {targetNodeId}命令,让源 节点准备迁出槽的数据
    • 3)源节点循环执行cluster getkeysinslot {slot} {count}命令,获取count个属于槽{slot}的键
    • 4)在源节点上执行migrate {targetIp} {targetPort} "" 0 {timeout} keys {keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量 迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能 单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数
    • 5)重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节点
    • 6)向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点

  • 使用伪代码模拟迁移过程如下:
def move_slot(source,target,slot):
    # 目标节点准备导入槽
    target.cluster("setslot",slot,"importing",source.nodeId);
    # 目标节点准备全出槽
    source.cluster("setslot",slot,"migrating",target.nodeId);
    while true :
        # 批量从源节点获取键
        keys = source.cluster("getkeysinslot",slot,pipeline_size);
        if keys.length == 0:
            # 键列表为空时,退出循环
            break;
        # 批量迁移键到目标节点
        source.call("migrate",target.host,target.port,"",0,timeout,"keys",keys);
    # 向集群所有主节点通知槽被分配给目标节点
    for node in nodes:
        if node.flag == "slave":
            continue;
        node.cluster("setslot",slot,"node",target.nodeId);
  • 第一步:我们现在6379所在槽中添加3个键,然后使用“cluster keyslot”命令查看这3个键属于哪些槽中,可以看到都在4096这个槽中
set key:test:5028 value:5028
set key:test:68253 value:68253
set key:test:79212 value:79212

  • 第二步:在目标节点(6385)上准备导入槽4096的数据,并且通过cluster nodes命令确认一下导入状态
# 最后的id为6379的id
cluster setslot 4096 importing fe7a63f5ff36e04446d1427ef2a262c940642d3a

# 查看一下导入状态
cluster nodes

  • 第三步:源节点(6379)上准备导出4096数据,并且通过cluster nodes命令确认一下导出状态
# 最后的id为6385的id
cluster setslot 4096 migrating e993fc8bb9e80cac7948e83a190e087bbe58f3aa

# 查看一下导入状态
cluster nodes

  • 第四步:源节点(6379)上批量获取4096对应的键,然后使用migrate命令批量迁移出这3个键
# 批量获取4096对应的键
cluster getkeysinslot 4096 100

# 迁移之前确认一下三个键存在于源节点中
mget key:test:5028 key:test:68253 key:test:79212

# 批量迁移出这3个键
migrate 127.0.0.1 6385 "" 0 5000 keys key:test:5028 key:test:68253 key:test:79212

  • 出于演示目的,我们继续查询这三个键,发现已经不在源节点中, Redis返回ASK转向错误,ASK转向负责引导客户端找到数据所在的节点, 细节将在后面“请求路由”文章中说明
mget key:test:5028 key:test:68253 key:test:79212

  • 第五步:通知所有主节点槽4096指派给目标节点6385
redis-cli -p 6379 cluster setslot 4096 node e993fc8bb9e80cac7948e83a190e087bbe58f3aa
redis-cli -p 6380 cluster setslot 4096 node e993fc8bb9e80cac7948e83a190e087bbe58f3aa 
redis-cli -p 6381 cluster setslot 4096 node e993fc8bb9e80cac7948e83a190e087bbe58f3aa 
redis-cli -p 6385 cluster setslot 4096 node e993fc8bb9e80cac7948e83a190e087bbe58f3aa 

  • 第六步:确认源节点6379不再负责槽4096改为目标节点6385负责

  • 此处我们只将6379所属的槽4096迁移给6385负责,但是其他槽还没有迁移,下面我们使用reids-lic cluster将剩余的槽迁移给6385

⑤使用redis-cli cluster进行迁移

  • 在④中我们使用了各种命令进行迁移,但是这种迁移每次需要自己手动输入需要迁移的键,如果键太多,那么上面的方法显然不适合,因此我们还可以使用redis-cli cluster命令进行迁移
  • 命令如下:
    • host:port:必传参数,集群内任意节点地址,用来获取整个集群信息
    • --from:制定源节点的id,如果有多个源节点,使用逗号分隔,如果是all源节点变为集群内所有主节点,在迁移过程中提示用户输入
    • --to:需要迁移的目标节点的id,目标节点只能填写一个,在迁移过程 中提示用户输入
    • --slots:需要迁移槽的总数量,在迁移过程中提示用户输入
    • --yes:当打印出reshard执行计划时,是否需要用户输入yes确认后再执行reshard
    • --timeout:控制每次migrate操作的超时时间,默认为60000毫秒
    • ·--pipeline:控制每次批量迁移键的数量,默认为10
redis-cli --cluster reshard host:port --from <arg> --to <arg> --slots <arg> --yes --timeout
<arg> --pipeline <arg>
  • reshard命令简化了数据迁移的工作量,其内部针对每个槽的数据迁移同样使用之前的流程

迁移数据

  • 第一步:在④中已经新节点6395迁移了一个槽4096,剩下的槽数据迁移使用redis-cli cluster完成,命令如下:
redis-cli --cluster reshard 127.0.0.1:6379
  • 输入上面的命令之后会让你输入一系列的信息,如下面所示
  • 第二步:首先让你输入需要迁移的槽数量,此处我们输入4096

  • 第三步:然后让你输入目标节点ID,只能指定一个,因为我们需要迁移到6385中,因此下面输入6385的ID

  • 第四步:之后输入源节点的ID,redis会从这些源节点中平均取出对应数量的槽,然后迁移到6385中,下面我们分别输入6379、6380、6381的节点ID。最后要输入done表示结束

  • 第五步:数据迁移之前会打印出所有的槽从源节点到目标节点的计划,确认计划无误后输入yes执行迁移工作:

  • 输入yes之后会输入大量的迁移信息,迁移完成之后reshard命令自动退出
  • 第六步:我们检查节点和槽映射的变化,如下如所示,6385负责的槽变为0-1364、4096、5461-6826、10923-12287

  • 第七步:由于槽用于hash运算本身顺序没有意义,因此无须强制要求节点负责槽的顺序性。迁移之后建议使用下面的命令检查节点之间槽的均衡性。命令如下:
redis-cli --cluster rebalance 127.0.0.1:6380

  • 通过上图可以看出迁移之后所有主节点负责的槽数量差异在2%以内,因此集群节点数据相对均匀,无需调整

⑥添加从节点

  • 上面我们把6385、6386节点加入到集群,节点6385迁移了部分槽和数据作为主节点,但相比其他主节点目前还没有从节点,因此该节点不具备故障转移的能力
  • 这时需要把节点6386作为6385的从节点,从而保证整个集群的高可用。使用下面的命令为主节点添加对应从节点,注意在集群模式下slaveof添加从节点操作不再支持。如下所示:
# 后面的ID为主节点6385的ID
cluster replicate e993fc8bb9e80cac7948e83a190e087bbe58f3aa

  • 从节点内部除了对主节点发起全量复制之外,还需要更新本地节点的集群相关状态,查看节点6386状态确认已经变成6385节点的从节点:

  • 到此整个集群扩容完成,集群关系结构如下图所示

三、集群收缩

  • 收缩集群意味着缩减规模,需要从现有集群中安全下线部分节点。安全下线节点流程如下图所示

  • 流程说明:
    • 1)首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到 其他节点,保证节点下线后整个集群槽节点映射的完整性
    • 2)当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其 他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭
  • 下面我们以上面的环境为基础,进行下线迁移

①下线迁移槽

  • 当前集群中有4个主节点和4个从节点,现在我们想把6381和6382节点进行下线(其中6381是主节点,6382复制6381)
  • 下线节点需要把自己负责的槽迁移到其他节点,原理与之前节点扩容的 迁移槽过程一致
  • 第一步:下线之前,先查看一下6381和6382的信息,6381是主节点,负责槽12288-16383,6382是它的从节点

  • 收缩正好和扩容迁移方向相反,6381变为源节点,其他主节点变为目标节点,源节点需要把自身负责的4096个槽均匀地迁移到其他主节点上。如下图所示

  • 下面使用reshard命令完成槽迁移,由于我们要将6381的槽均匀的分配到其它三个节点中,因此需要输入3次reshard命令,每次迁移的目标节点ID不同
  • 第二步:先迁移到6379节点上,下面输入命令的步骤与“二”中的步骤一样,需要输入很多参数
redis-cli --cluster reshard 127.0.0.1:6381

 

  • 然后输入yes同意迁移

  • 迁移完成之后查看一下信息,6379接管了1365个槽12288-16652

  • 第三步:再迁移到6380节点上,步骤与上面一样,不过需要输入6380的ID
redis-cli --cluster reshard 127.0.0.1:6381

  • 然后输入yes同意迁移

 

  • 迁移完成之后查看一下信息,6380接管了1365个槽13653-15017

  • 第四步:再迁移到6385节点上,步骤与上面一样,不过需要输入6385的ID
redis-cli --cluster reshard 127.0.0.1:6381

 

  • 然后输入yes同意迁移

 

  • 迁移完成之后查看一下信息,6385接管了1366个槽15018-16383

  • 通过上图也可以看出6381节点不再负责任何槽了

②忘记节点

  • 由于集群内的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点。也就是说让其他节点不再与要下线节点进行Gossip消息交换
  • Redis提供了clusteforget {downNodeId}命令实现该功能,如下图所示:

  • 当节点接收到cluster forget {down NodeId}命令后,会把nodeId指定的 点加入到禁用列表中,在禁用列表内的节点不再发送Gossip消息。禁用列表有效期是60秒,超过60秒节点会再次参与消息交换。也就是说当第一次forget命令发出后,我们有60秒的时间让集群内的所有节点忘记下线节点
  • 线上操作不建议直接使用cluster forget命令下线节点,需要跟大量节点命令交互,实际操作起来过于繁琐并且容易遗漏forget节点
  • 建议使用redis-cli --cluster del-node {host:port} {downNodeId}命令,内部实现的伪代码如下:
def delnode_cluster_cmd(downNode):
    # 下线节点不允许包含slots
    if downNode.slots.length != 0
        exit 1
    end
    # 向集群内节点发送cluster forget
    for n in nodes:
        if n.id == downNode.id:
            # 不能对自己做forget操作
            continue;
        # 如果下线节点有从节点则把从节点指向其他主节点
        if n.replicate && n.replicate.nodeId == downNode.id :
            # 指向拥有最少从节点的主节点
            master = get_master_with_least_replicas();
            n.cluster("replicate",master.nodeId);
        #发送忘记节点命令
        n.cluster('forget',downNode.id)
    # 节点关闭
    downNode.shutdown();
  • 从伪代码看出del-node命令帮我们实现了安全下线的后续操作。当下线主节点具有从节点时需要把该从节点指向到其他主节点,因此对于主从节点都下线的情况,建议先下线从节点再下线主节点,防止不必要的全量复制。

开始移除节点

  • 对于6381和6384节点下线操作,命令如下:
# 后面的为6381的ID
redis-cli --cluster del-node 127.0.0.1:6379 0c02519bbb8e6876e3376c94f9703d2d3d4db36a

# 后面的为6382的ID
redis-cli --cluster del-node 127.0.0.1:6379 4fb31d352bc6cdb074c1b04715f7500a615a411e

  • 节点下线后确认节点状态,发现不已经不包含6381和6382这两个节点了

  • 到目前为止,我们完成了节点的安全下线,新的集群结构如下所示

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章