本系列文章主要用户整理redis,分成4节:
本节为本系列第三篇
整理看的资料,将redis原理分成以下几个部分:
左侧部分主要是单点内容,右侧部分主要是集群内容。单点内容可分成了存储、控制,集群内容可以分成基于存储的数据同步以及搭建集群的3种控制方案。
本文是第三篇,主要介绍主辅同步以及redis的集群方案
主辅同步机制
单点宕机
假如我们现在只有一台作为缓存的Redis机器,通过持久化将热点数据写到磁盘,某时刻该Redis单点机器发生故障宕机,此期间缓存失效,主存储服务将承受所有的请求压力倍增,监控程序将宕机Redis机器拉起。
重启之后,该机器可以Load磁盘RDB数据进行快速恢复,恢复的时间取决于数据量的多少,一般秒级到分钟级不等,恢复完成保证之前的热点数据还在,这样存储系统的CacheMiss就会降低,有效降低了缓存击穿的影响。
主从架构
作为一个高可用的缓存系统单点宕机是不允许的,因此就出现了主从架构,对主节点的数据进行多个备份,如果主节点挂点,可以立刻切换状态最好的从节点为主节点,对外提供写服务,并且其他从节点向新主节点同步数据,确保整个Redis缓存系统的高可用。
如图展示了一个一主两从读写分离的Redis系统主节点故障迁移的过程,整个过程并没有停止正常工作,大大提高了系统的高可用:
主从复制
- 异步复制:当客户端向主结点写了hello world,主节点写成功之后就向客户端回复OK,这样主节点和客户端的交互就完成了,之后主节点向从结点同步hello world,从结点完成之后向主节点回复OK,整个过程客户端不需要等待从结点同步完成,因此整个过程是异步实现的。
- 同步复制:当客户端向主结点写了hello world,主节点向从结点同步hello world,从结点完成之后向主节点回复OK,之后主节点向客户端回复OK,整个过程客户端需要等待从结点同步完成,因此整个过程是同步实现的。
Redis选择异步复制可以避免客户端的等待,更符合现实要求,不过这个复制方式可以修改,根据自己需求而定吧。
从从复制
假如Redis高可用系统中有一主四从,如果四个从同时向主节点进行数据同步,主节点的压力会比较大,考虑到Redis的最终一致性,因此Redis后续推出了从从复制,从而将单层复制结构演进为多层复制结构,如下图:
全量复制
全量复制是从结点因为故障恢复或者新添加从结点时出现的初始化阶段的数据复制,这种复制是将主节点的数据全部同步到从结点来完成的,所以成本大但又不可避免。
Redis的全量复制过程主要分三个阶段:
- 快照阶段:从结点向主结点发起SYNC全量复制命令,主节点执行bgsave将内存中全部数据生成快照并发送给从结点,从结点释放旧内存载入并解析新快照,主节点同时将此阶段所产生的新的写命令存储到缓冲区。
- 缓冲阶段:主节点向从节点同步存储在缓冲区的操作命令,这部分命令主节点是bgsave之后到从结点载入快照这个时间段内的新增命令,需要记录要不然就出现数据丢失。
- 增量阶段:缓冲区同步完成之后,主节点正常向从结点同步增量操作命令,至此主从保持基本一致的步调。
考虑一个快照复制循环问题:
主节点执行bgsave是比较耗时且耗内存的操作,期间从结点也经历装载旧数据->释放内存->装载新数据的过程,内存先升后降再升的动态过程,从而知道无论主节点执行快照还是从结点装载数据都是需要时间和资源的。
抛开对性能的影响,试想如果主节点快照时间是1分钟,在期间有1w条新命令到来,这些新命令都将写到缓冲区,如果缓冲区比较小只有8k,那么在快照完成之后,主节点缓冲区也只有8k命令丢失了2k命令,那么此时从结点进行全量同步就缺失了数据,是一次错误的全量同步。
无奈之下,从结点会再次发起SYNC命令,从而陷入循环,因此缓冲区大小的设置很重要,如下图:
增量复制
增量复制是主从结点正常工作之后的每个时刻进行的数据复制方式,涓涓细流同步数据,这种同步方式又轻又快,优点确实不少,不过如果没有全量复制打下基础增量复制也没戏,所以二者不是矛盾存在而是相互依存的。
增量复制过程稍微简单一些,但是非常有用,试想复杂的网络环境下,并不是每次断开都无法恢复,如果每次断开恢复后就要进行全量复制,那岂不是要把主节点搞死,所以增量复制算是对复杂网络环境下数据复制过程的一个优化,允许一段时间的落后,最终追上就行。
增量复制是个典型的生产者-消费者模型,使用定长环形数组(队列)来实现,如果buffer满了那么新数据将覆盖老数据,因此从结点在复制数据的同时向主节点反馈自己的偏移量,从而确保数据不缺失。
这个过程非常好理解,kakfa这种MQ也是这样的,所以在合理设置buffer大小的前提下,理论上从的消费能力是大于主的生产能力的,大部分只有在网络断开时间过长时会出现buffer被覆盖,从结点消费滞后的情况,此时只能进行全量复制了。
redis集群方案
要支持集群首先要克服的就是分片问题,也就是一致性哈希问题,常见的方案有三种:
客户端分片:这种情况主要是类似于哈希取模的做法,当客户端对服务端的数量完全掌握和控制时,可以简单使用。
中间层分片:这种情况是在客户端和服务器端之间增加中间层,充当管理者和调度者,客户端的请求打向中间层,由中间层实现请求的转发和回收,当然中间层最重要的作用是对多台服务器的动态管理。
服务端分片:不使用中间层实现去中心化的管理模式,客户端直接向服务器中任意结点请求,如果被请求的Node没有所需数据,则像客户端回复MOVED,并告诉客户端所需数据的存储位置,这个过程实际上是客户端和服务端共同配合,进行请求重定向来完成的。
中间层分片
前面提到了变为N主N从可以有效提高处理能力和稳定性,但是这样就面临一致性哈希的问题,也就是动态扩缩容时的数据问题。
在Redis官方发布集群版本之前,业内有一些方案迫不及待要用起自研版本的Redis集群,其中包括国内豌豆荚的Codis、国外Twiter的twemproxy。
核心思想都是在多个Redis服务器和客户端Client中间增加分片层,由分片层来完成数据的一致性哈希和分片问题,每一家的做法有一定的区别,但是要解决的核心问题都是多台Redis场景下的扩缩容、故障转移、数据完整性、数据一致性、请求处理延时等问题。
Codis配合LVS等多种做法实现Redis集群的方案有很多都应用到生成环境中,表现都还不错
服务端分片(官方Gossip)
整体结构
官方版本实现了服务器层的Sharding分片技术,换句话说官方没有中间层,而是多个服务结点本身实现了分片,当然也可以认为实现sharding的这部分功能被融合到了Redis服务本身中,并没有单独的Sharding模块。
官方集群引入slot的概念进行数据分片,之后将数据slot分配到多个Master结点,Master结点再配置N个从结点,从而组成了多实例sharding版本的官方集群架构。
Redis Cluster 是一个可以在多个 Redis 节点之间进行数据共享的分布式集群,在服务端,通过节点之间的特殊协议进行通讯,这个特殊协议就充当了中间层的管理部分的通信协议,这个协议称作Gossip流言协议。
分布式系统一致性协议的目的就是为了解决集群中多结点状态通知的问题,是管理集群的基础,如图展示了基于Gossip协议的官方集群架构图:
Gossip协议已经在之前与Paxos、Craft一起做了分析,这里不再赘言。来看看redis如何使用gossip协议
节点的状态
Cluster中的每个节点都维护一份在自己看来当前整个集群的状态,主要包括:
- 当前集群状态
- 集群中各节点所负责的slots信息,及其migrate状态
- 集群中各节点的master-slave状态
- 集群中各节点的存活状态及不可达投票
信息同步
Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通信,集群的消息有以下几种类型:
- Meet 通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。
- Ping 节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。
- Pong 节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。
- Fail 节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。
由于去中心化和通信机制,Redis Cluster 选择了最终一致性和基本可用。例如当加入新节点时(meet),只有邀请节点和被邀请节点知道这件事,其余节点要等待 ping 消息一层一层扩散。
除了 Fail 是立即全网通知的,其他诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都需要等待被通知到,也就是Gossip协议是最终一致性的协议。
由于 gossip 协议对服务器时间的要求较高,否则时间戳不准确会影响节点判断消息的有效性。另外节点数量增多后的网络开销也会对服务器产生压力,同时结点数太多,意味着达到最终一致性的时间也相对变长,因此官方推荐最大节点数为1000左右。
如图展示了新加入结点服务器时的通信交互图:
基于Gossip协议的故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。
自己保存信息:当主节点A通过消息得知主节点B认为主节点D进入了疑似下线(PFAIL)状态时,主节点A会在自己的clusterState.nodes字典中找到主节点D所对应的clusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports链表中,并后续关于结点D疑似下线的状态通过Gossip协议通知其他节点。
一起裁定:如果集群里面,半数以上的主节点都将主节点D报告为疑似下线,那么主节点D将被标记为已下线(FAIL)状态,将主节点D标记为已下线的节点会向集群广播主节点D的FAIL消息,所有收到FAIL消息的节点都会立即更新nodes里面主节点D状态标记为已下线。
最终裁定:将 node 标记为 FAIL 需要满足以下两个条件:
- 有半数以上的主节点将 node 标记为 PFAIL 状态。
- 当前节点也将 node 标记为 PFAIL 状态。
也就是说当前节点发现其他结点疑似挂掉了,那么就写在自己的小本本上,等着通知给其他好基友,让他们自己也看看,最后又一半以上的好基友都认为那个节点挂了,并且那个节点自己也认为自己挂了,那么就是真的挂了,过程还是比较严谨的。
hash槽(slot)
前面说到了slot,这里来看看它。
一个 redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个数据都属于这16384个哈希槽中的一个。集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽。集群中的每一个节点负责处理一部分哈希槽。
slot返回有关哪个集群插槽映射到哪个redis实例的详细信息。该命令适用于redis集群客户端库实现,以便检索(或在收到重定向时更新)将集群散列槽与实际节点网络坐标(由ip地址和tcp端口组成)关联的映射,以便在接收到命令时,可以将其发送到命令中指定的键的正确实例。
Redis集群中的每个节点负责哈希槽的一个子集,例如,您可能有一个具有3个节点的集群,其中:
- 1、节点A包含从0到5460的散列槽。
- 2、节点B包含从5461到10922的散列槽。
- 3、节点C包含从10923到16383的散列槽。
这允许轻松地添加和删除集群中的节点。例如,如果我想添加一个新节点D,我需要将节点A,B,C中的一些散列槽移动到D。同样,如果我想从集群中删除节点A,我可以只移动由A使用的散列槽到B和C,当节点A将为空时,我可以将它从群集中彻底删除。
因为将散列槽从一个节点移动到另一个节点不需要停机操作,添加和移除节点或更改节点占用的散列槽的百分比也不需要任何停机时间。
master节点的slave节点不分配槽,只拥有读权限。但是注意在代码中redis cluster执行读写操作的都是master节点,并不是你想 的读是从节点,写是主节点。第一次新建redis cluster时,16384个槽是被master节点均匀分布的。