• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Redis Cluster技术应用实践(一)

Redis 彭东稳 8年前 (2017-03-03) 23893次浏览 已收录 0个评论

一、Redis Cluster介绍

Redis 在 3.0 版正式引入了集群这个特性。

Redis 集群是一个提供在多个 Redis 间节点间共享数据的程序集。

Redis 集群是一个分布式(distributed)、容错(fault-tolerant)的Redis内存K/V服务,集群可以使用的功能是普通单机Redis所能使用的功能的一个子集(subset),比如Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。还有比如set里的并集(unions)和交集(intersections)操作,就没有实现。通常来说,那些处理命令的节点获取不到键值的所有操作都不会被实现。在将来,用户或许可以通过使用 MIGRATE COPY 命令,在集群上用计算节点(Computation Nodes) 来执行多键值的只读操作, 但 Redis 集群本身不会执行复杂的多键值操作来把键值在节点间移来移去。

Redis 集群不像单机版本的 Redis 那样支持多个数据库,集群只有数据库 0,而且也不支持 SELECT 命令。

Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。

Redis集群的优点:

  • 无中心架构,分布式提供服务。
  • 数据按照slot存储分布在多个redis实例上。
  • 增加slave做standby数据副本,用于failover,使集群快速恢复。
  • 实现故障auto failover,节点之间通过gossip协议交换状态信息;投票机制完成slave到master角色的提升。
  • 支持在线增加或减少节点。
  • 降低硬件成本和运维成本,提高系统的扩展性和可用性。

Redis集群的缺点:

  • client实现复杂,驱动要求实现smart client,缓存slots mapping信息并及时更新。
  • 目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。
  • 客户端的不成熟,影响应用的稳定性,提高开发难度。
  • 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线。这种failover是没有必要,sentinel也存在这种切换场景。

Redis集群的目标:

Redis集群是Redis是一个分布式实现,主要是为了实现以下这些目标(按在设计中的重要性排序):

  • 在1000个节点的时候仍能表现得很好并且可扩展性(scalability)是线性的。
  • 没有合并操作,这样在Redis的数据模型中最典型的大数据值中也能有很好的表现。
  • 写入安全(Write safety):那些与大多数节点相连的客户端所做的写入操作,系统尝试全部都保存下来。不过公认的,还是会有小部分(small windows?)写入会丢失。
  • 可用性(Availability):在绝大多数的主节点(master node)是可达的,并且对于每一个不可达的主节点都至少有一个它的从节点(slave)可达的情况下,Redis 集群仍能进行分区(partitions)操作。

Redis集群拓扑结构

Redis集群是一个网状结构,每个节点都通过TCP连接跟其他每个节点连接。在一个有N个节点的集群中,每个节点都有N-1个流出的TCP连接,和N-1个流入的连接。这些TCP连接会永久保持,并不是按需创建的。

Redis Cluster技术应用实践(一)

二、Redis集群数据分片原理

槽(slot)概念

Redis集群的分片特征在于没有使用一致性哈希,而是引入了哈希槽的概念,将键空间分拆了16384个槽位,每一个节点负责其中一些槽位,集群的最大节点数量也是 16384 个(推荐的最大节点数量为 1000 个)。 这个槽是一个虚拟的槽,并不是真正存在的。当16384个槽位都有主节点负责处理时,集群进入”稳定“上线状态,可以开始处理数据命令。

为什么槽是 16384(2^14) 个,而不是 65536(2^16) 个呢?如果槽位为 65536,发送心跳信息的消息头达 8k,发送的心跳包过于庞大。集群节点越多,心跳包的消息体内携带的数据越多。如果节点过 1000 个,也会导致网络拥堵。因此 Redis 作者,不建议 Redis Cluster 节点数量超过 1000 个。 那么,对于节点数在 1000 以内的 Redis Cluster 集群,16384 个槽位够用了。没有必要拓展到 65536 个。

Redis Cluster中的每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成(redis-trib.rb脚本)。这里值得一提的是,在Redis Cluster中,只有Master才拥有槽的所有权,如果是某个Master的slave,这个slave只负责槽的使用,但是没有所有权。Redis Cluster怎么知道哪些槽是由哪些节点负责的呢?某个Master又怎么知道某个槽自己是不是拥有呢?

NOTE

为什么没有使用一致性 hash 算法,而是使用了哈希槽预分片? 缓存热点问题:一致性哈希算法在节点太少时,容易因为数据分布不均匀而造成缓存热点的问题。一致性哈希算法可能集中在某个 hash 区间内的值特别多,会导致大量的数据涌入同一个节点,造成 master 的热点问题(如同一时间 10W 的请求都在某个 hash 区间内)。

位序列结构

Master节点维护着一个16384/8字节的位序列,Master节点用bit来标识对于某个槽自己是否拥有。比如对于编号为1的槽,Master只要判断序列的第二位(索引从0开始)是不是为1即可。

Redis Cluster技术应用实践(一)

如上面的序列,表示当前Master拥有编号为1,134的槽。集群同时还维护着槽到集群节点的映射,是由长度为16384类型为节点的数组实现的,槽编号为数组的下标,数组内容为集群节点,这样就可以很快地通过槽编号找到负责这个槽的节点。位序列这个结构很精巧,即不浪费存储空间,操作起来又很便捷。

键空间分布算法

Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的使用公式CRC16(Key)&16383计算key属于哪个槽:HASH_SLOT = CRC16(key) mod 16384,其CRC16其结果长度为16位。集群的每个节点负责一部分hash槽。举个例子,比如当前集群有3个节点,那么:

  • 节点A包含0到5500号哈希槽。
  • 节点B包含5501到11000号哈希槽。
  • 节点C包含11001到16384号哈希槽。

这种结构很容易添加或者删除节点,比如如果我想新添加个节点D,我需要从节点A, B, C中的部分槽分到D上。如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可.。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。

Redis客户端在任意一个Redis实例发出请求,如果所需数据不在该实例中,通过重定向命令引导客户端访问所需的实例。工作流程如下。

1、Redis客户端在Redis1实例上访问某个数据。

2、在Redis1内发现这个数据是在Redis2这个实例中,给Redis客户端发送一个重定向的命令。

3、Redis客户端收到重定向命令后,访问Redis2实例获取所需的数据。

值得注意的是,指定的key会被存储在哪个slot,这个关系是铁打不变的。如果我提交了一批命令,往Redis中存储一批键,那么这些键一般会被映射到不同的slot,而不同的slot又可能由Redis Cluster中不同的节点服务,这样就和的预期有点不同,有没有办法将这批键映射到同一个slot呢?答案是可以。

键哈希标签原理

键哈希标签是一种可以让用户指定将一批键都能够被存放在同一个槽中的实现方法,用户唯一要做的就是按照既定规则生成key即可。Redis将每个键的键名的有效部分使用CRC16算法计算出散列值,然后取对16384的余数。这样使得每个键都可以分配到16384个插槽中,进而分配的指定的一个节点中处理。这里键名的有效部分是指,如果键名包含{}号,则{}号内的字符则视为有效值。例如键hello.world的有效部分为”hello.world”,键{user102}有效部分为”user102″。如果命令设计多个键(如MGET),只有当所有键都位于同一个节点时Redis才能正常支持。利用键的分配规则,可以将所有相关的键的有效部分设置成同样的值使得相关键都能分配到同一个节点以支持多键操作。比如{user102}.first和{user102}.last会被分配到同一个节点,所以可以使用MGET {user102}.first {user102}.last来同时获取两个键的值。

重定向客户端

Redis Cluster并不会代理查询,那么如果客户端访问了一个key并不存在的节点,这个节点是怎么处理的呢?比如我想获取key为msg的值,msg计算出来的槽编号为254,当前节点正好不负责编号为254的槽,那么就会返回客户端下面信息:

表示客户端想要的254槽由运行在IP为127.0.0.1,端口为6381的Master实例服务。如果根据key计算得出的槽恰好由当前节点负责,则当期节点会立即返回结果。这里明确一下,没有代理的Redis Cluster可能会导致客户端两次连接急群中的节点才能找到正确的服务,推荐客户端缓存连接,这样最坏的情况是两次往返通信。

主从复制模型

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品。在我们例子中具有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。

然而如果在集群创建的时候(或者过一段时间)我们为每个节点添加一个从节点A1,B1,C1,那么整个集群便有三个master节点和三个slave节点组成,这样在节点B失败后,集群便会选举B1为新的主节点继续服务,整个集群便不会因为槽找不到而不可用了。不过当B和B1都失败后,集群还是不可用的。

集群一致性保证

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。第一个原因是因为集群是用了异步复制,写操作过程:

1)客户端向主节点B写入一条命令。

2)主节点B向客户端回复命令状态。

3)主节点将写操作复制给他得从节点B1, B2 和 B3。

主节点对命令的复制工作发生在返回命令回复之后,因为如果每次处理命令请求都需要等待复制操作完成的话,那么主节点处理命令请求的速度将极大地降低 —— 我们必须在性能和一致性之间做出权衡。注意:Redis集群可能会在将来提供同步写的方法。

Redis集群另外一种可能会丢失命令的情况是集群出现了网络分区,并且一个客户端与至少包括一个主节点在内的少数实例被孤立。举个例子,假设集群包含A 、 B 、 C 、 A1 、 B1 、 C1六个节点, 其中A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端Z1,假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A 、C 、A1 、B1和C1 ,小部分的一方则包含节点B和客户端 Z1。Z1仍然能够向主节点B中写入,如果网络分区发生时间较短,那么集群将会继续正常运作,如果分区的时间足够让大部分的一方将B1选举为新的master,那么Z1写入B中得数据便丢失了。

注意,在网络分裂出现期间,客户端Z1可以向主节点B发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项。

三、Redis集群节点属性

Redis集群中的节点不仅要记录键和值的映射,还需要记录集群的状态,包括键到正确节点的映射。它还具有自动发现其他节点,识别工作不正常的节点,并在有需要时,在从节点中选举出新的主节点的功能。

为了执行以上列出的任务, 集群中的每个节点都与其他节点建立起了“集群连接(cluster bus)”, 该连接是一个 TCP连接,使用二进制协议进行通讯。节点之间使用Gossip协议来进行以下工作:

a)传播(propagate)关于集群的信息,以此来发现新的节点。

b)向其他节点发送 PING 数据包,以此来检查目标节点是否正常运作。

c)在特定事件发生时,发送集群信息。

除此之外, 集群连接还用于在集群中发布或订阅信息。集群节点不能前端代理命令请求, 所以客户端应该在节点返回 -MOVED或者 -ASK转向(redirection)错误时, 自行将命令请求转发至其他节点。客户端可以自由地向集群中的任何一个节点发送命令请求,并可以在有需要时,根据转向错误所提供的信息,将命令转发至正确的节点,所以在理论上来说,客户端是无须保存集群状态信息的。但如果客户端可以将键和节点之间的映射信息保存起来, 可以有效地减少可能出现的转向次数, 籍此提升命令执行的效率。

每个节点在集群中由一个独一无二的 ID标识,该ID是一个十六进制表示的160位随机数,在节点第一次启动时由/dev/urandom生成。节点会将它的ID保存到配置文件,只要这个配置文件不被删除,节点就会一直沿用这个 ID 。一个节点可以改变它的IP和端口号,而不改变节点ID 。集群可以自动识别出IP/端口号的变化,并将这一信息通过Gossip协议广播给其他节点知道。

下面是每个节点都有的关联信息, 并且节点会将这些信息发送给其他节点:

a)节点所使用的IP地址和TCP端口号。

b)节点的标志(flags)。

c)节点负责处理的哈希槽。

d)节点最近一次使用集群连接发送PING数据包(packet)的时间。

e)节点最近一次在回复中接收到PONG数据包的时间。

f)集群将该节点标记为下线的时间。

g)该节点的从节点数量。

如果该节点是从节点的话,那么它会记录主节点的节点ID 。如果这是一个主节点的话,那么主节点ID这一栏的值为0000000。在了解Redis Cluster的集群基本特征后,我们首先搭建出这个Redis Cluster集群。

四、Redis集群配置

1. 准备工作

要让集群正常工作至少需要3个主节点,在这里我们要创建6个redis节点并部署到1台物理节点上,其中三个为主节点,三个为从节点,对应的redis节点的ip和端口对应关系如下:

2. 安装Redis

下载redis,这里需要下载最新的3.2版本,之前的版本不支持集群模式(所有节点)。

3. 环境部署

172.18.13.14节点配置

1)创建集群需要的目录

2)为每个实例创建配置文件(最简单的配置信息)

$ cat /data/redis/cluster-6551/conf/redis.conf

$ cat /data/redis/cluster-6552/conf/redis.conf

等等配置文件。

文件中daemonize为yes,确保守护进程开启。cluster-enabled选项用于开实例的集群模式, 而cluster-conf-file选项则设定了保存节点配置文件的路径,默认值为nodes.conf.节点配置文件无须人为修改,它由Redis集群在启动时创建,并在有需要时自动进行更新。

3)启动各实例

4)查看各进程

启动实例时会打印的日志显示,因为nodes.conf文件不存在,所以每个节点都为它自身指定了一个新的 ID :

实例会一直使用同一个ID,从而在集群中保持一个独一无二(unique)的名字。

172.18.13.15节点配置

主机1部署完之后,按照相同的做法在主机2上操作即可。

现在我们已经有了六个正在运行中的Redis实例, 接下来我们需要使用这些实例来创建集群。通过使用Redis集群命令行工具redis-trib,编写节点配置文件的工作可以非常容易地完成:redis-trib位于Redis源码的src文件夹中,它是一个Ruby程序,这个程序通过向实例发送特殊命令来完成创建新集群, 检查集群, 或者对集群进行重新分片(reshared)等工作。但是redis-trib是Ruby程序,所以需要解决Ruby依赖。

4. 安装Ruby依赖

Redis Cluster的安装需要的环境我们需要准备好,最重要的最难解决的就是ruby环境。在这里,给大家一个国内镜像Ruby China,基本实时同步,由淘宝贡献。

1)安装Ruby环境

2)添加Ruby China镜像

3)安装gem redis

4. 集群创建

这个命令在这里用于创建一个新的集群(create),选项–replicas 1表示我们希望为集群中的每个主节点创建一个从节点。之后跟着的其他参数则是这个集群实例的地址列表,3个master3个slave。redis-trib会打印出一份预想中的配置给你看,如果你觉得没问题的话,就可以输入yes ,redis-trib就会将这份配置应用到集群当中,让各个节点开始互相通讯,最后可以得到如下信息:

5. 集群状态查看

集群状态信息查看命令“cluster info”

集群节点信息查看命令“cluster nodes”

第一个参数:节点ID;

第二个参数:IP和端口;

第三、四个参数:节点状态,会有master, slave, myself, fail, …这几种;如果是个从节点, 这里是它的主节点的NODE ID;

第五、六个参数:集群最近一次向节点发送PING命令之后,过去了多长时间还没接到回复;

第七、八个参数:节点最近一次返回PONG回复的时间节点的配置纪元(configuration epoch),详细信息请参考Redis集群规范;

第九、十个参数:本节点的网络连接情况,例如connected。

第十一个参数:节点目前包含的槽,例如10.99.73.11:6551目前包含号码为0至5460的哈希槽。

6. 集群测试

Redis集群现阶段的一个问题是客户端实现很少,以下是官网给出的目前Redis集群仅有的客户端的实现:

redis-rb-cluster,是作者编写的Ruby实现,用于作为其他实现的参考。该实现是对redis-rb的一个简单包装,高效地实现了与集群进行通讯所需的最少语义(semantic)。

redis-py-cluster,看上去是redis-rb-cluster的一个Python版本, 这个项目有一段时间没有更新了(最后一次提交是在好几个月之前), 不过可以将这个项目用作学习集群的起点。

Jedis,Jedis最近添加了对集群的支持,使用最多的是java客户端,详细请查看项目README中Jedis Cluster部分。

StackExchange.Redis,提供对C#的支持(并且包括大部分 .NET 下面的语言,比如: VB, F#等等)。

thunk-redis,提供对Node.js和io.js的支持。

测试Redis集群比较简单的办法就是使用redis-rb-cluster或者redis-cli ,接下来我们将使用redis-cli为例来进行演示:

我们输入redis-cli -h 127.0.0.1 -p 6551 -c命令,切忌要加入-c,否则我们进入的不是集群环境。进入客户端以后,我们输入set a 100发现他会进行跳转,这就是因为他经过计算以后,要存储100的hash槽在7003实例上。这样就表示我们的集群成功了!

redis-cli对集群的支持是非常基本的,所以它总是依靠 Redis 集群节点来将它转向(redirect)至正确的节点。一个真正的(serious)集群客户端应该做得比这更好: 它应该用缓存记录起哈希槽与节点地址之间的映射(map), 从而直接将命令发送到正确的节点上面。这种映射只会在集群的配置出现某些修改时变化, 比如说, 在一次故障转移(failover)之后, 或者系统管理员通过添加节点或移除节点来修改了集群的布局(layout)之后, 诸如此类。

另外关闭redis集群不能直接kill掉进程,或者关机,我们要通过命令:

进行关闭,这样在关闭之前,数据才能够进行保存。

一般应用程序端在操作redis cluster的时候只需要把集群节点都添加进配置文件即可,但是一般填写5-6个节点即可。在操作上,不管是主节点操作还是从节点操作都是可以进行一次操作的成功,因为就算在从节点进行写入,他也会重定向到主节点上进行操作的。

7. 集群常见操作命令

1)集群状态

2)节点(nodes)

3)槽(slot)

4)键(key)

8. 手动创建一个集群

A. 分别在各个Redis实例添加hash slots(这里是平均分配)

B. 分别在各个Redis实例执行SET-CONFIG-EPOCH

C. 连接集群,在一个Redis实例执行就可以了

D. 分别给每个Redis实例添加一个从节点

至此,手动创建一个集群就完成了,使用redis-trib.rb工具就是把这些步骤给包装了而已,使我们管理起来更加便捷。

接下来可以看下面两篇文章:

Redis cluster增加删除节点(二)

Redis cluster管理工具redis-trib.rb详解(三)


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (4)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!