基本的 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),然后再同步快照过程中的新命令流
- 连接正常时,Master 会发送一系列命令流来更新 Slave,包括:客户端写入、key 的过期或被(容量不足)逐出
-
Redis 主从同步方式默认为异步,也可以配置为同步,但是会降低整体性能,反过来降低了崩溃丢消息的概率
-
Redis 2.8.18 是第一个支持无磁盘复制的版本,在 Slave 需要全量同步时,Master 直接把内存中的 RDB 发送给 Slave,而不需要保存到硬盘,提高了性能。
哨兵模式
主从模式发生故障后,如果主自动启动了,可以自动恢复。否则需要手动选择一个从切换为主。 但是这个过程需要自动化,所以就有了哨兵模式,Redis2.8 提供了哨兵 2.0。
哨兵会通过心跳检查,判断 Master 是否客观下线,然后就由领头哨兵执行 Failover 操作,将其中一个从变为主,在 Failover 过程中系统阻塞对外服务。
-
哨兵的作用是保持主从复制集群的高可用,但是不保证数据不丢失,因为数据丢失是 Redis 主从模式固有的问题
-
哨兵判断客观下线过程:
- 其中一个哨兵发现 Ping Master 后没有响应,则当前哨兵认为 Master 主观下线
- 哨兵发送
SENTINEL is-master-down-by-addr
命令给其他哨兵判断是否 Master 主观下线 - 收到超过 quorum 的哨兵数量主观下线,则认为 Master 已客观下线,开始执行 Failover
-
选取领头哨兵过程:Raft 算法
- 发现 Master 客观下线的哨兵(A)向其他哨兵发送命令,要求选举自己成为领头哨兵
- 如果收到选举命令的哨兵没有选举过别的结点,则会同意选 A
- 如果 A 发现超过 quorum 的哨兵选择了自己,则认为自己是领头哨兵
- 当有多个哨兵同时参选,则会出现没有任何节点当选的可能,此时每个参选节点将等待一个随即时间重新发起竞选,直到选举成功
-
Failover(故障恢复)过程
- 从所有在线的从库中,选取设置的优先级最高的,优先级可以通过
slave-priority
来设置 - 如果优先级相同,则按照复制命令的偏移量 Offset 越大越优先
- 如果偏移量相同,则按照 RunID(每个库唯一,启动时自动生成),越小越优先
- 选择好节点后,领头哨兵将向这个节点发送
slaveof no one
,升级他为主库 - 然后向其他从数据库发送
slaveof
命令切换主库 - 最后更新各个结点(客户端、Redis 结点)的记录。将已经停止服务的旧的主数据库更新为新的主数据库的从数据库,当其恢复后自动以从数据库的身份加入到主从架构中
- 从所有在线的从库中,选取设置的优先级最高的,优先级可以通过
-
哨兵本身也同时部署多个,可以相互监控。推荐的哨兵部署方案:
- 为每个节点(无论是主数据库还是从数据库)都部署一个哨兵
- 使每个哨兵与其对应的节点的网络环境相同或相近
-
一般设置 quorum 的值为 N/2+1
配置方式
- 从配置文件
slaveof 192.168.1.1 6379
,就实现了基本的主从 - 从设置
slave-read-only
可以开启从的只读模式 - 如果主设置了密码,从需设置
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 |
|
对于 client,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入 master 来保证数据不丢失; 也可以将数据写入 kafka 消息队列,隔一段时间去消费 kafka 中的数据。
通过上面两个参数的设置我们尽可能的减少数据的丢失,具体的值还需要在特定的环境下进行测试设置。