之前简单分享了一下单机版Redis。包括:
单机版 Redis 响应快、支持的并发量高,如果有持久化的内存,甚至还能做到数据不丢失。那我们可以直接使用单机版 Redis 作为数据库吗?
假设我们使用单机版 Redis 作为存储介质。Redis 一般分为两种用途,缓存和持久化数据库。
我们先看最常见的一种情况,断电/重启:
在断电情况下,单机 Redis 作为业务数据库肯定是不满足条件的;作为缓存,如果数据量小、请求量少的话,并不是很影响业务使用(这种场景也没有用缓存的必要)。
那如果不考虑重启的情况,单机 Redis 就没问题了吗?
数据量、访问量小的情况下,貌似确实不会有什么问题,我们还是考虑数据量、访问量大的情况。
比如现在有一个单机 redis, 需要存储几亿个key。当 Redis 持久化时,如果数据量增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞。即使在内存、CPU资源充足的情况下,一次数据备份就要消耗很长时间,导致 Redis 的服务不可用。
不过,如果你不要求持久化保存 Redis 数据,那么,增加cpu的硬件性能和内存容量会是一个不错的选择。但是这样又有第二个问题,就是硬件成本问题:一块128G的内存条的价格远大于四块32G 的内存条的价格之和,cpu也是同理。硬件的成本会随着性能的不断提高呈指数上升
综上所述,单机版 Redis 主要存在以下两个问题:
那怎么办呢?单机不够用,就多加几台机器嘛!下面我们来看一下 Redis 提供的在多机环境下几种常见的资源使用策略,看看能否解决上述的两个问题
主从同步就是,多个 Redis 节点,一个主节点(master),多个副节点(slave)。读请求会发送到各个节点,写请求只会发给 master,然后 slave 定时会从 master 拉取新增的写操作以保证数据的一致性。
那如何从单机版加一个 slave 节点过渡到主从模式呢? 例如,现在有实例 1(ip:192.168.19.3)和实例 2(ip:192.168.19.5),我们在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据:
replicaof 192.168.19.3 6379
第一次请求到 master 的 RDB 文件全量同步,在 RDB 文件内容之后的操作都是增量同步进行。
之后就是重复1和3的过程。
上面讲的主从同步只是保证了 Redis 的数据有及时备份。可以分担一部分主库的访问压力,以及保证了从库的可用性。但是如果主库发生故障了,那就直接会影响到从库的同步,因为从库没有相应的主库可以进行数据复制操作了。
哨兵(Sentinel)就是为了解决这个问题而产生的。它实现了主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的三个问题。
下面我们就从这三个问题来看 Sentinel 对应的三种职能,监控、选主和通知。
哨兵需要判断主库是否处于下线状态。这里要先说两个概念,即主观下线和客观下线,
哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库响应超时了,那么,哨兵就会先把它标记为“主观下线”。如果检测的是从库,那么,哨兵简单地把它标记为主观下线就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。
如果检测的是主库,那么,哨兵还不能简单地把它标记为“主观下线”。因为很有可能存在这么一个情况:那就是哨兵误判了,其实主库并没有故障。可是,一旦启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。
那怎么减少误判呢?在日常生活中,当我们要对一些重要的事情做判断的时候,经常会和家人或朋友一起商量一下,然后再做决定。哨兵机制也是类似的,它通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。在判断主库是否下线时,不能由一个哨兵说了算,只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”,这个叫法也是表明主库下线成为一个客观事实了。这个判断原则就是:少数服从多数。
“客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。
当哨兵们判断主库客观下线的时候,就要开始下一个决策过程了,即从许多从库中,选出一个从库来做新主库。
一般来说,我把哨兵选择新主库的过程称为“筛选 + 打分”。
筛选条件: 网络连接状态(现在 + 之前)。在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态。如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了。
打分:
哨兵也是集群模式,也要保证服务的可用性,所以也会有相应的保证机制。这里不在本篇的 Redis 集群讨论范围之内,所以就不展开描述了。
看到这里,还记得我们一开始发现的 Redis 单机版的两个问题吗?可用性和数据臃肿。
上面主从模式 + 哨兵模式足以保证 Redis 服务的可用性,但是看起来并没有解决数据臃肿的问题, Redis Cluster 就为我们提供了解决这个问题的方案。
切片集群,也叫分片集群,就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。
这样单份数据可以缩小到备份不影响该实例的主进程,而且还可以解决硬件成本问题。
那数据都分散了,用户需要查某一条数据,Redis 怎么知道这条数据在哪个实例呢?
我们要先弄明白切片集群和 Redis Cluster 的联系与区别。 切片集群是一种保存大量数据的通用机制,这个机制可以有不同的实现方案。 Redis Cluster 方案采用 hash 槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个 hash 槽,这些 hash 槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个 hash 槽中。具体的映射过程分为两大步:
关于 CRC16 算法,不是我们讨论的重点。我们只需要每一个 key 都能算出来一个 0~16383 的数就好。
每一个 0~16383 的数被称为一个slot(槽)。一个 Redis 实例有多个槽,集群中的所有 Redis 实例的槽加起来一定等于 16384。换句话说,在手动分配 hash 槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
在定位键值对数据时,它所处的 hash 槽是可以通过计算得到的,这个计算可以在客户端发送请求时来执行。但是,要进一步定位到实例,还需要知道 hash 槽分布在哪个实例上。
Redis 实例会把自己的 hash 槽信息发给和它相连接的其它实例,来完成 hash 槽分配信息的扩散,Redis集群采用P2P的Gossip(流言)协议, Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播 当实例之间相互连接后,每个实例就有所有 hash 槽的映射关系了。相当于每一个实例都有一份 slot 与实例地址的映射关系表。
当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的 hash 槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。
GET hello:key(error) MOVED 13320 192.168.19.5:6379
如上图,用户连接上任意一个 Redis 实例,都可以查询集群中所有的key。比如连上实例2,查询key = “hello”。实例2会根据key计算 hash 槽的值。
客户端也会缓存请求过的key对应实例的地址。
如果实例槽有变动,比如手动增删了几个实例,剩下的实例还是可以自动同步新的 hash 槽对应的实例位置。
Redis Cluster 模式解决了数据臃肿的问题,但是单独的 Redis Cluster 模式并没有可用性,如果任意一个实例断电或者断网,这个实例所有 hash 槽的数据就会处于不可用的状态。
所以在实际使用场景下,上述的三种集群模式都会结合起来。
到这里我们就解决了开篇提出的两个问题。
开篇我们提出的单机版 Redis 作为数据库的两个弊端。然后介绍了 Redis 官方的集群方案分别是如何解决这两个弊端的。
其实除了 Redis 官方提供的集群方案,也有很多优秀的开源 Redis 集群方法。 比如:
推特的 twemproxy
国内豌豆荚出品的 Codis
这两种开源方案和 Redis 官方的集群方案最大的区别就是数据节点的管理方式。
那去中心化和中心化我们又该如何选择呢?