vlambda博客
学习文章列表

深入了解Redis分布式集群架构


这一篇我带大家一起来了解下面试最常被问到的问题:高可用Redis分布式集群的应用,在搞清楚这些之前我们肯定要知道它的原理性的东西,这样才能根据它的特性设计出优秀的集群架构,遇到棘手问题时才有突破口。本篇文章我们会先从基础原理讲到架构演进,可能概念较多内容较长,请耐心阅读谢谢。

Redis怎么做到数据不丢的?

虽然它是一个内存数据库,但是也不影响他对数据完整性的支持,这就有赖于它的持久化技术:

  • 快照:是全量备份,在存储上是非常紧凑的二进制内存数据。我知道Redis是单线程的,所以它在持久化的化的时候会调用操作系统的rdbSave函数fork一个子进程将当前内存中非过期数据序列化(需要注意的是rdbSave函数有两个指令实现,SAVE 和 BGSAVE两个,前者为阻塞指令,后者为非阻塞,既我当前所说的),父进程将不受快照操作的阻塞,这也就是操作系统鼎鼎大名的:COW(Copy On Write)机制。这个机制我们作为一个题外话插入,拓展一下大家的知识储备,面试的时候能把这个讲清楚也是一个加分项:
  1. COW:子进程刚产生的时候它和主进程共享内存里面的数据段跟代码段,Linux为了节省内存资源,让他们共享起来,在进程分离的一瞬,内存基本没变化。进程分离的步骤大致是:fork函数会在子进程同时返回,在父进程里面返回子进程的pid,在子进程里返回0,如果系统内存资源不足则返回-1,fork失败。子进程做数据持久化不会修改现有内存数据,只对数据进行遍历然后序列化到磁盘。这时候有人就会问了:子进程读父进程的数据,主进程此时还对外提供服务,那么主进程肯定会不停的修改内存数据,子进程怎么保证自己读到的数据跟主进程的最新数据一致呢?这时候我们就得引入系统的COW机制了,COW将数据段分成N个数据页,当主进程修改任意一个页面数据的时候,将会把此页面从共享内存中复制一份分离出来,然后对复制出来的新页面进行修改,此时子进程的内存页是没有变化的。这就是快照的概念了,当fork成功的一瞬间一直到它持久化到磁盘的数据始终是一致的。当下次子线程进入的时候就会共享到那些被修改过的页面数据了(主进程未修改过的+页分离数据)。一般情况下页分离的数量取决于Redis的热数据的多少。
  2. RDB文件结构:一条RDB文件包含以下内容 ,| REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM |。REDIS,标识着这是一个Redis的RDB文件;RDB-VERSION,为一个四字节的整数表示,不同RDB-VERSION版本的文件是不兼容的;DB-DATA,它会在RDB文件中出现多次,每个DB-DATA部分保存着一个非空数据库的所有数据;SELECT-DB,表示键值对应的数据库号码,确保数据被还原到正确的数据库;OPTIONAL-EXPIRE-TIME 域是可选的,如果键没有设置过期时间那么这个域就不会出现;TYPE-OF-VALUE域记录着VALUE域的值所使用的编码;VALUE域则是根据值的实际数据结构保存的格式。
  • AOF日志:是增量备份,是内存数据修改的指令文本记录,它跟RDB有一个区别就是包含过期key,当key被惰性删除或者定期删除会在文件中追加一条del指令。AOF在长期运行中会比较庞大,定期会重写,而且备份回放AOF比快照回放慢得多。这里需要注意的一点是:Redis在收到客户端指令时会先将指令存储到AOF日志文件中,然后再执行指令。这样好处就是如果发生宕机已经持久化到AOF中的指令可以通过回放来恢复数据,防止数据意外丢失。当AOF随着时间越来越大的时候就会需要对其重写:Redis提供了bgrewriteaof指令对AOF进行重写,其原理就是开辟一个子线程对当前内存进行遍历转化成一个新的文本指令,生成一个新的AOF文件保存在操作系统cache中,操作系统再异步将操作系统cache中的数据写到磁盘,生成真正的AOF文件。生成新AOF文件结束后将在这期间发生的增量指令追加到AOF文件中,追加完成后立即使用最新AOF文件删除旧AOF文件。在上述过程中会存在一个问题,当Redis的AOF写入操作系统cache成功后未来得及刷盘就宕机的话,这部分数据是会丢失的。下面就引入了解决此问题的办法:操作系统函数fsync,它可以将操作系统cache中指定内容强制刷新到磁盘中。只要调用fsync执行成功就能保证AOF文件不丢失。只要是IO操作,那必然跟内存操作存在量级的性能差异,所以fsync的频率(fsync三种方式,AOF_FSYNC_NO:不保存;AOF_FSYNC_EVERYSEC:每一秒钟保存一次;AOF_FSYNC_ALWAYS:每执行一个命令保存一次)也是影响Redis性能的一个指标,我们要根据自己系统的特性在保证高性能的同时尽量少的丢失数据。我们一般为了提升主节点的性能可能会不开启fsync,这个工作交给从节点,但是这一前提是要做好容错和监控处理,我们可以通过冗余设计或者更高级的分布式设计减少错误概率,保证至少要保证一个从节点的数据同步正常。

Redis数据恢复

重启Redis时,rdbLoad函数就会被执行,它将读取RDB文件,并将RDB文件数据载入到内存中。但是我们很少用选择某一时间的RDB快照方式来恢复数据,因为这样会丢失很多数据,较好的选择就是通过AOF日志来恢复数据,但是回放AOF日志文件是相对很耗时的一个操作。我们可以采用手动RDB+AOF的方式恢复:选某一时间点的RDB文件恢复数据后,跟RDB最后生成时间通过运维工具去修改AOF日志文件,选择RDB文件时间之后的AOF指令保存为新的AOF文件回放(为了保证数据的完整性,一般会选择RDB时间前一点点)。到了Redis4.0它给运维同学带来了解放双手的混合持久化模式,它将RDB文件和增量的AOF日志存放在一起,这时候AOF不在是全量日志,而是自这次持久化开始到持久化结束时间发生的增量日志。这样就很大程度的提高了数据恢复的速度也减少了手工运维的烦恼。

提示:发布与订阅功能将不受数据载入的影响。

Redis主从同步

  • 增量同步:Redis主节点会将自己存储在Buffer中的操作指令异步同步给从库,从节点收到同步成功指令后会像主节点上报自己同步到文件偏移量。因为Redis主库的Buffer使用的是环形数组数据存储结构,如果Buffer满了会从数组的头部开始覆盖写入,如果主从延迟过大,就会存在Buffer中的写入速度大于同步速度而导致指令丢失的可能。
  • 快照同步:为了解决增量同步主从延迟数据丢失的问题引入了快照同步方式,这个过程较消耗性能。当发现增量同步有丢失数据的风险时,主库会fork一个子进程对主库做一次当前内存快照备份发送给从库。从节点接收到主库的RDB备份后,释放掉当前所有的数据,让回放RDB文件。回放完成后通知主库再进行增量同步。在整个快照过程中主节点的Buffer还在不停的前移,如果复制回放rdb文件的时间比Buffer前移的更慢的话,就会导致在RDB同步的过程中Buffer中未同步的指令又被覆盖则rdb同步失败,这样就会导致主库再次进入快照过程中,将新的Rdb文件同步给从库。这样就陷入一个死循环中,我们在平时设计中要考虑到主从延迟导致的Buffer堆积空间大小,给定一个包含网络延迟导致的Buffer堆积的合理大小,做到不低估,不浪费。
  • 无盘复制:我上面说到过一般为了提升主节点性能我们会将日志文件刷盘的操作交给从库来操作。所谓无盘复制就是主节点通过Socket将快照发生到从节点,主节点一边遍历内存一边发送RDB文件内容,从节点将收到的完整的RDB文件存储到磁盘,再进行回放。
  • 同步复制:Redis的主从复制大部分都是异步的,想要保证数据的强一致性,我们可以使用Redis提供的wait指令进行同步复制,假如将wait的等待时间设置为无限等待从库同步完成,那么当网络发生分区或者延迟较高的时候,就出现严重阻塞,影响Redis的可用性。

过期key的的主从同步:需要注意的一点是在主从同步过程中对于过期key处理是不同的。有一条原则就是:过期key统一由主节点删除。主节点在删除一条key时会显示的向所有从节点发送一条del指令,从节点在自己的内存遇到过期key时只需要向客户端返回过期,不做删除动作,等待主节点同步del来删除。

Redis Sentinel

RedisCluster

在上面解决了集群的健壮性后,那么随之探索的就是数据的存储的性能上,在集群模式下都是通过全量数据冗余来保证数据的一致性与可用性,在空间上造成了巨大的浪费。这一节我们将介绍Redis的分布式集群存储方式RedisCluster,它的单个节点上不再是全量数据,而只含有整个集群的一份数据。这样既改善了存储空间浪费的问题,同时也增横向增加了Redis服务整体的吞吐性。RedisCluster将所有数据存储区域划分为16384个slots(槽位),每个节点负责一部分槽位,槽位的信息存储于每个节点中。当客户端请求进来时候会拉去一份槽位信息列表缓存在本地,RedisCluster的每个节点会将集群的配置信息持久化到自己的配置文件中,所以需要引入一套可维护的配置文件管理方案,尽量做到自动化。

  • 槽位算法:RedisCluster 默认会根据key使用crc32算法进行hash得到一个整数,然后用这个整数对16384取模定位key所在的槽位。它还运行用户在key字符串里面嵌入tag将key强制写入指定的槽位。
  • 迁移:当有新的节点加入或者断开节点时,就会触发Redis槽位迁移。当一个槽位正在迁移时候在原节点的状态为migrating,在目标节点的状态为importing。原节点的单个key执行dump指令得到序列化内容,再向目标节点发送restore携带序列化内容作为参数的指令,目标节点接收到内容后反序列化复制到内存中,响应给原节点成功。原节点收到成功响应后把当前节点的key删掉就完成了节点数据迁移。这个过程是一个同步的操作,在复制完成之前原节点时处于阻塞状态的,不会进入新的数据,直到原节点的key被删除完成。如果key内容过大就会导致迁移阻塞时间过长,出现卡顿现象,所以再次强调大key的危害。上面说完了在迁移过程中服务端的变化,现在我们来说一下槽位迁移对于客户端的变化:这时候新旧节点会同时存在部分key,客户端访问到旧节点,如果旧节点存在就正常处理返回。如果客户端访问的数据不在旧节点,它会向客户端发生一个重定向指令(-ASK targetNodeAddr),客户端收到重定向后,先去目标节点执行一个不带参数的asking指令,然后在目标节点执行操作。因为在没有完全迁移完槽位目标节点还不归新节点管理,如果只适合直接发生操作指令,目标节点会返回给客户端一个-MOVED重定向指令,让它去原节点执行,这样就出现了重定向循环。不带参的asking指令目的就是打开目标节点选项让它当做自己的槽位的请求来处理。通过上面的过程得知在迁移过程中,平时的一个指令需要三个ttl才能完成。
  • 跳转:当RedisCluster发生槽位变化的数据迁移时,这时候客户端保存的槽位信息就和RedisCluster的槽位信息不一致,当客户端访问到错误的槽位时候,当前槽位会相应给客户端一个可能包含此数据的槽位信息,当客户端访问成功后更新本地槽位信息。
  • 容错:RedisCluster为每个主节点设置了若干从节点,主节点故障时,集群会主动提升某个从节点作为主节点,当无主节点时Redis整个不可用。也可以通过 cluster-require-full-coverage参数设置允许部分节点故障,其他节点依然可以对外提供服务。实际在异常无处不在生成环境中突然部分节点变得不可用,间隔一会又突然好了是很常见的时候,为了解决这问题我们也通过设置容忍最大离线时间(cluster-node-timeout)来避免,当超过这个最大超时时间则认为节点不可用。还有一个作为被乘数系数来放大超时时间的参数:cluster-slave-validity-factor,当值为0的时候是不能容忍异常短暂离线,系数越大相对越宽松。RedisCluster作为一个去中心化的中间件,一个节点认为某个节点离线,叫可能离线,当所有或者当大多数节点认为某节点离线才叫真正离线,此时集群会剔除此节点或者触发主从切换。它是用Gossip协议来广播自己的状态以及对整个集群变化的感知。比如一个节点发现某节点离线,它会将这个信息向整个集群广播,其他节点也会收到这个信息,如果收到信息的节点发现目标节点状态正常则不更新这条信息,并发送目标节点正常的消息给整个集群,就这样一个个节点的传播消息。当整个集群半数以上节点都持有某目标节点已离线的的信息时候才认为,某目标节点离线的这一事实,否则不予处理。

结尾

在此我只是粗略的将Redis从单机到集群,再到分布式集群的一些常见概念作出解释,大家有兴趣可以根据某一个点深入了解,或者在留言区域一起讨论学习。喜欢我的文章的话请帮忙转发谢谢。