Redis Cluster与Codis
Redis扩展数据存储量:
- 纵向扩展:升级单个redis实例资源配置,包括增加内存容量、磁盘容量、高配置PCU。
- 优缺点:实施简单、直接。但是单个实例数据量大,RDB持久化时,fork子进程可能会阻塞一定时间,不考虑持久化还能接受。其次硬件容量跟成本是有限制的,不能无限扩。
- 横向扩展:横向增加当前Redis实例个数。分片集群解决了持久化的阻塞问题和硬件成本。
Redis集群分片方案:
- client分片访问,client对key做 hash,然后按取模或一致性hash,把 key的读写分散到不同的 Redis 实例上。
- 在 Redis 前加一个 proxy,把路由策略、后端 Redis 状态维护的工作都放到 proxy 中进行,client 直接访问 proxy,后端 Redis 变更,只需修改 proxy 配置即可。
- 直接使用 Redis cluster。Redis 创建之初,使用方直接给 Redis 的节点分配 slot,后续访问时,对 key 做 hash 找到对应的 slot,然后访问 slot 所在的 Redis 实例。在需要扩容缩容时,可以在线通过 cluster setslot 指令,以及 migrate 指令,将 slot 下所有 key 迁移到目标节点,即可实现扩缩容的目的。
Redis-Cluster的作用:
- Redis集群的每个数据库存有集群中的所有数据,从而导致集群的总数据存储量受限于内存最小的节点,形成了木桶效应。在redis3.0之前,我们是通过在客户端去做的分片,通过hash环的方式对key进行分片存储。分片虽然能够解决各个节点的存储压力,但是导致维护成本高、增加、移除节点比较繁琐。
- Redis-Cluster支持集群功能,集群的特点在于拥有和单机实例一样的性能,同时在网络分区以后能够提供一定的可访问性以及对主数据库故障恢复的支持。
常见的几种数据分区方式详情:
- 节点取余:根据key的hash值和节点数取模计算出节点ID,向对应节点提交数据
- 一致性哈希:用环形结构标识取值范围,通过哈希函数每个节点都会被分配到环上的一个位置,每个键值对也会被映射到环上的位置,然后顺时针找到相邻的节点。新增或删除节点会造成数据分布不均匀。
- 虚拟槽:每个redis节点上有一定数量的槽。当客户端提交数据时,要先根据CRC16(key)&16383来计算出数据要落到哪个虚拟槽内。这是服务端分区,客户端可以将数据提交到任意节点上,如果存储该数据的槽不在这个节点,通过MOVED异常让客户端去重定向。
- 拓扑结构:

一个Redis Cluster由多个Redis节点构成。不同节点组服务的数据没有交集,每个一节点组对应一个数据分片。节点组内部分为主备两类节点,对应master和slave节点。两者数据实时一致,通过异步化的主备复制机制来保证。一个节点组有且只有一个master节点,同时可以有0到多个slave节点,在这个节点组中只有master节点对用户提供写服务,读服务可以由master或者slave提供
redis-cluster是基于gossip协议实现的无中心化节点的集群,因为去中心化的架构不存在统一的配置中心,各个节点对整个集群状态的认知来自于节点之间的信息交互。在Redis Cluster中,这个信息交互是通过Redis Cluster Bus来完成的
结点ID:Cluster中每个节点都有一个唯一名称,在结点首次启动时产生,修改结点IP地址无需修改结点ID,Redis Cluster使用gossip协议自动检测结点的IP和端口变化
节点握手:
RedisCluster中的节点A发送MEET消息通知另外一个节点B,让节点B把节点A当做Cluster中的一员。如果节点A认识节点B,节点B认识节点C,那么节点B可以发送包含节点C信息的MEET消息给节点A,从而节点A也认识节点C了,即节点A将与节点C建立连接。
这个机制确保了以下两个结论的成立:
- Redis Cluster能够在人工组建了一个初始网络后,最终能够自动完成全网连接拓扑的建立。
- Redis Cluster能够阻止一个节点在改变IP和端口后错误的混入其它Cluster。
Redis Cluster分区:采用了虚拟槽分区,使用分散度良好的哈希函数将数据映射到一个固定范围内的整数集合(槽-slot)。范围是0~16383,通过公式CRC16(key)%16383计算数据要落到哪个虚拟槽内。虚拟槽分区是服务端分区。
重定向客户端:客户端可以将数据提交到任意一个redis cluster节点上,如果存储该数据的槽不在这个节点上,则返回给客户端moved异常,异常信息包括目标节点和槽的信息,客户端通过moved异常,永久的将请求转移到目标节点。
HashTags:
- 在集群环境中,执行MSET命令,多个key可能分配到不同的机器,导致某些key没有被改变。此时会要求某些相关联的key分配到相同机器:
- 分片其实就是一个hash的过程,对key做hash取模然后划分到不同的机器上,只要保证相关联的key的hash值相同,就是可以将其分配到相同机器。Redis引入了HashTag的概念,可以使得数据分布算法可以根据key的某一个部分进行计算,然后让相关的key落到同一个数据分片。
- 举个简单的例子,加入对于用户的信息进行存储, user:user1:id、user:user1:name/ 那么通过hashtag的方式,user:{user1}:id、user:{user1}.name;
- 当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash
分片迁移:
在一个稳定的Redis cluster下,每一个slot对应的节点是确定的,但是在某些情况下,节点和分片对应的关系会发生变更
- 新加入master节点
- 某个节点宕机
也就是说当动态添加或减少节点时,需要将16384个槽做个再分配,槽中的键值也要迁移。这一过程,在目前实现中需要人工介入。
新增一个主节点:从各个节点的前面拿取一部分slot到新节点上
删除一个主节点:先将节点的数据移动到其他节点上,然后才能执行删除
槽迁移的过程:槽迁移的过程中有一个不稳定状态,这个不稳定状态会有一些规则,这些规则定义客户端的行为,从而使得Redis Cluster不必宕机的情况下可以执行槽的迁移。下面这张图描述了我们迁移编号为1、2、3的槽的过程中,他们在MasterA节点和MasterB节点中的状态。

工作流程:从A迁移到B
- 把Master B对应的slot状态设置为IMPORTING,表示slot正在迁入
- 把MasterA对应的slot状态设置为MIGRATING,表示slot正在迁移
为了保证slot数据的一致性,MasterA此时对于slot内部数据提供读写服务的行为和通常状态下是有区别的。
MIGRATING状态:
- 如果访问的Key还没有迁移出去,则正常处理
- 如果key已经迁移或者根本就不存在这个key,则回复客户端ASK信息让它跳转到MasterB去执行
ask异常:在redis cluster中,如果新增或者删除节点,需要进行虚拟槽的迁移。ask异常与moved异常类似,只不过ask异常发生在虚拟槽的迁移过程中,另外它与moved异常不同,ask异常只是将下一次的请求转移到目标节点
IMPORTING状态:
- 在该状态下,B仍然能对外提供该slot的读写服务,但和通常状态下也是有区别的
- 当来自客户端的正常访问不是从ASK跳转过来的,说明客户端还不知道迁移正在进行,很有可能操作了一个目前还没迁移完成的并且还存在于MasterA上的key,如果此时这个key在A上已经被修改了,那么B和A的修改则会发生冲突。所以对于MasterB上的slot上的所有非ASK跳转过来的操作,MasterB都不会处理,而是通过MOVED命令让客户端跳转到MasterA上去执行
- 这样的状态控制保证了同一个key在迁移之前总是在源节点上执行,迁移后总是在目标节点上执行,防止出现两边同时写导致的冲突问题。而且迁移过程中新增的key一定会在目标节点上执行,源节点也不会新增key,使整个迁移过程既能对外正常提供服务,又能在一定的时间点完成slot的迁移
Redis Cluster不采用把key直接映射到实例,而采用哈希槽的原因:
- 整个集群存储key数据量无法预估,当key数量非常多时,直接记录每个key对应实例映射关系,这个映射表会非常大,无论该表存储在服务端还是客户端都会占用大量内存
- Redis Cluster采用无中心化模式,客户端在某个节点访问一个key,如果key不在这个节点上,这个节点需要有纠正客户端路由到正确节点的能力,这就需要节点之间互相交换路由表,每个节点拥有整个集群完整的路由关系,如果存储映射关系,节点间交换信息也会非常庞大,消耗过多网络资源,并且每个节点都需要额外存储其他节点的路由表,内存占用过大浪费资源
- 当集群在扩容、缩容、数据均衡时,节点之间会发生数据迁移,迁移时需要修改每个key的映射关系,维护成本高。
- 在中间增加一层哈希槽,可以把数据和节点解耦, key通过Hashit算,只需要关心映射到了哪个哈希槽,然后再通过哈希槽和节点的映射表找到节点,相当于消耗了很少的CPU资源,不但让数据分布更均匀,还可以让这个映射表变得很小,利于客户端和服务端保存,节点之间交换信息时也变得轻量。
- 当集群在扩容、缩容、数据均衡时,节点之间的操作例如数据迁移,都以哈希槽为基本单位进行操作,简化了节点扩容、缩容的难度,便于集群的维护和管理。
Redis Cluster集群管理问题:
- 该机器方案就是为了解决单个节点数据量大,写入量大产生的性能瓶颈问题。多个节点组成集群,提高集群性能和可靠性,随之而来的就是集群的管理问题,核心就有请求路由、数据迁移。
- 请求路由:一般都是采用哈希槽的映射关系表找到指定节点,然后在这个节点上操作的方案。Redis Cluster在每个节点记录完整的映射关系(便于纠正客户端的错误路由请求),同时也发给客户端让客户端缓存一份,便于客户端直接找到指定节点,客户端与服务端配合完成数据的路由,这需要业务在使用Redis Cluster时,必须升级为集群版的SDK才支持客户端和服务端的协议交互。
- 其他Redis集群化方案例如Twemproxy,Codis都是中心化模式(增加Proxy层) ,客户端通过Proxy对整个集群进行操作,Proxy后面可以挂N多个Redis实例,Proxy层维护了路由的转发逻辑。操作Proxy就像是操作一个普通Redis一样,客户端也不需要更换SDK,而Redis Cluster是把这些路由逻辑做在了SDK中。当然,增加一层Proxy也会带来一定的性能损耗。
- 数据迁移:当集群节点不足以支撑业务需求时,就需要扩容节点,扩容就意味着节点之间的数据需要做迁移,而迁移过程中是否会影响到业务,这也是判定一个集群方案是否成熟的标准。Twemproxy不支持在线扩容,它只解决了请求路由的问题,扩容时需要停机做数据重新分配。而Redis Cluster和Codis都做到了在线扩容(不影响业务或对业务的影响非常小) ,重点就是在数据迁移过程中,客户端对于正在迁移的key进行操作时,集群如何处理?还要保证响应正确的结果?
- Redis Cluster和Codis都需要服务端和客户端/Proxy层互相配合,迁移过程中,服务端针对正在迁移的key,需要让客户端或Proxy去新节点访问(重定向),这个过程就是为了保证业务在访问这些key时依旧不受影响,而且可以得到正确的结果。由于重定向的存在,所以这个期间的访问延迟会变大。等迁移完成之后,Redis Cluster每个节点会更新路由映射表,同时也会让客户端感知到,更新客户端缓存。Codis会在Proxy层更新路由表,客户端在整个过程中无感知。
- 除了访问正确的节点之外,数据迁移过程中还需要解决异常情况(迁移超时、迁移失败)、性能问题(如何让数据迁移更快、bigkey如何处理) ,这个过程中的细节也很多。Redis Cluster的数据迁移是同步的,迁移一个key会同时阻塞源节点和目标节点,迁移过程中会有性能问题。而Codis提供了异步迁移数据的方案,迁移速度更快,对性能影响最小,当然,实现方案也比较复杂
实例通信方法对集群规模的影响:
Cluster的实例规模上限是1k,因为实例多后,实例间通信会增大。当集群实例超过一定规模,集群吞吐量反而会下降。Cluster运行时,每个实例都会保存Slot和实例对应关系,及自身状态信息。实例间按照Gossip协议通信,来让集群中每个实例知道其他所有实例的状态信息。
Gossip协议原理:
- 实例间按照一定频率,从集群中随机挑选一些实例,把PING消息发送给这些实例,检测是否在线并交换彼此的状态信息。PING消息中封装了发送消息的实例自身的状态信息、部分其他实例的状态信息,及Slot映射表。
- 一个实例接收PING消息后,会给发送PING消息的实例,发送PONG消息。PONG消息包含内容与PING一致。
Gossip保证一段时间后,集群中每个实例都能获取其他所有实例的状态信息,即是有新节点加入、节点故障、Slot变更等,实例间通过PING/PONG就可以完成实例间的信息同步。
实例间通信开销受到消息大小和通信频率影响,每个实例发送一个Gossip消息,除了自身状态信息,默认会传递集群十分之一实例的状态信息,当集群规模增大,消息的大小也会变大。随着集群规模增加,心跳消息数量越来越多,会占据一部分集群的网络通信带宽,降低集群服务正常客户端请求的吞吐量。
实例间通信频率:
- Cluter实例启动后,默认每秒从本地实例表随机选5个实例,再找出其中最久没有通信的实例,发送PING消息。这里随机选择可能存在有些实例一直没有被发送PING消息,导致它们维护的集群状态过期,为了避免该情况,Cluster实例会按100ms一次的频率,扫描本地实例列表,发现有实例最近一次接受PONG消息的实际,已大于配置项(cluster-node-timeout)的一半,就给该实例发送PING消息,更新该实例上的集群状态信息。
- 当集群规模扩大后,因为网络拥塞或不同服务器间的流量竞争,会导致实例间的网络通信延迟增加,如果有部分实例无法收到其他实例发送的PONG消息,会引起实例间频繁的发送PING消息,为集群网络通信带来额外开销。
- 可以通过cluster-node-timeout去定义集群实例被判断为故障的心跳超时时间,如果该值过小,会频繁出现PONG消息接收超时,从而导致实例间PING消息发送频繁。如果设置过大,当实例真发生了故障,需要等到这个时长后,才能检测出故障,导致实际故障恢复时间被延长,影响集群服务正常使用。适当设置大小。
Codis:Codis使用codis proxy直接和客户端连接,codis proxy是和单实例客户端兼容的。而和集群相关的管理工作(例如请求转发、数据迁移等),都由codis proxy,codis dashboard这些组件来完成,不需要客户端参与。这样一来,业务应用使用Codis集群时,就不用修改客户端了,可以复用和单实例连接的客户端,既能利用集群读写大容量数据,又避免了修改客户端增加复杂的操作逻辑,保证了业务代码的稳定性和兼容性。

数据分片机制:
在Codis集群中,数据保存在哪个server上,通过逻辑槽(Slot)映射来完成的。
- Codis集群共1024个Slot,从0到1023。可以手动分配给server,也可用dashboard进行自动分配
- 客户端要读写数据时,使用CRC32算法计算key的哈希值,然后对1024取模,得到Slot的编号。然后根据Slot跟Server的关系,就可以知道数据在哪个Server上。
Slot和Server的映射关系叫路由表,在dashboard分配好后,路由表会发给proxy并缓存在proxy本地,同时保存路由表在zk集群中。proxy收到客户端请求,查询本地路由表,就可以完成正常请求转发。
Codis的分片与Cluster分片的差异:
- Codis的路由表由dashboard分配和修改,并保存在zk集群。一旦数据位置变化,路由表被修改了。dashboard会把修改后的路由表发送给proxy,proxy根据最新路由表进行请求转发
- 在cluster中,路由表是通过每个实例间相互通信传递,在每个实例保存一份。当路由信息发生变化,需要在所有实例间通过网络消息进行传递,如果实例数量较多,就会消耗较多的集群网络资源。
集群扩容和数据迁移:Codis集群扩容有,增加Server和增加proxy。
增加Server:启动新的server,将它加入集群,把部分数据迁移到新的server
迁移过程:
- 在源Server上,Codis从要迁移的Slot中随机选择一个数据,发送给目的Server
- 目的Server确认收到后,返回确认消息,源Server会在本地将刚迁移的数据删除
重复上面过程,直到要迁移的Slot中数据迁移完成。该过程中数据从源server发送到目的Server是同步的,源Server在该阶段阻塞,无法处理新的请求操作。如果迁移数据是big key,源server就会阻塞较长时间。为此Codis实现了异步迁移,上述发送数据过程,源Server可不会阻塞,仍可以处理新请求。目的server收到并保存本地后,会发送ACK,标识迁移完成,源Server进行数据删除。该过程中,迁移的数据会被设为只读,保证不出现数据不一致的问题。对于big key,异步迁移采用拆分指令方式,对big key中每个元素,用一条指令进行迁移,而不是把整个big key进行序列化后再整体运输,避免big key迁移,因为要序列化大量数据导致阻塞源Server。
同时bigkey迁移未完成的阶段,如果Codis故障导致bigkey的数据同时存在源Server和目的Server,破坏迁移原子性。所以Codis会在目标Server上给bigkey设置临时过期时间,发生上述情况,目标Server上的key会在过期后删除,不影响迁移原子性。当完成正常迁移,bigkey元素的临时过期时间会被删除。
增加proxy,当客户端增加一个proxy无法支撑大量请求操作时进行扩容。字节启动proxy,通过dashboard将proxy加入集群,zk上会保存最新的proxy访问列表,客户端读取后,把请求发送给新增的proxy。
集群可靠性保证:
- Codis Server:主从集群、哨兵机制。多个Server group,每个group中是一主多从的server。数据分布加入了group的粒度,proxy转发请求,按照数据所在Slot和group的关系,写请求发送到group主库,读请求发送到group主库或从库。
- proxy与zk:proxy信息来自于zk,zk集群只要半数节点存在就能继续服务,保证了数据的可靠性。proxy故障只需要重启,重启后的proxy通过dashboard从zk集群获取路由表,继续进行请求转发工作
- Codis和Cluster的选择:
- 从稳定性和成熟度来看,Codis应用得比较早,在业界已经有了成熟的生产部署。虽然Codis引入了proxy和Zookeeper,增加了集群复杂度。但是,proxy的无状态设计和Zookeeper自身的稳定性,也给Codis的稳定使用提供了保证。而Redis Cluster的推出时间晚于Codis,相对来说,成熟度要弱于Codis,如果你想选择一个成熟稳定的方案, Codis更加合适些。
- 从业务应用客户端兼容性来看,连接单实例的客户端可以直接连接codis proxy,而原本连接单实例的客户端要想连接Redis Cluster的话,就需要开发新功能。所以,如果你的业务应用中大量使用了单实例的客户端,而现在想应用切片集群的话,建议你选择Codis,这样可以避免修改业务应用中的客户端。
- 从使用Redis新命令和新特性来看,Codis server是基于开源的Redis 3.2.8开发的,所以,Codis并不支持Redis后续的开源版本中的新增命令和数据类型。另外,Codis并没有实现开源Redis版本的所有命令,比如BITOP,BLPOP,BRPOP,以及和与事务相关的MUTLI,EXEC等命令。Codis官网上列出了不被支持的命令列表,你在使用时记得去核查一下。所以,如果你想使用开源Redis版本的新特性,Redis Cluster是一个合适的选择。
- 从数据迁移性能维度来看,Codis能支持异步迁移,异步迁移对集群处理正常请求的性能影响要比使用同步迁移的小。所以,如果你在应用集群时,数据迁移比较频繁的话,Codis是个更合适的选择。
本文来自博客园,作者:难得,转载请注明原文链接:https://www.cnblogs.com/zhangbLearn/p/18829285

浙公网安备 33010602011771号