Redis-10-集群

数据分布

数据分布理论

常见的分区规则有哈希分区和顺序分区

分区方式 特点 代表产品
哈希分区 离散度好;数据分布业务无关;无法顺序访问 Redis Cluster; Cassandra; Dynamo
顺序分区 离散度易倾斜;数据分布业务相关;可顺序访问 Bigtable; HBase; Hypertable

Redis Cluster 采用哈希分区规则,常见的哈希分区规则有几种:

  1. 节点取余分区

    使用特定的数据,如 Redis 的键或用户 ID,再根据节点数量 N 使用公式:$hash(key) \% N$ 计算出哈希值,用来决定数据映射到哪一个节点上。

    • 优点:简单,常用于数据库的分库分表规则
    • 缺点:当节点数量变化时,数据节点映射关系需要重新计算,会导致数据的重新迁移
  2. 一致性哈希分区

    一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节点分配一个 token,范围一般在 $0 - 2^{32}$,这些 token 构成一个哈希环。数据读写执行节点查找操作时,先根据 key 计算 hash 值,然后顺时针找到第一个大于等于该哈希值的 token 节点。

    • 优点:加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响
    • 缺点:
      • 加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景
      • 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案
      • 普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡
  3. 虚拟槽分区

    使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,这个范围一般远远大于节点数。整数定义为槽(slot),槽是集群内数据管理和迁移的基本单位,每个节点会负责一定数量的槽。

Redis 数据分区

Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,计算公式为 $slot = CRC(key) \& 16383$。每一个节点负责维护一部分槽以及槽所映射的键值数据。

Redis 虚拟槽分区的特点

  • 解耦数据和节点之间的关系,简化了节点扩容和收缩难度
  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
  • 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景

集群功能限制

Redis 集群相对单机在功能上存在一些限制:

  1. key 批量操作支持有限,目前只支持具有相同 slot 值的 key 执行批量操作
  2. key 事务操作支持有限,只支持多 key 在同一节点上的事务操作
  3. key 作为数据分区的最小粒度,因此不能将一个大的键值对象如 hash、list 等映射到不同的节点
  4. 不支持多数据库空间,单机下的 Redis 可以支持 16 个数据库,集群模式下只能使用一个数据库空间,即 db0
  5. 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构

搭建集群

搭建集群工作需要以下三个步骤:

  1. 准备节点
  2. 节点握手
  3. 分配槽

准备节点

Redis 集群一般由多个节点组成,节点数量至少为 6 个才能保证组成完整高可用的集群。

节点 ID 在集群初始化时只创建一次,节点重启时会加载集群配置文件进行重用,而 Redis 的运行 ID 每次重启都会变化。

每个节点目前只能识别出自己的节点信息,每个节点彼此并不知道对方的存在。

节点握手

节点握手是指一批运行在集群模式下的节点通过 Gossip 协议彼此通信,达到感知对方的过程,由客户端发起命令:cluster meet {ip} {port}

cluster meet 命令是一个异步命令,执行之后立刻返回,内部发起与目标节点握手通信:

  1. 节点 6379 本地创建 6380 节点信息对象,并发送 meet 消息
  2. 节点 6380 接受到 meet 消息后,保存 6379 节点信息并回复 pong 消息
  3. 之后节点 6379 和 6380 彼此定期通过 ping/pong 消息进行正常的节点通信

节点建立握手之后,由于目前所有的槽没有分配到节点,因此集群无法完成槽到节点的映射,这时集群处于下线状态,还不能正常工作,所有的数据读写都被禁止。

分配槽

Redis 集群把所有的数据映射到 16384 个槽中,通过 cluster addslots 命令为节点分配槽。如 redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0..5461}

作为一个完整的集群,每个负责处理槽的节点都应该具有从节点,保证当它出故障时可以自动进行故障转移。使用 cluster replicate {nodeId} 命令让一个节点成为从节点,该命令执行必须在对应的从节点上执行,nodeId 是要复制的主节点的节点 ID。

用 redis-trib.rb 搭建集群

redis-trib.rb 是采用 Ruby 实现的 Redis 集群管理工具。内部通过 Cluster 相关命令帮我们简化集群创建、检查、槽迁移和均衡等常见运维操作,使用之前需要安装 Ruby 依赖环境。

  1. Ruby 环境准备
  2. 准备节点
  3. 创建集群

    • redis-cli --cluster create <ip1:port1>...<ipN:portN> --cluster-replicas 1
    • –replicas 参数指定集群中每个主节点配备几个从节点,这里设置为 1
  4. 集群完整性检查

    • 集群完整性指所有的槽都分配到存活的主节点上,只要 16384 个槽中有一个没有分配给节点则表示集群不完整。check 命令只需要给出集群中任意一个节点地址就可以完成整个集群的检查工作,命令 redis-cli --cluster check ip:port

节点通信

通信流程

节点元数据是指:节点负责哪些数据,是否出现故障等状态信息。

常见的元数据维护方式为:集中式和 P2P 方式。

Redis 集群采用 P2P 的 Gossip(流言) 协议,该协议的工作原理是节点彼此不断通信交换信息,一段时间后所有节点都会知道集群完整的信息,这种方式类似流言传播。

通信过程说明:

  1. 集群中的每个节点都会单独开辟一个 TCP 通道,用于节点之间彼此通信,通信端口号在基础端口上加 10000
  2. 每个节点在固定周期内通过特定规则选择几个节点发送 ping 消息
  3. 接收到 ping 消息的节点用 pong 消息作为响应

Gossip 消息

常用的 Gossip 消息可分为:ping 消息、pong 消息、meet 消息、fail 消息

  • meet 消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet 消息通信正常完成后,接收节点会加入到集群中并进行周期性的 ping、pong 消息交换
  • ping 消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送 ping 消息,用于检测节点是否在线和交换彼此状态信息。ping 消息发送封装了自身节点和部分其他节点的状态数据。
  • pong 消息:当接收到 ping、meet 消息时,作为响应消息回复给发送方确认消息正常通信。pong 消息内部封装了自身状态数据。节点也可以向集群内广播自身的 pong 消息来通知整个集群对自身状态进行更新。
  • fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail 消息,其他节点接收到 fail 消息之后把对应节点更新为下线状态。

所有的消息格式划分为:消息头和消息体。

接收节点收到ping/meet消息时,执行解析消息头和消息体流程:

  1. 解析消息头过程:消息头包含了发送节点的信息,如果发送节点是新节点且消息是 meet 类型,则加入到本地节点列表;如果是已知节点,则尝试更新发送节点的状态,如槽映射关系、主从角色等状态。
  2. 解析消息体过程:如果消息体的 clusterMsgDataGossip 数组包含的节点是新节点,则尝试发起与新节点的 meet 握手流程;如果是已知节点,则根据 clusterMsgDataGossip 中的 flags 字段判断该节点是否下线,用于故障转移。

节点选择

消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量。

  1. 选择发送消息的节点数量

    • 每个节点维护定时任务默认每秒执行 10 次,每秒会随机选取 5 个节点找出最久没有通信的节点发送 ping 消息
    • 每 100 毫秒都会扫描本地节点列表,如果发现节点最近一次接受 pong 消息的时间大于$cluster_node_timeout / 2$,则立刻发送 ping 消息
  2. 消息数据量

    • 消息头主要占用空间的字段是 myslots[CLUSTER_SLOTS/8] ,占用 2KB,这块空间占用相对固定
    • 消息体会携带一定数量的其他节点信息用于信息交换,消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高

集群伸缩

伸缩原理

上层原理:集群伸缩 = 槽和数据在节点之间的移动

扩容集群

Redis 集群扩容操作可分为如下步骤:

  1. 准备新节点

  2. 加入集群
    对于加入集群的新节点的后续操作一般有两种选择:

    • 为它迁移槽和数据实现扩容
    • 作为其他主节点的从节点负责故障转移
  3. 迁移槽和数据
    1) 制定槽迁移计划:迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀
    2) 迁移数据:数据迁移过程是逐个槽进行的
    3) 添加从节点:从节点内部除了对主节点发起全量复制之外,还需要更新本地节点的集群相关状态

收缩集群

流程说明:

  1. 首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽节点映射的完整性
  2. 当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭

当下线主节点具有从节点时需要把该从节点指向到其他主节点,因此对于主从节点都下线的情况,建议先下线从节点再下线主节点,防止不必要的全量复制

请求路由

请求重定向

在集群模式下,Redis 接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点

  • 如果节点是自身,则处理建键命令
  • 否则回复 MOVED 重定向错误,通知客户端请求正确的节点

这个过程称为 MOVED 重定向。

使用 redis-cli 命令时,可以加入 -c 参数以支持自动重定向

  • redis-cli 自动帮我们连接到正确的节点执行命令,这个过程是在 redis-cli 内部维护,实质上是 client 端接到 MOVED 信息之后再次发起请求,并不在 Redis 节点中完成请求转发
  • 节点对于不属于它的键命令只回复重定向响应,并不负责转发

Smart 客户端

原理:Smart 客户端通过在内部维护 slot→node 的映射关系,本地就可实现键到节点的查找,从而保证 IO 效率的最大化,而 MOVED 重定向负责协助 Smart 客户端更新 slot→node 映射。

ASK 重定向

  1. 客户端 ASK 重定向流程

    Redis 集群支持在线迁移槽(slot)和数据来完成水平伸缩,当 slot 对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。客户端键命令执行流程:
    1) 客户端根据本地 slots 缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端
    2) 如果键对象不存在,则可能存在于目标节点,这时源节点会回复 ASK 重定向异常。格式如下:(error) ASK {slot} {targetIP}:{targetPort}
    3) 客户端从 ASK 重定向异常提取出目标节点信息,发送 asking 命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息

    ASK 与 MOVED 虽然都是对客户端的重定向控制,但是有着本质区别

    • ASK 重定向说明集群正在进行 slot 数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新 slots 缓存
    • MOVED 重定向说明键对应的槽已经明确指定到新的节点,因此需要更新 slots 缓存
  2. 节点内部处理

    为了支持 ASK 重定向,源节点和目标节点在内部的 clusterState 结构中维护当前正在迁移的槽信息,用于识别槽迁移情况

    节点每次接收到键命令时,都会根据clusterState内的迁移属性进行命令处理,如下所示:

    • 如果键所在的槽由当前节点负责,但键不存在则查找 migrating_slots_to 数组查看槽是否正在迁出,如果是,返回 ASK 重定向
    • 如果客户端发送 asking 命令打开了 CLIENT_ASKING 标识,则该客户端下次发送键命令时查找 importing_slots_from 数组获取 clusterNode,如果指向自身则执行命令
    • 需要注意的是,asking 命令是一次性命令,每次执行完后客户端标识都会修改回原状态,因此每次客户端接收到 ASK 重定向后都需要发送 asking 命令
    • 批量操作。ASK 重定向对单键命令支持得很完善,但是,在开发中我们经常使用批量操作,如 mget 或 pipeline。当槽处于迁移状态时,批量操作会受到影响

故障转移

故障发现

  1. 主观下线

    如果在 cluster-node-timeout 时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态

  2. 客观下线

    当半数以上持有槽的主节点都标记某个节点是主观下线时,触发客观下线流程

    流程说明:
    1) 当消息体内含有其他节点的 pfail 状态时,会判断发送节点的状态,如果发送节点是主节点则对报告的 pfail 状态处理,从节点则忽略
    2) 找到 pfail 对应的节点结构,更新 clusterNode 内部下线报告链表
    3) 根据更新后的下线报告链表告尝试进行客观下线

    • 维护下线报告链表
      • 每个节点 ClusterNode 结构中都会存在一个下线链表结构,保存了其他主节点针对当前节点的下线报告
      • 每个下线报告都存在有效期,每次在尝试触发客观下线时,都会检测下线报告是否过期,对于过期的下线报告将被删除
    • 尝试客观下线
      • 首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出
      • 当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态
      • 向集群广播一条 fail 消息,通知所有的节点将故障节点标记为客观下线,fail 消息的消息体只包含故障节点的 ID。该步骤职责如下
        • 通知集群内所有的节点标记故障节点为客观下线状态并立刻生效
        • 通知故障节点的从节点触发故障转移流程

故障恢复

故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。

故障恢复流程:

  1. 资格检查

    每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过 $cluster-node-time * cluster-slave-validity-factor$,则当前从节点不具备故障转移资格。

  2. 准备选举时间

    当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。

    • 之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。
  3. 发起选举

    当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:
    1) 更新配置纪元
    2) 广播选举消息:在一个配置纪元内只能发起一次选举

  4. 选举投票

    • 只有持有槽的主节点才会处理故障选举消息
    • 每个持有槽的节点在一个配置纪元内都有唯一的一张选票
    • 投票过程其实是一个领导者选举的过程
    • 故障主节点也算在投票数内
    • 投票作废:每个配置纪元代表了一次选举周期,如果在开始投票之后的 $cluster-node-timeout * 2$ 时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止
  5. 替换主节点

    当从节点收集到足够的选票之后,触发替换主节点操作:
    1) 当前从节点取消复制变为主节点。
    2) 执行 clusterDelSlot 操作撤销故障主节点负责的槽,并执行 clusterAddSlot把这些槽委派给自己。
    3) 向集群广播自己的 pong 消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。

故障转移时间

  1. 主观下线(pfail)识别时间 = cluster-node-timeout
  2. 主观下线状态消息传播时间 <= cluster-node-timeout / 2
  3. 从节点转移时间 <= 1000毫秒

集群运维

集群完整性

默认情况下,当集群 16384 个槽中任意一个没有指派到节点时,整个集群不可用。但是当持有槽的主节点下线时,从故障发生到自动完成转移操作期间整个集群都是不可用状态,对于大多数业务来说,无法容忍这种情况,因此建议将参数 cluster-require-full-coverage 配置为 no,当主节点故障时只影响它所负责槽的相关命令,不会影响其他主节点的可用性。

带宽消耗

集群带宽消耗主要分为:读写命令消耗 + Gossip 消息消耗。

搭建 Redis 集群时需要根据业务数据规模和消息通信成本做出合理规划:

  • 在满足业务需要的情况下尽量避免大集群。同一个系统可以针对不同业务场景拆分使用多套集群。这样每个集群既满足伸缩性和故障转移要求,还可以规避大规模集群的弊端。
  • 适度提高 cluster-node-timeout 以降低消息发送频率,但同时 cluster-nodetimeout 还影响故障转移的速度,因此需要根据自身业务场景兼顾二者的平
    衡。
  • 如果条件允许集群尽量均匀部署在更多机器上,避免集中部署。

Pub/Sub 问题

当频繁应用 Pub/Sub 功能时应该避免在大量节点的集群内使用,否则会严重消耗集群内网络带宽。针对这种情况建议使用 sentinel 结构专门用于 Pub/Sub 功能,从而规避这一问题。

集群倾斜

集群倾斜指不同节点之间数据量和请求量出现明显差异

  1. 数据倾斜

    • 节点和槽分配严重不均。
      • 可使用 redis-cli --cluster info ip:port 进行查看
      • 当节点对应槽数量不均匀时,可使用 redis-cli --cluster rebalance ip:port 进行平衡
    • 不同槽对应键数量差异过大
      • 键通过 CRC16 哈希函数映射到槽上,正常情况下槽内键数量会相对均匀。但当大量使用 hash_tag 时,会产生不同的键映射到同一个槽的情况
      • 通过命令 cluster countkeysinslot {slot} 可以获取槽对应的键数量,识别出哪些槽映射了过多的键。
      • 通过命令 cluster getkeysinslot {slot} {count} 循环迭代出槽下所有的键。从而发现过度使用 hash_tag 的键。
    • 集合对象包含大量元素
      • 可使用 redis-cli --bigkeys 识别大集合对象
    • 内存相关配置不一致
      • 内存相关配置指 hash-max-ziplist-value、setmax-intset-entries 等压缩数据结构配置
      • 当集群大量使用 hash、set 等数据结构时,如果内存压缩数据结构配置不一致,极端情况下会相差数倍的内存,从而造成节点内存量倾斜
  2. 请求倾斜

    集群内特定节点请求量/流量过大将导致节点之间负载不均,影响集群均衡和运维成本。避免方式如下:

    • 合理设计键,热点大集合对象做拆分或使用 hmget 替代 hgetall 避免整体读取
    • 不要使用热键作为 hash_tag,避免映射到同一槽
    • 对于一致性要求不高的场景,客户端可使用本地缓存以减少热键调用

集群读写分离

集群模式下从节点不接受任何读写请求,发送过来的键命令会重定向到负责槽的主节点上(其中包括它的主节点)

  1. 只读连接

    • 当需要使用从节点分担主节点读压力时,可以使用 readonly 命令打开客户端连接只读状态。
    • 当开启只读状态时,从节点接收读命令处理流程为:如果对应的槽属于自己正在复制的主节点则直接执行读命令,否则返回重定向信息。
    • readonly 命令时连接级别生效,因此每次新建连接时都需要执行 redaonly 开启只读状态。
    • 执行 readwrite 命令可以关闭连接只读状态
  2. 读写分离

    集群模式下读写分离涉及对客户端修改如下:

    • 维护每个主节点可用从节点列表
    • 针对读命令维护请求节点路由
    • 从节点新建连接开启 readonly 状态

    集群模式下读写分离成本比较高,可以直接扩展主节点数量提高集群性能,一般不建议集群模式下做读写分离。

手动故障转移

Redis 集群提供了手动故障转移功能:指定从节点发起转移流程,主从节点角色进行切换,从节点变为新的主节点对外提供服务,旧的主节点变为它的从节点。

在从节点上执行 cluster failover 命令发起转移流程,默认情况下转移期间客户端请求会有短暂的阻塞,但不会丢失数据,流程如下:

  1. 从节点通知主节点停止处理所有客户端请求
  2. 主节点发送对应从节点延迟复制的数据
  3. 从节点接收处理复制延迟的数据,直到主从复制偏移量一致为止,保证复制数据不丢失
  4. 从节点立刻发起投票选举(这里不需要延迟触发选举)。选举成功后断开复制变为新的主节点,之后向集群广播主节点 pong 消息
  5. 旧主节点接受到消息后更新自身配置变为从节点,解除所有客户端请求阻塞,这些请求会被重定向到新主节点上执行
  6. 旧主节点变为从节点后,向新的主节点发起全量复制流程

手动故障转移的应用场景主要如下:

  1. 主节点迁移
  2. 强制故障转移

    cluster failover 命令提供了两个参数 force/takeover

    • cluster failover force:用于当主节点宕机且无法自动完成故障转移情况。从节点接到 cluster failover force 请求时,从节点直接发起选举,不再跟主节点确认复制偏移量(从节点复制延迟的数据会丢失),当从节点选举成功后替换为新的主节点并广播集群配置。
    • cluster failover takeover:用于集群内超过一半以上主节点故障的场景,因为从节点无法收到半数以上主节点投票,所以无法完成选举过程。可以执行 cluster failover takeover 强制转移,接到命令的从节点不再进行选举流程而是直接更新本地配置纪元并替换主节点。会导致配置纪元存在冲突的可能,当冲突发生时,集群会以 nodeId 字典序更大的一方配置为准

手动故障转移时,在满足当前需求的情况下建议优先级:cluster failver > cluster failover force > cluster failover takeover