Redis集群方案

基本的 Redis 分布式结构可参考Redis 分布式 这里继续探讨 Redis 集群方案中涉及的一致性问题

基本概念

“Redis 集群”的说法并不准确,只要是多台主配合实现功能就可以称为”集群”

  • Redis 实际上有两种分布式结构:
    • 主从复制集群
    • cluster 模式集群

主从复制集群

  • 主从复制集群实现了 HA(高可用),解决了单点故障
  • 主从间数据保持同步,数据是全量的
  • 主从复制取 AP,强调速度快,但是同步不精准,一致性不足

  • 哨兵只是提供 HA 切换的功能(人工也是一样的),不影响整体架构

cluster 模式集群

  • cluster 模式实现了”分治、分片”,解决了容量、压力的瓶颈问题
  • 每个结点存储一部分数据
  • Cluster 模式取 CP

  • 往往主从复制模式和 Cluster 模式是综合应用的。可以给 Cluster 模式的每一个主加上从已保证 HA(High Availability)
  • cluster 前面可以加一个 Codis 代理,以便执行 pipeline 等辅助操作

partitioning 分区方案

  • 客户端 如:Predis
  • 代理 如:Twemproxy、Codis
  • 查询路由(Query routing) 如:Cluster 模式

  • 一个客户端同时与多个 Redis 实例连接,在客户端可以先 Hash 一次,然后再到对应的 Redis 进行操作。
  • 为了避免客户端和 Redis 连接过多以及实例扩展困难的情况,在中间增加代理解决
  • Cluster 模式,每个结点都保存了相同算法,这样每个 key 该路由到哪个结点处理,是整体确定的

  • 分区方案下,由于 key 被分配到不同物理主机上,导致无法做一些聚合操作或事务操作:
    • 聚合示例:取两个 set 的交集
    • 事务示例:执行事务操作/在 lua 脚本使用多个 key
    • 方案: 这种情况可以用HashTag解决

主从复制模式

  • 一个 Master 可以配备一个或多个 Slave

  • 为了保证 Slave 与 Master 之间数据一致,有以下机制:

    • 连接正常时,Master 会发送一系列命令流来更新 Slave,包括:客户端写入、key 的过期或被(容量不足)逐出
      • 每个 Slave 会被分配一个 ID,Slave 会记录自己的和 Master 的 ID 以及对应 MasterID 的 Offset 值
    • 当 Master 和 Slave 之间的连接断开后,Slave 在重新连接后会努力尝试网络断开期间丢失的命令流psync masterID offset
    • 如果无法同步(Offset 与最新命令 ID 差异超过了指定阀值或 Slave 存储的 MasterID 与实际不一致), Slave 会请求全量同步,全量同步会同步快照(bgsave),然后再同步快照过程中的新命令流
  • Redis 主从同步方式默认为异步,也可以配置为同步,但是会降低整体性能,反过来降低了崩溃丢消息的概率

  • Redis 2.8.18 是第一个支持无磁盘复制的版本,在 Slave 需要全量同步时,Master 直接把内存中的 RDB 发送给 Slave,而不需要保存到硬盘,提高了性能。

哨兵模式

主从模式发生故障后,如果主自动启动了,可以自动恢复。否则需要手动选择一个从切换为主。 但是这个过程需要自动化,所以就有了哨兵模式,Redis2.8 提供了哨兵 2.0。

哨兵会通过心跳检查,判断 Master 是否客观下线,然后就由领头哨兵执行 Failover 操作,将其中一个从变为主,在 Failover 过程中系统阻塞对外服务。

  • 哨兵的作用是保持主从复制集群的高可用,但是不保证数据不丢失,因为数据丢失是 Redis 主从模式固有的问题

  • 哨兵判断客观下线过程:

    1. 其中一个哨兵发现 Ping Master 后没有响应,则当前哨兵认为 Master 主观下线
    2. 哨兵发送SENTINEL is-master-down-by-addr命令给其他哨兵判断是否 Master 主观下线
    3. 收到超过 quorum 的哨兵数量主观下线,则认为 Master 已客观下线,开始执行 Failover
  • 选取领头哨兵过程:Raft 算法

    1. 发现 Master 客观下线的哨兵(A)向其他哨兵发送命令,要求选举自己成为领头哨兵
    2. 如果收到选举命令的哨兵没有选举过别的结点,则会同意选 A
    3. 如果 A 发现超过 quorum 的哨兵选择了自己,则认为自己是领头哨兵
    4. 当有多个哨兵同时参选,则会出现没有任何节点当选的可能,此时每个参选节点将等待一个随即时间重新发起竞选,直到选举成功
  • Failover(故障恢复)过程

    1. 从所有在线的从库中,选取设置的优先级最高的,优先级可以通过slave-priority来设置
    2. 如果优先级相同,则按照复制命令的偏移量 Offset 越大越优先
    3. 如果偏移量相同,则按照 RunID(每个库唯一,启动时自动生成),越小越优先
    4. 选择好节点后,领头哨兵将向这个节点发送slaveof no one,升级他为主库
    5. 然后向其他从数据库发送slaveof命令切换主库
    6. 最后更新各个结点(客户端、Redis 结点)的记录。将已经停止服务的旧的主数据库更新为新的主数据库的从数据库,当其恢复后自动以从数据库的身份加入到主从架构中
  • 哨兵本身也同时部署多个,可以相互监控。推荐的哨兵部署方案:

    1. 为每个节点(无论是主数据库还是从数据库)都部署一个哨兵
    2. 使每个哨兵与其对应的节点的网络环境相同或相近
  • 一般设置 quorum 的值为 N/2+1

配置方式

  1. 从配置文件slaveof 192.168.1.1 6379,就实现了基本的主从
  2. 从设置slave-read-only可以开启从的只读模式
  3. 如果主设置了密码,从需设置masterauth <password>

cluster 模式

特性

  • 通过哈希的方式将数据分片,每个结点分配 16384 个 slot(槽)的一部分
  • 每份分片会存储在多个互为主从的多个节点上
  • 数据写入先写主结点,再同步到从结点(默认为异步同步,可以配置为阻塞同步)
  • 同一分片多个结点间的数据不保持强一致性,如果修改为阻塞同步到从结点同时保证每条 AOF 写入则可以保证强一致性,但是性能会非常低
  • 客户端可以和任意结点连接,读取数据时,如果 key 没有分配在该结点上,redis 会返回转向指令,指向正确的结点
  • 扩容时需要把旧结点的数据迁移一部分到新结点

  • 在 redis cluster 模式下,每个 redis 实例要开放两个端口号,比如一个是 6379,另一个就是加 1W 的端口号,比如 16379 这个端口号用来做结点间通信的,也就是 cluster bus 的通信,用于故障检测、配置更新、故障转移授权。cluster bus 采用二进制 gossip 协议,占用更少的带宽。

  • 优点
    • 无中心架构,支持动态扩容,对业务侧透明
    • 具备 Sentinel 监控和自动 Failover 能力
    • 客户端无需连接所有结点,连接集群中任意可用结点即可
    • 高性能,客户端直连 redis 服务,免去了 proxy 代理的损耗
  • 缺点
    • 运维复杂,迁移槽位需要人工干预
    • 只能使用 0 号数据库
    • 不支持批量(pipline 管道)操作
    • 分布式逻辑和存储耦合(无法使用 key 聚合操作)
      • 比如 transaction 涉及多个结点上的 key,那么命令入队时会返回错误
      • 比如多个 set 取交集,同样会返回错误
  • cluster 前面可以增加一个代理,比如 codis,支持 pipline 等功能

Redis Sharding

在 Redis Cluster 出现之前,业界普遍采用多 Redis 实例集群方法。主要思想是通过客户端 Hash 将 key 做一次散列分配到特定 Redis 实例。 Java Redis 客户端驱动 Jedis 支持 Redis Sharding 功能。

  • 优点
    • 实现简单,服务端实例彼此独立,非常容易扩展,系统灵活
  • 缺点
    • 由于 Sharding 处理在客户端,规模较大时有较大挑战
    • 客户端不支持动态增删结点,服务端 Redis 拓扑结构变化时,每个客户端都需要更新调整
    • 连接不能共享,当应用规模增大时,连接成本高(连接数量、建立连接时间),资源浪费不易优化
  • 客户端和 Redis 集群之间可以加一个 proxy 代理,可以减少连接数,但是会出现代理瓶颈及代理延迟损耗
    • 如果单台代理压力过大,可以在前面放一个 LVS 负载均衡,LVS 本身做主备配置

集群模式下丢消息的情况

Redis 集群出于效率的考虑,并不保证 100%不丢失数据。 在分布式系统中,衡量一个系统的可用性,我们一般情况下会说 4 个 9,5 个 9 的系统达到了高可用(99.99%,99.999%,据说淘宝是 5 个 9)。 对于 redis 集群,我们不可能保证数据完全不丢失,只能做到使得尽量少的数据丢失。

主从复制——异步复制导致丢消息

主从复制模式下,主节点和从结点之间设定为异步复制,也就是主节点写入后立即给 Client 响应 OK,然后再复制到从结点。 在给从结点复制前,如果主节点宕机,则数据丢失。

  • 如果 master 开启 AOF 是否可以防止丢失? 不可以。 一般主从模式下会设置哨兵,假设 master 的 AOF 写入了然后宕机,哨兵切换从为主, 当原来的主重启后会自动转换角色为从,这样 AOF 中的最后一条数据还是丢失了。

集群模式——脑裂导致丢消息

集群模式下,每个 master 负责自己的 slot。假设一个 master 正为一个连接的 client 提供服务,这时网络发生脑裂,这个 master 被隔离开。 哨兵会意识到 master 失效了,将 slave 转为主,集群继续运行。而这时原有的主在意识到自己已被断开前可能会一直为 client 提供服务,造成数据丢失。

解决方案

可以通过下面两个参数调整消息丢失率:

1
2
min-slaves-to-write 1 // 至少要写入一个从才返回 OK
min-slaves-max-lag 10 // 至少一个 slave 的延迟不能超过 10s,否则停止服务

对于 client,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入 master 来保证数据不丢失; 也可以将数据写入 kafka 消息队列,隔一段时间去消费 kafka 中的数据。

通过上面两个参数的设置我们尽可能的减少数据的丢失,具体的值还需要在特定的环境下进行测试设置。