浅谈Redis(二)——集群版

语言: CN / TW / HK

前言

之前简单分享了一下单机版Redis。包括: - 为什么用 Redis 以及为什么不用 Redis - Redis 为什么这么快 - Redis 的数据结构 - Redis 单线程吞吐量高的原因

单机版 Redis 响应快、支持的并发量高,如果有持久化的内存,甚至还能做到数据不丢失。那我们可以直接使用单机版 Redis 作为数据库吗

假设我们使用单机版 Redis 作为存储介质。Redis 一般分为两种用途,缓存和持久化数据库。

我们先看最常见的一种情况,断电/重启: - 业务数据库: 使用持久化内存可以保证重启后数据不丢失。但是断电的过程中整个业务没有数据源就会瘫痪掉。 - 缓存: 如果缓存断电瘫痪,倒不至于会对业务造成直接瘫痪,毕竟业务数据库中有完整的数据。如果 Redis 宕机,就会导致缓存雪崩——所有缓存都失效,所有的请求都是直接打到数据库,导致数据库压力激增,导致服务响应时间增加。

在断电情况下,单机 Redis 作为业务数据库肯定是不满足条件的;作为缓存,如果数据量小、请求量少的话,并不是很影响业务使用(这种场景也没有用缓存的必要)。

那如果不考虑重启的情况,单机 Redis 就没问题了吗

数据量、访问量小的情况下,貌似确实不会有什么问题,我们还是考虑数据量、访问量大的情况。

比如现在有一个单机 redis, 需要存储几亿个key。当 Redis 持久化时,如果数据量增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞。即使在内存、CPU资源充足的情况下,一次数据备份就要消耗很长时间,导致 Redis 的服务不可用

不过,如果你不要求持久化保存 Redis 数据,那么,增加cpu的硬件性能和内存容量会是一个不错的选择。但是这样又有第二个问题,就是硬件成本问题:一块128G的内存条的价格远大于四块32G 的内存条的价格之和,cpu也是同理。硬件的成本会随着性能的不断提高呈指数上升

综上所述,单机版 Redis 主要存在以下两个问题: 1. 可用性(断电后,业务不可用) 2. 数据臃肿(单机CPU对大量数据的操作) - 备份阻塞导致服务不可用 - 成本指数增长

那怎么办呢?单机不够用,就多加几台机器嘛!下面我们来看一下 Redis 提供的在多机环境下几种常见的资源使用策略,看看能否解决上述的两个问题

一、主从同步(Master-Slave)

主从同步就是,多个 Redis 节点,一个主节点(master),多个副节点(slave)。读请求会发送到各个节点,写请求只会发给 master,然后 slave 定时会从 master 拉取新增的写操作以保证数据的一致性。

master-slave.jpg

那如何从单机版加一个 slave 节点过渡到主从模式呢? 例如,现在有实例 1(ip:192.168.19.3)和实例 2(ip:192.168.19.5),我们在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据: shell replicaof 192.168.19.3 6379 第一次请求到 master 的 RDB 文件全量同步,在 RDB 文件内容之后的操作都是增量同步进行。

master-slave-sync.jpg

之后就是重复1和3的过程。

二、哨兵机制(Sentinel)

上面讲的主从同步只是保证了 Redis 的数据有及时备份。可以分担一部分主库的访问压力,以及保证了从库的可用性。但是如果主库发生故障了,那就直接会影响到从库的同步,因为从库没有相应的主库可以进行数据复制操作了。

哨兵(Sentinel)就是为了解决这个问题而产生的。它实现了主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的三个问题。 - 主库真的挂了吗? - 该选择哪个从库作为主库? - 怎么把新主库的相关信息通知给从库和客户端呢?

下面我们就从这三个问题来看 Sentinel 对应的三种职能,监控、选主和通知。

2.1 监控

哨兵需要判断主库是否处于下线状态。这里要先说两个概念,即主观下线客观下线

哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库响应超时了,那么,哨兵就会先把它标记为“主观下线”。如果检测的是从库,那么,哨兵简单地把它标记为主观下线就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。

如果检测的是主库,那么,哨兵还不能简单地把它标记为“主观下线”。因为很有可能存在这么一个情况:那就是哨兵误判了,其实主库并没有故障。可是,一旦启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。

那怎么减少误判呢?在日常生活中,当我们要对一些重要的事情做判断的时候,经常会和家人或朋友一起商量一下,然后再做决定。哨兵机制也是类似的,它通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。在判断主库是否下线时,不能由一个哨兵说了算,只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”,这个叫法也是表明主库下线成为一个客观事实了。这个判断原则就是:少数服从多数。

“客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。

客观下线.jpg

2.2 选主

当哨兵们判断主库客观下线的时候,就要开始下一个决策过程了,即从许多从库中,选出一个从库来做新主库。

一般来说,我把哨兵选择新主库的过程称为“筛选 + 打分”。

选主.png

筛选条件: 网络连接状态(现在 + 之前)。在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态。如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了。

打分: - 首先,优先级最高的从库得分高。用户可以手动设置从库的优先级。 - 其次,和旧主库同步程度最接近的从库得分高。 - 最后,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。

哨兵也是集群模式,也要保证服务的可用性,所以也会有相应的保证机制。这里不在本篇的 Redis 集群讨论范围之内,所以就不展开描述了。

三、 Redis Cluster

看到这里,还记得我们一开始发现的 Redis 单机版的两个问题吗?可用性数据臃肿

上面主从模式 + 哨兵模式足以保证 Redis 服务的可用性,但是看起来并没有解决数据臃肿的问题, Redis Cluster 就为我们提供了解决这个问题的方案。

3.1 数据切片

切片集群,也叫分片集群,就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。

这样单份数据可以缩小到备份不影响该实例的主进程,而且还可以解决硬件成本问题。

那数据都分散了,用户需要查某一条数据,Redis 怎么知道这条数据在哪个实例呢

我们要先弄明白切片集群和 Redis Cluster 的联系与区别。 切片集群是一种保存大量数据的通用机制,这个机制可以有不同的实现方案。 Redis Cluster 方案采用 hash 槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个 hash 槽,这些 hash 槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个 hash 槽中。具体的映射过程分为两大步: - 首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值; - 然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的 hash 槽。

关于 CRC16 算法,不是我们讨论的重点。我们只需要每一个 key 都能算出来一个 0~16383 的数就好。

每一个 0~16383 的数被称为一个slot(槽)。一个 Redis 实例有多个槽,集群中的所有 Redis 实例的槽加起来一定等于 16384。换句话说,在手动分配 hash 槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。

slots.jpg

3.2 客户端如何定位数据?

在定位键值对数据时,它所处的 hash 槽是可以通过计算得到的,这个计算可以在客户端发送请求时来执行。但是,要进一步定位到实例,还需要知道 hash 槽分布在哪个实例上。

Redis 实例会把自己的 hash 槽信息发给和它相连接的其它实例,来完成 hash 槽分配信息的扩散,Redis集群采用P2P的Gossip(流言)协议, Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播 当实例之间相互连接后,每个实例就有所有 hash 槽的映射关系了。相当于每一个实例都有一份 slot 与实例地址的映射关系表

当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的 hash 槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。

shell GET hello:key (error) MOVED 13320 192.168.19.5:6379

redis_cluster_instances.jpg

如上图,用户连接上任意一个 Redis 实例,都可以查询集群中所有的key。比如连上实例2,查询key = “hello”。实例2会根据key计算 hash 槽的值。 - 如果槽就在实例2上,则直接查询结果返回; - 如果不在实例2上,则返回存在key的实例地址。

客户端也会缓存请求过的key对应实例的地址。

如果实例槽有变动,比如手动增删了几个实例,剩下的实例还是可以自动同步新的 hash 槽对应的实例位置。

3.3 实际应用

Redis Cluster 模式解决了数据臃肿的问题,但是单独的 Redis Cluster 模式并没有可用性,如果任意一个实例断电或者断网,这个实例所有 hash 槽的数据就会处于不可用的状态。

所以在实际使用场景下,上述的三种集群模式都会结合起来。

cluster_node.jpg

cluster_nodes.jpg

到这里我们就解决了开篇提出的两个问题。 - 保证可用性: cluster node 中的主从同步和哨兵选举机制 - 避免数据臃肿:cluster node 集群的数据切片

四、小结

开篇我们提出的单机版 Redis 作为数据库的两个弊端。然后介绍了 Redis 官方的集群方案分别是如何解决这两个弊端的。

其实除了 Redis 官方提供的集群方案,也有很多优秀的开源 Redis 集群方法。 比如:

推特的 twemproxy

国内豌豆荚出品的 Codis

这两种开源方案和 Redis 官方的集群方案最大的区别就是数据节点的管理方式。 - Redis 是去中心化,hash 槽和节点的映射每个节点都会有保存,通过 Gossip 协议传播; - 这两种方案是中心化。有单独的代理节点管理hash 槽和节点的映射。

那去中心化和中心化我们又该如何选择呢?