vlambda博客
学习文章列表

Redis学习总结 -- 集群

在中,介绍了哨兵如何保证Redis服务高可用的。然而,哨兵是基于单机版Redis的,随着业务规模的不断变大,单机版Redis服务会面临着内存、流量等瓶颈问题。单机扩容虽然能解一时燃眉之急,但最终也会达到单机极限,导致无法扩容。当单机扩容之旅行不通时,我们只能盯着水平扩容-多机Redis。


分布式解决方案

为了解决单机瓶颈问题,纷纷转战分布式解决方案,具体的解决方案如下:


1、客户端分区

多个Redis实例组成一个“分布式集群”,每个Redis实例都存储一部分数据,客户端根据key的hash值映射到特定的Redis实例上进行数据操作,比较有代表性的就是Redis Sharding。Redis Sharding是Redis Cluster出来之前业界常用的Redis分布式解决方案。

在部署时,多个Redis实例可以使用同一套哨兵系统以保证自身高可用。在使用过程中,客户端只需要配置哨兵信息和各个Redis实例的mastername,客户端启动后,会保持跟各个Redis实例的链接。


由上图可知,客户端对数据分区逻辑绝对可控,可以根据业务特点选择合适的数据分区方案。虽然Redis实例间无关联便于水平扩展,但是客户端却无法支持动态增删Redis实例节点,每次增删Redis实例节点都需要修改相应的配置或逻辑。

在客户端分区方案中,客户端需要感知所有的Redis示例信息,维护与各个Redis实例节点的连接。那么有没有一种方案不需要感知后面的Redis实例信息呢?答案是肯定的,这就是下面所讲的代理方案。在计算机领域,是没有增加一层中间层解决不了的问题,如果有,那就再加一层呗!


2、代理方案

客户端和Redis实例间有个代理层,客户端通过代理层访问Redis实例,代理负责将请求转发到正确的Redis实例中。

Redis学习总结 -- 集群

在该方案中,由于代理层为客户端屏蔽了Redis实例信息,因此客户端接入非常方便,只需要连接代理。正是因为新增了代理层,所以多了一层性能损耗,也可能引入新的性能瓶颈,或者“单点”问题。

目前代理方案的主流实现 Twemproxy 和 Codis。

Twemproxy也叫 nutcraker,是Twitter开源的,实现了Redis和Memcache协议,即支持Redis和Memcache代理。Twemproxy需要使用Keepalived做高可用,在运行时,只有一台Twemproxy在工作,另外一台处于备机,当一台挂掉以后,vip自动漂移,备机接替工作。

Redis学习总结 -- 集群

在该方案中,客户端发送请求到Twemproxy中,Twemproxy根据相应的路由规则转发到相应的Redis节点中。为了保证Redis实例的高可用,还需要部署twemproxy-sentinel-agent。twemproxy-sentinel-agent监控sentinal里面master信息,如果sentinal mater信息发生变化,那么修改Twemproxy 的servers配置参数,并重启Twemproxy。

 

Twemproxy最大的问题是无法平滑的扩缩容,而Codis解决了Twemproxy扩缩容问题,并且兼容Twemproxy。Codis是一种分布式集群解决方案,由豌豆荚开源。它发展起来的一个重要原因是在Redis官方集群方案漏洞百出的时候率先成熟稳定的。

Redis学习总结 -- 集群

上图是官方的架构图,codis-proxy是客户端连接的代理服务,实现了Redis协议,可以看作是一个Redis服务器。Codis赖ZooKeeper 来存放数据路由表和 codis-proxy 节点的元信息。

Codis的主要原理是将所有的key分为1024个槽,每个槽对应一个分组(Redis实例),当请求过来时,首先对key进行CRC32计算得到hash值,然后除以1024求余就得到对应的槽编号,最后根据槽编号与分组的对应关系得到具体的分组。

当进行扩容时,配置槽与新增分组的映射关系,只需要将对应槽中的数据迁移到新增分组上即可,避免大范围的数据移动;当进行缩容时,只需要将待缩容分组对应的槽迁移到其他分组即可,避免大范围的数据移动


3、Redis Cluster

对于官方而言,缺少官方分布式解决方案是难以接受的。随着不停的迭代,官方终于推出了自己的分布式解决方案:去中心化。在整个方案中, 并不存在代理层等“类中心节点”,所有Redis实例节点都直接对外提供访问。Redis实例之间通过gossip协议交换互相的状态,以及探测新加入的节点信息。redis集群支持动态加入节点,动态迁移slot,以及自动故障转移。


后面我们主要学习Redis cluster的原理和流程。


Redis Cluster


Redis cluster是Redis去中心化的分布式解决方案,集群中的每个节点都是平等关系,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。


对于Redis cluster我们首先了解它是如何使用的,然后了解学习其基本原理,比如数据如何分片的、Redis实例间是如何通讯的、Redis实例故障迁移、如何扩缩容等。


1、使用流程

在Codis中,数据的具体分发是由代理层来完成的,而在Redis cluster中,数据分发是由client来完成的,具体流程如下:

  • 客户端连接到任一Redis实例上

  • 客户端发送命令到该Redis实例上

  • 如果命令中的key所在的槽就位于该Redis实例上,则直接进行操作,否则,返回MOVED错误以及key对应节点信息。

  • 客户端收到MOVED错误和key对应的节点信息,则连接到该Redis实例进行数据操作。


目前客户端有两种方案获取数据分布表:

1、客户端每次根据返回的MOVED信息缓存一个slot对应的节点,但是这种做法在初期会经常造成访问两次集群。
2、在节点返回MOVED信息后,通过cluster nodes命令获取整个数据分布表,更新本地缓存的数据分布信息。


此外,需要注意的是,我们要操作的数据所在的槽可能正在进行迁移,如果key还在待迁移节点上,则会在该节点上直接执行,否则返回ASK错误和迁移的目标节点的信息。客户端收到目标节点的信息后,首先发送ASKING命令进行确认,当通过后执行原先的命令。


不过不用担心重复造轮子,因为这些细节一般都会提供统一的库或sdk,使用者很方便的就可以使用。


在使用的过程中,需要区分MOVED和ASK错误的区别:

  • MOVED:正常情况下,client会拥有全局的slot映射信息,但某些场景下(比如扩容后部分slot槽信息变更),client拥有的slot映射信息是错误的,此时访问该slot时就会访问错误的实例上,该实例就会返回MOVED错误,并告诉client正确的实例信息。

  • ASK:该错误只发生在数据迁移过程中,当访问的key数据所在slot正在迁移(slot会对应多个key,迁移过程中key是分批迁移),假设数据从A迁移到B,此时如果A中不存在该key,那么可能key可能位于B中,此时A就会返回ASK错误,并返回B的实例信息。


这里需要注意的是,在数据迁移(A->B)过程中,如果先访问到B,由于slot信息还未与B建立映射关系,此时哪怕key已经位于B上了,B也会返回MOVED错误。对于处于迁移过程中的key,B只会处理ASKING命令,其他命令都会返回MOVED错误,并重定向到A实例上,然后走正常的ASK、ASKING流程。


2、数据分片

Redis cluster采用虚拟槽分区算法,即将Redis实例的存储空间分为13684个槽。Redis cluster使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和。假设Redis cluster有三个Redis实例节点:

  • Redis1 对应的slot为0~8000

  • Redis2 对应的slot为800112000

  • Redis3对应的slot为12001~16383


对于key为my_name计算其槽编号CRC16('my_name')%16384 = 2412,由于2412位于0~8000之间,因此my_name这个key应该位于Redis1实例中。


通过以上公式得到的槽编号都是固定的,然而在某些场景下,我们希望使用MSET通过操作多个key。往往这些key处在不同的Redis实例中,这样我们并不能保证操作的原子性。为了保证操作的原子性,我们希望能够保证这些key在同一个Redis实例中。通过以上公式并不能保证这些,此时我们就需要采用其他方案--Hash Tag。


Hash Tag允许用key的部分字符串来计算hash,一个key包含占位符 {} (占位符可配置,hash_tag)的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash。比如对user:{user1}:ids和user:{user1}:tweets这两个key,由于{}中的内容都是user1,因此这两个key hash值一致,所以在同一个槽中。


3、Redis实例间通讯

在Redis cluster中,Redis实例间是需要互通有无的。Redis实例间有多种消息,比如

  • Meet消息:新节点首次加入时发布,其他节点收到后需要回复pong消息。

  • Ping消息:经常用于心跳,一般会封装一些自身信息和其他节点状态数据,其他节点收到后需要回复Pong消息。

  • Pong消息:收到Meet/Ping消息后回复,通常也会携带自身信息。

  • Fail消息:节点下线或故障时,会广播这个信息。

为了便于消息在集群中传播,Redis Cluster采用Gossip协议来加快消息在集群中的传播。


4、故障发现和恢复


4.1 故障发现

在进行故障迁移之前,首先要判定Redis实例是否故障。在Redis cluster中,Redis实例故障判定逻辑跟哨兵一致,也分为主观下线和客观下线。当Redis实例被标记为客观下线才认为Redis实例节点故障。

主观下线:节点间会定时发送Ping消息,当超过 cluster-node-timeout时未收到Pong消息,则认为对方节点下线。

客观下线:当半数以上的Redis实例节点都认为某Redis实例节点下线时,才会认为Redis实例节点下线,即大部分节点认为下线才是真的下线。


当收到其他节点的主观下线消息后,首先会进行时效性检查,如果超过 cluster-node-timeout*2 的时间,就忽略这个消息;否则,如果有半数以上的节点都认为主观下线,则将其标记为客观下线,并向集群中广播Fail消息,通知所有的节点将故障节点标记为客观下线,这个消息指包含故障节点的 ID。


4.2 故障恢复

当确认节点故障时,后面就需要从众多从节点中选择一个从节点进行恢复,流程如下:

  1. 资格检查:过滤掉与故障主节点长时间断开的从节点,当断开时间超过了 cluster-node-timeout*cluster-slave-validity-factor(从节点有效因子,默认为 10),那么相应的从节点就没有故障转移的资格。

  2. 新主节点选举:对符合资格的从节点按照从节点上复制偏移量从大到小进行排序,依次选择偏移量最大的从节点来触发选举。选举原理跟Raft算法类似,从节点在发起选举前都会生成一个递增的epoch,在消息传播过程中,所有节点使用最大的epoch来更新本地epoch值。

  3. 数据更新:当从节点收到半数以上的主节点的票数时,表示主节点已经选定了,会触发替换主节点的操作:删除旧主节点的槽数据,并将这些槽数据加到自身上,并向集群广播主节点已选定的消息。

  4. 数据同步:从节点连接到最新的主节点,并进行数据同步复制。


这里需要注意的是,故障恢复是由故障主节点的从节点发起的,参与投票的是其他正常的主节点。参与选举的是主节点,一方面是因为主节点才有全量的slot信息等,另一方面是为了节省成本,从节点不参与投票,因此对故障主节点的从节点数量就没有要求。


5、扩缩容

作为一个分布式缓存集群,因为业务变化总会遇到扩缩容的问题。

5.1 Redis cluster集群扩容

为了支持Redis cluster的动态扩容,Redis cluster有一套标准的扩容流程,具体扩容流程如下:

  1. 准备新节点

    配置和其他Redis实例节点保持一致,不需要设置主从节点

  2. 加入集群

    执行cluster meet serverip serverport 命令加入集群

  3. 数据迁移

    数据迁移分为两部分:槽迁移和数据迁移。在迁移过程中,先把槽准备好,然后才开始进行数据迁移,具体流程如下:

在数据迁移过程中,首先要确定哪些slot迁入目标节点,哪些slot从源节点迁出;然后分slot分批次从源节点中读取待迁移key列表,并将这些key迁移到目标节点;最后完成数据迁移后,向所有主节点广播目标节点的槽信息。


在key迁移过程中,client向源节点发送数据迁移请求,而源节点收到请求后,将作为client向目标节点发送数据,目标节点收到数据并处理完毕后将返回OK,源节点收到OK应答后,将删除本地key,并返回client处理完成。


在数据迁移过程中,会不会出现写冲突呢?答案是 不会。Redis是单线程服务,不会出现两个请求同时处理。


5.2 Redis cluster集群缩容

缩容有两种情况:

  • 待移除节点不含槽数据或者从节点,可以直接通知集群内其他节点忘记下线节点,当所有节点忘记该节点后就可以正常关闭。

  • 待移除节点包含槽数据,需要先将节点上槽数据迁移到其他节点上,保证节点下线后整个槽节点映射的完整性

在缩容的数据迁移流程跟扩容的数据迁移流程正好相反。首先确定待移除节点的槽,以及这些槽移到哪些节点上;然后,分slot分批次的读取源节点中的待迁移key列表,并将这些key迁移到指定节点上;最后,完成数据迁移后,向所有主节点广播下线信息后正常关闭。


redis-trib.rb是官方提供的Redis Cluster的管理工具,支持的操作如下:

  • create:创建集群

  • check:检查集群

  • info:查看集群信息

  • fix:修复集群

  • reshard:在线迁移slot

  • rebalance:平衡集群节点slot数量

  • add-node:添加新节点

  • del-node:删除节点

  • set-timeout:设置节点的超时时间

  • call:在集群所有节点上执行命令

  • import:将外部redis数据导入集群

有兴趣实操可以使用redis-trib.rb help得到更多帮助。


附录

  • Twemproxy github https://github.com/twitter/twemproxy/

  • Codis github https://github.com/CodisLabs/codis

  • Codis的架构设计 https://blog.csdn.net/shmiluwei/article/details/51958359

  • Redis技巧:分片技术和Hash Tag https://www.jianshu.com/p/c441b882c1c6

  • 不懂Redis Cluster原理,我被同事diss了!https://baijiahao.baidu.com/s?id=1663270958212268352&wfr=spider&for=pc

  • 【redis】深入理解redis cluster --- 扩容集群 https://www.cnblogs.com/chenYanfeng/articles/9210344.html