关于Redis Cluster,我所知道的事(一)


因9月30号要在组内做分享,这些天我一直在看Redis Cluster。老实说东西有点多,我一下子也看不完,因此打算先把现阶段看过的东西梳理一下。文章的结构如下:
  • Redis单机环境的安装与基本数据结构介绍

  • Redis Cluster介绍

  • Redis Cluster环境搭建

  • 补遗

参考资料主要为Redis的两篇官方文档以及网上搜到的一些零碎的资料。因为是第一次接触这块内容,不敢保证所写内容都是正确的,如本文有误,烦请批评指正。


## Redis单机环境的安装与基本数据结构介绍

Redis的安装是非常简单的,首先是下载官方的压缩包,解压,cd到对应的目录,然后执行make命令即可。然后我们需要启动redis-server,命令是:src/redis-server
redis有个内置的client,通过它我们可以通过命令行的方式与redis-server进行交互,命令是:src/redis-cli

Redis的数据结构主要有以下几种:Strings,Lists,Sets,Hashes,Sorted sets和Bitmaps and HyperLogLogs。关于每种数据结构的介绍以及基本的使用方法可以参考这里:An introduction to Redis data types and abstractions 因为这个比较简单,这里就不再赘述了。


## Redis Cluster介绍

Redis Cluster,又叫Redis集群,是一种可以令数据在多个Redis节点之间进行传输的设施,通常用作构建分布式缓存。Redis集群的优势主要集中在以下两个方面:

  1. 自动分割数据到不同的节点
  2. 部分节点宕机或不可达的情况下,能够继续处理命令

需要注意的是,Redis集群并不支持处理多命令,当然这并不是绝对的,后面会对此进行进一步地介绍。

每个节点都需要保持两个TCP端口处于打开状态,其中一个用于与客户端进行交互,另一个用于集群内部节点之间传输信息。这两个连接的端口号的差值默认为10000,例如第一个端口号为6379,则另外一个为16379。第二个端口用于集群链接(cluster bus),这是一种使用一个二进制协议来进行点对点(node-to-node)交互的通道。集群链接用户宕机检测,配置更新,失效备援(failover)认证等等。客户端永远都不需要试图通过集群链接端口进行交互,而应当通过普通的Redis命令端口。

哈希槽(hash slots)

哈希槽是redis集群中的一个重要的概念,它其实就是代表这一个keys的集合。redis集群共有16384个哈希槽,每个key通过CRC16校验然后对16384进行取模来决定该key应当被放到哪个槽中。集群中的每个节点负责一部分哈希槽,举例,比如现在集群中存在三个节点,则:

  1. 节点 A 包含 0 到 5500号哈希槽
  2. 节点 B 包含5501 到 11000 号哈希槽
  3. 节点 C 包含11001 到 16384号哈希槽

这种设计使得添加或者删除集群中的节点变得比较简单,比如说我想添加一个节点D,我只需要将A,B,C中的一部分槽移动到D即可;同样地,如果我想移除节点A,则只需要将A所负责的那一部分槽移动到B和C节点,然后将没有任何槽的A节点从集群中删除即可。

需要说明的是,移动哈希槽并不需要停止服务,因此无论是添加、删除还是改变节点所包含的槽占总槽数的百分比,都是不需要停机的。

哈希标签(hash tags)

redis集群并不是在任何情况下都不支持执行多命令的。事实上如果我们希望做到这点,则首先需要引入一个哈希标签(hash tags)的概念,这个东西简单来说就是用大括号{}包裹起来的一个key的子串,比如说this{foo}key和another{foo}key,其中的{foo}就是一个哈希标签,因为这两个key拥有相同的哈希标签{foo},因此可以保证他们会被放到相同的哈希槽中,从而使得它们可以被用于多命令执行中。

这里插一句,之所以this{foo}key和another{foo}key会被放到同一个哈希槽中,是因为只有其中的{foo}会被哈希,因此它们的哈希值是相同的。另外需要说明的是,哈希标签中的大括号是不支持嵌套的,也就是说,redis集群只会将一个key中的第一次出现的{和第一次出现的}之间的内容进行哈希,而不会去管key中存在多少个大括号。另外,如果大括号之间没有字符,则对整个key进行哈希,这种将{}放到key中的做法通常用来保证整个key会被哈希。

主从模型(master-slave model)

这个非常好理解,我们假设我们的集群中存在三个master和三个slave,且master和slave一一对应,设为A,B,C和A1,B1,C1,则当B宕机时,集群便会推举B1成为新的master,使得整个集群得以继续正常运转。当然,当B和B1都处于宕机状态时,整个集群便可不用了。

当然,每个master可以拥有多个slave,slave越多,集群的健壮性也就越大。另外,redis集群还拥有复制迁移(replicas migration)的特性,这个特性简单来说就是说,假设集群当前的slave节点数要稍稍多于master数,则当某个只拥有一个slave的master的slave宕机时,该特性可以保证一个slave会被迁移到这个master上,从而尽可能地保证了整个集群的可用性。

一致性保证(consistency guarantees)

redis集群并不能保证数据的强一致性(strong consistency),因此在一些特定的情况下,集群可能会丢失写操作。具体来说,由于集群采用的是异步备份(asynchronous replication),因此在一个写操作中,实际上发生了一下几个步骤:

  1. 客户端向节点B写数据
  2. B节点返回OK给客户端
  3. B节点将写操作的结果传播(propagates)给它的suoyouslave节点

我们可以看到B节点并没有在返回结果给客户端之前,等待它的slave节点对该操作结果进行确认。因此,假设B节点确认了客户端发过来的写操作,但是却在发给它的slave之前宕机了,它的其中一个slave将会被推举为一个新的master,使得这一写操作永远丢失。

这听上去不够好,然而如果要等master节点的每一个slave都确实收到了写操作的结果之后再发OK给客户端的话,这显然会带来严重的性能问题。因此这事实上是一种对性能和一致性的一种权衡。

为了尽可能地缓解这一问题,redis集群通过wait命令实现同步写操作,这多少让写操作丢失出现的可能性降低了一点。

还有一种可能导致写操作丢失的情况是集群出现了网络分区(network partition)即客户端和至少一个master节点被孤立于其他的多数节点。举个例子:假设集群包含 A 、 B 、 C 、 A1 、 B1 、 C1 六个节点, 其中 A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端 Z1。当发生网络分区之后,假设集群被分为两块,一块是A 、C 、A1 、B1 和 C1,另一块是B和Z1,则当分区的时间足够长时,B1将被推举为新的master,则Z1向B写入的数据将永久丢失。因为在等待一个制定时长的超时时间之后,(下面会提到的)当一个slave节点被推举为新的master节点时,处于少数分区中的master节点将停止接收写操作。

Redis-cluster 架构图

注意,在网络分区期间,Z1向B发送写命令的最大时间是有限制的,这一限制被称为节点超时。(node timeout)
节点超时的意思是说,当多数分区中的node已经推选出了新的master时,在少数分区中的master节点将停止接收写操作。节点超时还有一个作用就是当某个master节点处于宕机或不可达的状态时,等过了这个超时时间之后,这个master节点中的某个slave节点将作为新的master节点,来替换掉它。

CLUSTER NODES命令

CLUSTER NODES命令可用于监察集群中各个节点的相关信息,其信息格式为:

1
<id> <ip:port> <flags> <master> <ping-sent> <pong-recv> <config-epoch> <link-state> <slot> <slot> ... <slot>

下面分别来介绍每个参数的含义:

  1. 节点ID,由40个随机生成的字符组成,并唯一地标识这个节点。每个节点都会记住其他节点的ID,即它们彼此之间是通过ID来进行辨识的,而不是通过IP和端口号。这也就是说,哪怕某个节点的IP和端口号发生了变化,其他节点也能够找到这个节点。
  2. IP和端口号
  3. 标识(flags) 如master, slave, myself, fail, …
  4. 如果该节点为slave,则显示其master的节点ID,否则显示为’-‘
  5. 集群最近一次向节点发送 PING 命令之后, 过去了多长时间还没接到回复
  6. 节点最近一次收到 PONG 回复的时间
  7. 节点的配置纪元(Configuration epoch)即当前节点(若当前节点为slave,则为其master节点)的配置纪元。每遇到一次失效备援,一个新的,唯一的,单调性的配置纪元就会被创建。它主要用于当多个节点声称负责同一组哈希槽时,配置纪元高的节点将最终负责这一组哈希槽。(配置纪元其形式就是一个非负的数字)
  8. 点对点链接的状态信息,可为connected或disconnected
  9. 该节点所负责的哈希槽的范围

添加、删除、复制迁移、升级节点

添加一个master节点

首先我们要做的,就是添加一个新的空节点。(具体做法请参照下一节)当节点可以正常启动之后,我们通过redis-trib来添加这个节点到现有的集群中,其命令格式为:

1
./redis-trib.rb add-node 新节点IP和端口号 随便一个已经在集群中的节点的IP和端口号

在这里redis-trib所做的工作其实并不多,它主要就是发送了一个CLUSTER MEET命令,这个命令的作用是让两个节点相互认识,从而将新的节点加入到集群中。

不过此时新节点中还没有包含任何数据,因为它没有被分配哈希槽。分配给新节点一些哈希槽,其实也就是分片(reshard)的过程,其命令格式为:./redis-trib.rb reshard 随便一个已经在集群中的节点的IP和端口号。我们只需要指定集群中某一个节点的IP和端口号,redis-trib就能够自动寻找到集群中的其他节点。接下来,我们只需要按照命令行中的提示进行操作即可。具体的下一节有述。

添加一个slave节点

添加一个slave节点的命令是:

1
./redis-trib.rb add-node --slave 新节点IP和端口号 随便一个已经在集群中的节点的IP和端口号

注意:这里并没有指明说我们要把这个slave节点分配到哪一个master节点,事实上,系统会自动挑选一个master节点,然后将这个slave节点分配给它。当然你也可以指定要分配给的master节点,命令为:

1
./redis-trib.rb add-node --slave --master-id master节点ID 新节点IP和端口号 随便一个已经在集群中的节点的IP和端口号

还有一种做法,就是使用CLUSTER REPLICATE 命令。具体做法是,首先我们要访问到这个新的节点,然后执行命令:

1
cluster replicate master节点ID

移除一个节点

如果我们希望移除一个slave节点,只需要执行如下命令:

1
./redis-trib del-node 随便一个已经在集群中的节点的IP和端口号 `希望删除的节点ID`

如果希望移除一个master节点会稍微复杂一些,首先我们要将该节点负责的哈希槽清空,即将它们迁移到其他节点,然后再行移除。还有一种做法就是通过人工的方式(CLUSTER FAILOVER)来制造失效备援,这样当这个master节点的slave节点被推举为新的master节点时,这个旧的master节点就可以被移除了。当然,这么做并不会在实际上减少master节点的数量。

复制迁移

命令格式为:

1
CLUSTER REPLICATE master节点ID

这个命令的作用其实就是切换一个slave节点的master节点。这个命令的优势在于,当我们配置的slave节点数比master数稍多时,当某个master节点的唯一的slave节点宕机了,通过该命令可以将一个多的slave节点分配给该节点,从而提高了整个集群的稳定性。

升级节点

升级一个slave节点是比较简单的,我们只需要将其停机,升级redis版本,然后再重启即可。然而升级一个master节点会稍显复杂,其思路主要就是通过人工出发失效备援,导致master节点和slave节点进行角色互换,升级完成之后再出发一次新的人工失效备援即可。

Redis Cluster环境搭建

搭建一个redis集群环境主要有两种方法:一种是人工操作的方法,另一种时通过执行create-cluster脚本的方法,下面我将分别介绍这两种方法。
注:构建一个集群需要的节点数最少为三个master节点。

人工搭建

方便起见,我们建立三个master节点和三个slave节点。需要执行的操作具体如下:

mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005

给每个目录(7000到7005)分别创建redis.conf。redis.conf的基本格式如下:

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

配置解释:

  1. cluster-config-file指定了redis集群的配置文件所在路径,不过该文件不需要我们人工进行修改,它会在集群简历之后自动创建并自动更新。
  2. appendonly,即Append-only file,是redis的一种实现持久化的方式。将其设为yes之后,任何对redis数据集的修改都会被记录到这个AOF文件中,以便在redis可能遇到故障需要重启时通过载入该文件来恢复到重启前的状态)

注意每个redis.conf中的port都应该与自己对应的port相同。然后拷贝redis-server到cluster-test目录中,然后在终端打开6个tab,每个tab都分别cd到对应的目录中(如7000),然后执行命令:

../redis-server ./redis.conf

这样每个空的节点就都启动起来了。之后我们要做的就是将这些运行中的redis实例构建为一个集群,具体做法是执行如下命令:

./redis-trib.rb create –replicas 1 127.0.0.1:7000 127.0.0.1:7001
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

如果你不做一些事先准备的话,上述命令应该是没法运行的。为了能够成功通过redis-trib.rb执行命令,你首先需要安装ruby。以ubuntu为例,命令是:

1
2
sudo apt-get install ruby-full
sudo gem install redis

另外,如果你要在多台机子上构建redis集群,那么上述的create命令中,节点的ip地址就不可以使用本地地址,而要使用每台机子的真实IP地址,如:

./redis-trib.rb create –replicas 1 192.168.31.243:7000 192.168.31.153:7003 192.168.31.243:7001 192.168.31.153:7004 192.168.31.243:7002 192.168.31.153:7005

注意这里的**–replicas 1**的意思是说为每个master节点分配一个slave节点。那么如果一切顺利的话,我们会在终端上看到如下所示的信息:

[OK] All 16384 slots covered

由上图我们可以看到,master节点对应的端口号为:7001,7000,7003,其余的为slave节点,正好是三对三。

create-cluster脚本搭建

显而易见通过这种方式来构建redis集群真的是太麻烦了,那么简单的方法就是通过这个create-cluster脚本来搭建。create-cluster脚本放在redis目录中的utils/create-cluster目录中。为了启动三个master节点和三个slave节点,我们只需要执行如下几条命令:

create-cluster start
create-cluster create

然后我们再执行上述的redis-trib命令即可。注意,通过这种方式默认创建的第一个节点的端口号为30001。

如果要让整个集群停止运作,只需要执行:

create-cluster stop

你可能会问,如果我想要创建更多的master和slave节点要怎么办?很简单,修改create-cluster脚本即可。

补遗

  1. 集群中的节点并不具有代理功能,即如果收到的命令不属于自己需要处理的范围,该节点并不会将这个命令自动转发给正确的节点,而是会返回给客户端一个MOVED类型的错误。客户端需要能够处理这种错误,并将命令再次发给正确的节点,其实最好能将哈希槽和节点的对应关系缓存起来,这样执行命令的效率才会更高。

  2. 集群中的节点还具有记录key和value映射关系,集群状态,key到正确节点的映射关系,以及自动发现其他节点,识别工作不正常节点的功能。

  3. 节点之间使用Gossip 协议 来进行以下工作:

    1. 传播(propagate)关于集群的信息,以此来发现新的节点。
    2. 向其他节点发送 PING 数据包,以此来检查目标节点是否正常运作。(如正常会返回PONG)
    3. 在特定事件发生时,发送集群信息。
  4. 一个节点可以改变它的IP和端口号,集群可以通过节点ID自动识别出其IP/端口号的变化, 并将这一信息通过Gossip协议广播给其他节点知道。

关于Redis Cluster,我所知道的事(一)

https://justuno.github.io/2015/09/28/2015-09-28-redis-cluster-1/

作者

Justuno

发布于

2015-09-28

更新于

2015-09-30

许可协议

评论