0%

redis之集群

前面介绍数据持久化、主从复制、故障自动恢复这些功能,提升单台redis服务器的可靠性,但是在qps比较高的情况下,单台服务器的性能难以抗住这些流量,所以需要对redis进行集群部署。

简单来说实现方式就是,多个主从节点构成一个集群,每个节点存储一部分数据,这样写请求也可以分散到多个主节点上,解决写压力大的问题。同时,集群化可以在节点容量不足和性能不够时,动态增加新的节点,对进群进行扩容,提升性能。

当然,集群化也意味着Redis部署架构更复杂,管理和维护起来成本也更高。而且在使用过程中,也会遇到很多问题,这也衍生出了不同的集群化解决方案,它们的侧重点各不相同。

这篇文章我们先来整体介绍一下Redis集群化比较流行的几个解决方案,先对它们有整体的认识,最后在详细介绍下redis官方实现的集群方案。

集群化方案要想实现集群化,就必须部署多个主节点,每个主节点还有可能有多个从节点,以这样的部署结构组成的集群,才能更好地承担更大的流量请求和存储更多的数据。

可以承担更大的流量是集群最基础的功能,一般集群化方案还包括了上面提到了数据持久化、数据复制、故障自动恢复功能,利用这些技术,来保证集群的高性能和高可用。

另外,优秀的集群化方案还实现了在线水平扩容功能,当节点数量不够时,可以动态增加新的节点来提升整个集群的性能,而且这个过程是在线完成的,业务无感知。

业界主流的Redis集群化方案主要包括以下几个:

  • 客户端分片
  • Codis
  • Twemproxy
  • Redis Cluster

它们还可以用是否中心化来划分,其中客户端分片、Redis Cluster属于无中心化的集群方案,Codis、Tweproxy属于中心化的集群方案。

是否中心化是指客户端访问多个Redis节点时,是直接访问还是通过一个中间层Proxy来进行操作,直接访问的就属于无中心化的方案,通过中间层Proxy访问的就属于中心化的方案,它们有各自的优劣,下面分别来介绍。

客户端分片

客户端分片主要是说,我们只需要部署多个Redis节点,具体如何使用这些节点,主要工作在客户端。客户端通过固定的Hash算法,针对不同的key计算对应的Hash值,然后对不同的Redis节点进行读写。

客户端分片集群模式
客户端分片需要业务开发人员事先评估业务的请求量和数据量,然后让DBA部署足够的节点交给开发人员使用即可。

这个方案的优点是部署非常方便,业务需要多少个节点DBA直接部署交付即可,剩下的事情就需要业务开发人员根据节点数量来编写key的请求路由逻辑,制定一个规则,一般采用固定的Hash算法,把不同的key写入到不同的节点上,然后再根据这个规则进行数据读取。

可见,它的缺点是业务开发人员使用Redis的成本较高,需要编写路由规则的代码来使用多个节点,而且如果事先对业务的数据量评估不准确,后期的扩容和迁移成本非常高,因为节点数量发生变更后,Hash算法对应的节点也就不再是之前的节点了。

所以后来又衍生出了一致性哈希算法,就是为了解决当节点数量变更时,尽量减少数据的迁移和性能问题。

这种客户端分片的方案一般用于业务数据量比较稳定,后期不会有大幅度增长的业务场景下使用,只需要前期评估好业务数据量即可。

Codis

随着业务和技术的发展,人们越发觉得,当我需要使用Redis时,我们不想关心集群后面有多少个节点,我们希望我们使用的Redis是一个大集群,当我们的业务量增加时,这个大集群可以增加新的节点来解决容量不够用和性能问题。

这种方式就是服务端分片方案,客户端不需要关心集群后面有多少个Redis节点,只需要像使用一个Redis的方式去操作这个集群,这种方案将大大降低开发人员的使用成本,开发人员可以只需要关注业务逻辑即可,不需要关心Redis的资源问题。

多个节点组成的集群,如何让开发人员像操作一个Redis时那样来使用呢?这就涉及到多个节点是如何组织起来提供服务的,一般我们会在客户端和服务端中间增加一个代理层,客户端只需要操作这个代理层,代理层实现了具体的请求转发规则,然后转发请求到后面的多个节点上,因此这种方式也叫做中心化方式的集群方案,Codis就是以这种方式实现的集群化方案。

Proxy集群模式

proxy

Codis架构图

codis

Codis是由国人前豌豆荚大神开发的,采用中心化方式的集群方案。因为需要代理层Proxy来进行所有请求的转发,所以对Proxy的性能要求很高,Codis采用Go语言开发,兼容了开发效率和性能。

Codis包含了多个组件:

  • codis-proxy:主要负责对请求的读写进行转发
  • codis-dashbaord:统一的控制中心,整合了数据转发规则、故障自动恢复、数据在线迁移、节点扩容缩容、自动化运维API等功能
  • codis-group:基于Redis 3.2.8版本二次开发的Redis Server,增加了异步数据迁移功能
  • codis-fe:管理多个集群的UI界面

可见Codis的组件还是挺多的,它的功能非常全,除了请求转发功能之外,还实现了在线数据迁移、节点扩容缩容、故障自动恢复等功能。

Codis的Proxy就是负责请求转发的组件,它内部维护了请求转发的具体规则,Codis把整个集群划分为1024个槽位,在处理读写请求时,采用crc32Hash算法计算key的Hash值,然后再根据Hash值对1024个槽位取模,最终找到具体的Redis节点。

Codis最大的特点就是可以在线扩容,在扩容期间不影响客户端的访问,也就是不需要停机。这对业务使用方是极大的便利,当集群性能不够时,就可以动态增加节点来提升集群的性能。

为了实现在线扩容,保证数据在迁移过程中还有可靠的性能,Codis针对Redis进行了修改,增加了针对异步迁移数据相关命令,它基于Redis3.2.8进行开发,上层配合Dashboard和Proxy组件,完成对业务无损的数据迁移和扩容功能。

因此,要想使用Codis,必须使用它内置的Redis,这也就意味着Codis中的Redis是否能跟上官方最新版的功能特性,可能无法得到保障,这取决于Codis的维护方,目前Codis已经不再维护,所以使用Codis时只能使用3.2.8版的Redis,这是一个痛点。

另外,由于集群化都需要部署多个节点,因此操作集群并不能完全像操作单个Redis一样实现所有功能,主要是对于操作多个节点可能产生问题的命令进行了禁用或限制,具体可参考Codis不支持的命令列表。

Twemproxy

Twemproxy是由Twitter开源的集群化方案,它既可以做Redis Proxy,还可以做Memcached Proxy。

它的功能比较单一,只实现了请求路由转发,没有像Codis那么全面有在线扩容的功能,它解决的重点就是把客户端分片的逻辑统一放到了Proxy层而已,其他功能没有做任何处理。

Tweproxy推出的时间最久,在早期没有好的服务端分片集群方案时,应用范围很广,而且性能也极其稳定。

但它的痛点就是无法在线扩容、缩容,这就导致运维非常不方便,而且也没有友好的运维UI可以使用。Codis就是因为在这种背景下才衍生出来的。

Redis Cluster

采用中间加一层Proxy的中心化模式时,这就对Proxy的要求很高,因为它一旦出现故障,那么操作这个Proxy的所有客户端都无法处理,要想实现Proxy的高可用,还需要另外的机制来实现,例如Keepalive。

而且增加一层Proxy进行转发,必然会有一定的性能损耗,那么除了客户端分片和上面提到的中心化的方案之外,还有比较好的解决方案么?

Redis官方推出的Redis Cluster另辟蹊径,它没有采用中心化模式的Proxy方案,而是把请求转发逻辑一部分放在客户端,一部分放在了服务端,它们之间互相配合完成请求的处理。

Redis Cluster是在Redis 3.0推出的,早起的Redis Cluster由于没有经过严格的测试和生产验证,所以并没有广泛推广开来。也正是在这样的背景下,业界衍生了出了上面所说的中心化集群方案:Codis和Tweproxy。

但随着Redis的版本迭代,Redis官方的Cluster也越来越稳定,更多人开始采用官方的集群化方案。也正是因为它是官方推出的,所以它的持续维护性可以得到保障,这就比那些第三方的开源方案更有优势。

Redis Cluster没有了中间的Proxy代理层,那么是如何进行请求的转发呢?

Redis把请求转发的逻辑放在了Smart Client中,要想使用Redis Cluster,必须升级Client SDK,这个SDK中内置了请求转发的逻辑,所以业务开发人员同样不需要自己编写转发规则,Redis Cluster采用16384个槽位进行路由规则的转发。

没有了Proxy层进行转发,客户端可以直接操作对应的Redis节点,这样就少了Proxy层转发的性能损耗。

Redis Cluster也提供了在线数据迁移、节点扩容缩容等功能,内部还内置了哨兵完成故障自动恢复功能,可见它是一个集成所有功能于一体的Cluster。因此它在部署时非常简单,不需要部署过多的组件,对于运维极其友好。

Redis Cluster在节点数据迁移、扩容缩容时,对于客户端的请求处理也做了相应的处理。当客户端访问的数据正好在迁移过程中时,服务端与客户端制定了一些协议,来告知客户端去正确的节点上访问,帮助客户端订正自己的路由规则。

虽然Redis Cluster提供了在线数据迁移的功能,但它的迁移性能并不高,迁移过程中遇到大key时还有可能长时间阻塞迁移的两个节点,这个功能相较于Codis来说,Codis数据迁移性能更好。这里先了解一个大概就好,后面我会专门针对Codis和Redis Cluster在线迁移功能的性能对比写一些文章。

现在越来越多的公司开始采用Redis Cluster,有能力的公司还在它的基础上进行了二次开发和定制,来解决Redis Cluster存在的一些问题,我们期待Redis Cluster未来有更好的发展。

总结

总结

redis cluster

Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。

集群的节点、槽指派、命令执行、重新分片、转向、故障 转移、消息等各个方面进行介绍。

节点

一个Redis集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。

连接各个节点的工作可以使用CLUSTER MEET命令来完成,该命令的格式如下:

1
CLUSTER MEET <ip> <port

向一个节点node发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。

加入节点的通信过程如下图

节点通信图

上图客户端向加点A发送加入节点B的命令,节点A与节点B进行通信,正常交换完成,会将节点B加入到集群,之后节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,经过一段时间之后,节点B会被集群中的所有节点认识。

槽指派

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。 当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。

通过向节点发送CLUSTER ADDSLOTS命令,我们可以将一个或多个槽指派(assign)给节点负责:

1
CLUSTER ADDSLOTS <slot> [slot ...]

节点收到命令后,将会记录或者更新自己负责的槽,底层是一个二进制位数组(bit array),这个数组的长度为 16384/8=2048个字节,共包含16384个二进制位。

线图展示了一个slots数组示例:这个数组索引1、3、5、8、9、10上的二进制位的值都为1,而其余所有二进制位的值都为0,这表示节点负责处理槽1、3、5、8、9、10。

节点

一个节点除了会将自己负责处理的槽记录之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。

因为集群中的每个节点都会将自己的slots数组通过消息发送给集群中的其他节点,并且每个接收到slots数组的节点都会将数组保存到相应节点的clusterNode结构里面,因此,集群中的每个节点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点。

node

命令执行

在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。 当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:

  • 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令。
  • 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向(redirect)至正确的节点,并再 次发送之前想要执行的命令。

  1. redis使用CRC16(key)语句用于计算键key的CRC-16校验和,然后和16383进行与操作,得到对应的槽。
  2. 判断槽是否由当前节点负责处理,如果是,则处理并返回结果,如果不是,则返回MOVED错误

3.当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED错误,指引客户端转向至正在负责槽的节点。格式为 MOVED <slot> <ip>:<port>,客户端需要响应这个错误,并去对应的节点执行命令。

重新分片

Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属 的键值对也会从源节点被移动到目标节点。

重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。

Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过 向源节点和目标节点发送命令来进行重新分片操作。

redis-trib对集群的单个槽slot进行重新分片的步骤如下:

  1. redis-trib对目标节点发送CLUSTER SETSLOT<slot>IMPORTING<source_id>命令,让目标节点准备好从源 节点导入(import)属于槽slot的键值对。
  2. redis-trib对源节点发送CLUSTER SETSLOT<slot>MIGRATING<target_id>命令,让源节点准备好将属于 槽slot的键值对迁移(migrate)至目标节点。
  3. redis-trib向源节点发送CLUSTER GETKEYSINSLOT<slot> <count>命令,获得最多count个属于槽slot的键值对的键名(keyname)。
  4. 对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATE<target_ip><target_port><key_name>0<timeout>命令,将被选中的键原子地从源节点迁移至目标节点。
  5. 重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。每次迁移键的过程如下图
  6. redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT<slot>NODE<target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。

如果重新分片涉及多个槽,那么redis-trib将对每个给定的槽分别执行上面给出的步骤

整个过程

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。 当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:

  • 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
  • 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。

ask过程

和接到MOVED错误时的情况类似,集群模式的redis-cli在接到ASK错误时也不会打印错误,而是自动根据错误提供的IP地址和端口进行转向动作。

ASK错误和MOVED错误的区别
ASK错误和MOVED错误都会导致客户端转向,它们的区别在于:

  • MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节 点,因为该节点就是目前负责槽i的节点。
  • 与此相反,ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节 点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现。

复制和故障转移

Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。

整个过程和哨兵模式基本保持一致。

消息简介

集群中的各个节点通过发送和接收消息(message)来进行通信,我们称发送消息的节点为发送者(sender),接收消息的节点为接收者(receiver)。

节点发送的消息主要有以下五种:

  • MEET消息:当发送者接到客户端发送的CLUSTER MEET命令 时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前 所处的集群里面。
  • PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长 时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
  • PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播 自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识,例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条PONG消息,以此来让集群中的其他节点立即知道这个节点已经变 成了主节点,并且接管了已下线节点负责的槽
  • FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消 息的节点都会立即将节点B标记为已下线。
  • PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。

一条消息由消息头(header)和消息正文(data)组成。

补充知识

16位的原因
CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?

  1. 如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
    在消息头中,最占空间的是myslots[CLUSTER_SLOTS/8]。
    当槽位为65536时,这块的大小是:
    65536÷8÷1024=8kb
    因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
  2. redis的集群主节点数量基本不可能超过1000个。
    集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
  3. 槽位越小,节点少的情况下,压缩比高
    Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。文件压缩率指的是,文件压缩前后的大小比。

详细的可以参考为什么redis集群有16384个槽

问题

  1. 数据迁移问题:
    Redis集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。在扩缩容的时候,需要进行数据迁移。而Redis为了保证迁移的一致性,迁移所有操作都是同步操作,执行迁移时,两端的Redis均会进入时长不等的阻塞状态,对于小Key,该时间可以忽略不计,但如果一旦Key的内存使用过大,严重的时候会接触发集群内的故障转移,造成不必要的切换。

  2. 带宽消耗问题:
    Redis集群是无中心节点的集群架构,依靠Gossip协议协同自动化修复集群的状态,但goosip有消息延时和消息冗余的问题,在集群节点数量过多的时候,goosip协议通信会消耗大量的带宽,主要体现在以下几个方面:

  • 消息发送频率:跟cluster-node-timeout密切相关,当节点发现与其他节点的最后通信时间超过cluster-node-timeout/2时会直接发送ping消息

  • 消息数据量:每个消息主要的数据占用包含:slots槽数组(2kb)和整个集群1/10的状态数据
    节点部署的机器规模:机器的带宽上限是固定的,因此相同规模的集群分布的机器越多,每台机器划分的节点越均匀,则整个集群内整体的可用带宽越高

集群带宽消耗主要分为:读写命令消耗+Gossip消息消耗,因此搭建Redis集群需要根据业务数据规模和消息通信成本做出合理规划:

在满足业务需求的情况下尽量避免大集群,同一个系统可以针对不同业务场景拆分使用若干个集群。
适度提供cluster-node-timeout降低消息发送频率,但是cluster-node-timeout还影响故障转移的速度,因此需要根据自身业务场景兼顾二者平衡
如果条件允许尽量均匀部署在更多机器上,避免集中部署。如果有60个节点的集群部署在3台机器上每台20个节点,这是机器的带宽消耗将非常严重

  1. Pub/Sub广播问题:

集群模式下内部对所有publish命令都会向所有节点进行广播,加重带宽负担,所以集群应该避免频繁使用Pub/sub功能

  1. 集群倾斜:
    集群倾斜是指不同节点之间数据量和请求量出现明显差异,这种情况将加大负载均衡和开发运维的难度。因此需要理解集群倾斜的原因

  2. 数据倾斜:
    节点和槽分配不均
    不同槽对应键数量差异过大
    集合对象包含大量元素
    内存相关配置不一致

  3. 请求倾斜:
    合理设计键,热点大集合对象做拆分或者使用hmget代替hgetall避免整体读取

  4. 集群读写分离:
    集群模式下读写分离成本比较高,直接扩展主节点数量来提高集群性能是更好的选择。

参考

  1. redis集群化的3种方案对比
  2. 为什么Redis集群有16384个槽
  3. 深入学习Redis之Redis Cluster
  4. Redis 集群演进探讨和总结