什么是Redis-Cluster集群?
作者:程序员马丁
在线博客:https://open8gu.com
大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。
回答话术
分片集群是 Redis 在 3.0 以后提供的一个集群部署方案,它主要解决的是集群的横向扩展问题。
Redis 集群是一个去中心化的结构,它没有类似注册中心这样的全局管理者,因此集群中的每个节点都会通过 Gossip 协议与其他节点保持通信,以监控彼此的健康状态,并交换包括哈希槽的分配情况、疑似下线的节点情报在内的各项数据,并最终在每个节点都全量的保存集群的各项元数据。
Redis 集群中的每个主节点都可以拥有多个从节点,主节点将会通过主从复制向从节点同步数据,不过与传统主从模式不同的是,Redis 集群中的从节点一般不对外提供读服务,它们仅用于作为备份。当主节点下线后,集群中的其他节点将会通过投票选举选出新的主节点,并且自动的完成主从切换,实现故障转移。
在 Redis 集群中,共划分了 16384 (即 2 ^ 14)个哈希槽,集群中的每个节点都拥有其中的一部分槽位,当客户端发起请求时,可以直接请求集群中的任意节点,节点将会通过哈希函数确认 Key 落在哪一个槽位上,进而确认客户端的请求最终需要路由到集群中的哪一个节点。此时,如果当前节点即为目标节点,将直接执行命令,否则将会返回 MOVED 响应告知客户端应当改为请求哪一个节点。(常用的客户端基本都具备自动完成重定向功能,并且为了性能还都会在本地缓存槽位和节点的映射关系,直接在本地计算出最终要路由的节点,从而避免每次都要访问到错误的节点后再重定向)
基于这样的机制,当 Redis 集群需要进行扩容或缩容时,就可以通过重新分配哈希槽灵活变更集群中的节点数量,甚至可以通过为具备更强性能的节点分配更多的槽位,手动实现“数据倾斜”。
问题详解
1. 分片集群
当资源不够的时候,我们就需要对服务进行扩容,扩容可以分为横向和纵向,纵向很好理解,就是直接给机器加内存加 CPU。不过,单台服务器的硬件扩容始终是有上限的,因此我们会需要考虑横向扩容,比如:
- 为了存储超出单台机器内存上限的数据,我们需要将数据拆分到不同的机器进行存储。
- 面对越来越大的流量和计算规模,我们则需要允许同时让更多的 Redis 实例来一并提供服务以缓解压力。
简单的来说,就是同时上更多的机器从而分担单台机器的压力,Redis 集群(Cluster)就是类似这样的东西,某种程度上来说,它的概念比较接近传统概念的多主多从。
和 MySQL 等传统数据库的分库分表一样,Redis 集群同样基于分片算法实现。
在 Redis 集群中,共划分了 16384 (即 2 ^ 14)个哈希槽,集群中的每个节点都拥有其中的一部分槽位,当客户端发起请求时,可以直接请求集群中的任意节点,节点将会通过哈希函数确认 Key 落在哪一个槽位上,进而确认客户端的请求最终需要路由到集群中的哪一个节点。
其中,每个 Redis 实例都可以拥有多个从节点(不过从节点一般只作为备份,并不提供服务),并且不同的 Redis 实例之间会通过 Gossip 协议保持通信,它们将会分享彼此的哈希槽分配情况,并在主节点宕机时连接到新的备用节点。
虽然说从节点一般只用来做备份,但是你也可以通过将其设置为
readonly
后让它可以对外提供读服务。不过这个做法不太常见:一方面原因是主从复制有延迟,从节点的数据与主节点可能不一致;另一方面是既然都已经集群部署了,直接多加几台机器把切片分的更细点,要比搞读写分离更方便,而且性能提升的也更明显。
2. 集群搭建
当我们在配置文件中配置相关参数,并且指定 Redis 以集群模式启动后,实际上各个节点还仍然处于独立状态。
我们需要执行 CLUSTER MEET <IP> <PORT>
命令让节点之间互相发现。比如,在下图中,我们让 A 节点分别通过 CLUSTER MEET 192.168.0.2 6379
与 CLUSTER MEET 192.168.0.3 6379
命令将 B 和 C 节点拉进集群。
此时,集群实际上已经搭建完成,但是在最开始的时候, B 和 C 并不知道彼此的存在,只有 A 掌握集群的全貌,随后,A 节点将会根据 Gossip 协议与 B 和 C 进行通信,并且一并把自己维护的集群状态元数据告知 B 和 C,此后,B 和 C 将会意识到彼此的存在,并且建立关系。
这里提到的“建立关系”,本质上是指 Redis 实例在本地为新节点建立 ClusterNode
对象,并且添加到 clusterState
字典的过程。有了这个字典,节点才可以在后续对其他节点进行健康检查和数据交换。
3. 节点间的通信
由于 Redis 的集群是去中心化的,这意味着集群中实际上没有一个类似注册中心一样的角色,所以每个节点都需要通过 Gossip 协议与其他节点保持通信,这个通信端口通常是默认的服务端口加 10000,比如默认的服务端口是 6379,那么集群通信端口就是 16379。
3.1. Gossip 协议
尽管我们常说节点之间会“保持通信”,但实际上,Redis 集群并不要求每个节点都必须一直与其他所有节点同时保持连接,这要归功于 Redis 使用的 Gossip 协议,Gossip 可以译为流言或者八卦,这很好的反映了这个协议的特点。
举个例子,假如现在有一个新节点 D 要加入:
- C 邀请 D 加入集群,现在 C 知道了 D 的存在;
- B 与 C 进行 ping & pong,通过 C 得知 D 加入了集群,现在 B 知道了 D 的存在;
- A 与 B 进行 ping & pong,通过 B 得知 D 加入了集群,现在 A 也知道了 D 的存在。
可见,当有一个新的事件发生后,它会逐步的在集群中传播,即使中间有节点挂掉,只要消息的传播路径没有完全被切断,那么其他节点也会最终会被通知到,比如新节点的加入,或者节点下线……等。
当然,实际上 Redis 也不是完全依赖这种方式传播消息,对于一些时效性要求比较强的消息 —— 比如节点下线或者主从切换 —— 则会直接通过广播的方式进行通知。
3.2. 节点间的数据交换
组群后,每个 Redis 实例都会在本地维护一个集群实例列表,然后定期从中挑选节点发送 ping 消息,而另一个节点收到了之后会回以 pong 响应。两个节点通过这个步骤来确认彼此健康状态,并传递其其他节点的下线状态,以及交换彼此持有的槽位信息等数据。
需要注意的是,ping 的目标并不是完全随机的,它遵循两个规则:
- 默认情况下,Redis 实例每隔一秒都会从已知的集群节点中挑选出 5 个实例,然后再从中挑选出一个最久没有 ping 过的节点发送消息。
- 每隔一段时间,Redis 将会检查其他节点对当前节点请求的响应情况,如果发现有节点最近一次响应距今已接近超时时间
cluster-node-timeout
,那么它会立刻向该节点发起 ping 请求,若再无响应则会标记为主观下线。
3.3. 为什么集群规模不是越大越好?
基于上述情况,我们不难意识到,Redis 的集群规模并不是越大越好。
由于 Redis 集群是去中心化集群,因此节点总是全量存储整个集群的信息,并且集群中的节点数量越多,需要元数据占用的存储空间就越多,并且节点间通信带来的性能开销也就越大。此外,去中心化的结构也导致我们很难清晰的掌握整个集群的状态,节点状态变化的延迟性也会带来极大的管理成本。