目录
1. 主从同步
1.1 定义
主从同步,当主节点Master挂掉的时候,从节点Slave接管服务,否则主节点需要经过数据恢复和重启的过程,这就可能会拖延很长的时间,从而影响线上业务的持续服务;
1.2 CAP原理
C:Consistent一致性
A:Availability可用性
P:Partition tolerance分区容忍性
分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景叫作网络分区;
在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操作将无法同步到另外一个节点,所以数据的 一致性将无法满足,因为两个分布式节点的数据不再保持一致。除非我们牺牲可用性,也就是暂停分布式节点服务,在网络分区发生时,不再提供修改数据的功能,直到网络状况完全恢复正常再继续对外提供服务;
总结为一句话:当网络分区发生时,一致性和可用性两难全;
1.3 Redis的最终一致
Redis主从数据同步是异步同步,所以分布式Redis并不满足一致性要求;
当客户端在Redis主节点修改数据后,主节点立即返回修改结果,即使在主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以Redis满足可用性;
Redis只是保证最终一致性,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态保持一致;如果网络断开,主从节点的数据将会出现大量不一致,一旦网络恢复,Redis从节点会采用多种策略努力追赶,尽力保持和主节点一致;
1.4 主从同步与从从同步
Redis同步支持主从同步、从从同步,从从同步功能是Redis后续版本增加的功能,以减轻主节点的同步负担;
1.5 增量同步
Redis同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存buffer中,然后异步将buffer中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一边向主节点反馈自己同步到哪里了(偏移量〉 。
因为内存的buffer是有限的,所以Redis主节点不能将所有的指令都记录在内存buffer中,Redis的复制内存buffer是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容;
因此,当网络状况不好,从节点在短时间内无法和主节点进行同步,那么当网络状况恢复后, Redis主节点中那些没有同步的指令在buffer中有可能已经被后续的指令覆盖掉了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到快照同步;
在Redis 2.8之前, 断线之后重连的从服务器总要执行一次完整重同步(full resynchronization)操作, 但是从 Redis 2.8 开始, 从服务器可以根据主服务器的情况来选择执行完整重同步还是部分重同步(partial resynchronization);
1.6 快照同步
快照同步大致流程如下:主节点先用bgsave,往磁盘生成文件,然后再通过网络IO把文件传输到从节点,从节点保存下来之后,再从磁盘加载到内存中,更别提在网络传输IO中出点网络问题,然后从节点加载的时候再出点小毛病,所以,快照同步是个非常耗费资源的操作;
下面详细说说快照同步过程:
首先需要在主节点上进行一次bgsave,将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点,从节点将快照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空,加载完毕后通知主节点继续进行增量同步;
在整个快照同步进行的过程中,主节点的复制buffer还在不停地往前移动,如果快照同步的时间过长或者复制buffer太小,都会导致同步期间的增量指令在复制buffer中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环,所以务必配置一个合适的复制 buffer大小参数,避免快照复制的死循环;
1.7 增加从节点
当从节点刚刚加入到集群时,它必须先进行一次快照同步,同步完成后再继续进行增量同步;
1.8 无盘复制
主节点在进行快照同步时,会进行很耗时的文件IO操作,在机械磁盘存储时,快照同步会对系统的负载产生较大影响。特别是当系统正在进行AOF的fsync操作时,如果发生快照同步,fsync将会被推迟执行,这就会严重影响主节点的服务效率;
从Redis2.8.18开始, Redis支持无盘复制。所谓无盘复制是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一边将序列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载;
1.9 wait指令
Redis的复制是异步进行的,但wait指令可以让异步复制变身同步复制,确保系统的强一致性(不能保证100%的强一致性〉,注意:Redis3.0以后才出现该指令;
wait提供两个参数:第一个参数是从节点的数量N,第二个参数是时间t,以毫秒为单位;两个参数的含义是:等待wait指令之前的所有写操作同步到N个从节点(也就是确保N个从节点的同步没有滞后),最多等待时间t;
如果时间t=O ,表示无限等待直至N个从节点同步完成;假设此时出现了网络分区,wait 指令第二个参数时间t=O,主从同步无法继续进行,wait指令会永远阻塞,Redis服务器将丧失可用性;
2. Redis Sentinel
2.1 定义
当主节点因为某种原因无法提供服务时,需要Redis自动进行主从切换,为此,Redis官方提供了一种方案:Redis Sentinel;
Sentinel负责持续监控主从节点的健康,当主节点挂掉时,自动选择个最优的从节点切换成为主节点。客户端来连接集群时,会首先连接Sentinel,通过Sentinel来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 Sentinel要地址,Sentinel会将最新的主节点地址告诉客户端。如此应用程序将无须重启即可自动完成节点切换;
2.2 消息丢失
由于Redis主从采用异步复制,意昧着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别多。 Sentinel无法保证消息完全不丢失,但是也能尽量保证消息少丢失;它有2个选项可以限制主从延迟过大:
①min-slaves-to-write 1表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,只提供读服务,主节点从而丧失可用性;
②min-slaves-max-lag 10表示主从复制是否异常,参数单位是秒,表示如果在10s内没有收到从节点的反馈,就意昧着从节点同步异常,要么是网络断开了,要么是一直没有给反馈;
3. Cluster
3.1 由来
在大数据高并发场景下,单个Redis实例往往会显得捉襟见肘,原因有下面几个:
①内存层面,如果单个Redis实例内存过大,快照RDB文件过大,然后主从同步时全量同步时间过长,且实例重启恢复时加载RDB文件也会消耗很长时间;
②单个Redis实例,无法高效的利用CPU,CPU的一个核心面对海量数据的读写操作,压力比较大;
为此,Redis集群方案应运而生,把多个小内存的Redis实例整合起来、将多台计算机上的CPU整合起来,一起完成完成海量数据存储和高并发读写操作;
3.2 特点
Redis Cluster将所有数据划分为16384个槽位,每个节点负责其中-部分槽位;
槽位的信息存储于每个节点中,当Redis Cluster客户端来连接集群时,也会得到一份集群的槽位配置信息,因此当客户端要查找某个key时,可以直接定位到目标节点;客户端为了可以直接定位某个具体的key所在的节点,需要缓存槽位相关信息,这样才可以准确快速地定位到相应的节点。同时因为可能会存在客户端与服务器存储槽位的信息不一致的情况,还需要纠正机制来实现槽位信息的校验调整;
Redis Cluster每个节点会将集群的配置信息持久化到配置文件中,所以必须确保配置文件是可写的,且尽量不要依靠人工修改配置文件;
3.3 槽位定位算法
Redis Cluster默认会对key值使用crcl6算法进行hash,得到-个整数值,然后用这个整数值对16 384进行取模来得到具体槽位。Redis Cluster 还允许用户强制把某个key挂在特定槽位上。通过在 key字符串里面嵌入tag标记,这就可以强制key所挂的槽位等于tag所在的槽位;
3.4 跳转
当客户端向个错误的节点发出了指令后,该节点会发现指令的 key所在的槽位并不归自己管理, 这时它会向客户端发送一个特殊的跳转指令MOVED,该指令携带目标操作的节点地址,告诉客户端去连接这个节点以获取数据。客户端在收到MOVED指令后,要立即纠正本地的槽位映射表。后续所有key将使用新的槽位映射表。
3.5 迁移
Redis Cluster提供了工具redis-trib可以让运维人员手动调整槽位的分配情况,该工具通过组合各种原生的Redis Cluster指令来实现;
Redis迁移的单位是槽,Redis一个槽一个槽的进行迁移,当一个槽正在迁移时,这个槽就处于中间过渡状态,这个槽在源节点的状态为migrating,在目标节点的状态为importing,表示数据正在从源节点流向目标节点,大致流程如下:从源节点获取内容→存到目标节点→从源节点删除内容,下面详细说明下过程:
①迁移工具redis-trib首先会在源节点和目标节点设置好中间过渡状态,把源节点对应的槽位标记为“migrating”,把目标节点对应的槽位标记为“importing”;
②迁移工具一次性获取源节点槽位的所有key列表,拿到key列表之后,针对每个key进行迁移;
③源节点对key执行dump指令从而得到序列化内容,然后向目标节点发送restore指令,该指令携带序列化的内容来作为指令参数;
④目标节点收到restore之后再进行反序列化就可以将内容恢复到目标节点的内存中,然后返回源节点OK,源节点收到OK后再把当前节点的key删除掉,这就是单个key迁移的全过程;
注意:
①Key迁移过程是同步的,在目标节点执行restore指令到源节点删除key之间,源节点的主线程会处于阻塞状态,直到key被成功删除;
如果迁移过程中突然出现网络故障,整个槽的迁移只进行了一半,这时两个节点依旧处于中间过渡状态,待下次迁移工具重新连上时,会提示用户继续进行迁移。
②在迁移过程中,如果每个key的内容都很小,migrate指令会执行得很快,它就不会影响客户端的正常访问。但是如果key的内容很大,因为migrate指令是阻塞指令,会同时导致源节点和目标节点卡顿,影响集群的稳定型,所以在集群环境下,业务逻辑要尽可能避免产生很大的key;
③在迁移过程中,源节点的迁移流程会发生变化:
当目标节点对应槽位上存在部分key数据时,源节点先尝试访问目标节点某一个key:
①如果该key对应的数据在目标节点里面,那么目标节点正常处理;
②如果对应的key不在目标节点里面,那么有2种可能:key只存在于源节点、目标节点是有该key的话但后来被删除了,那么这种情况下,目标节点不知道到底是哪个情况,如果是第一种情况则大大方方同步过来就行,如果是第二种情况,那相当于集群里数据不一致了,此刻,目标节点向源节点返回-ASK targetNodeAddr重定向指令;
当源节点收到ASK重定向指令后,先去目标节点执行一个不带任何参数的ASKING指令,然后在目标节点再重新执行原先的操作指令,那为什么需要执行一个不带参数的ASKING指令呢?
因为在迁移没有完成之前,按理说这个槽位不归源节点管理,如果这个时候向目标节点发送该槽位的指令,目标节点是不认的,目标节点会向源节点返回一个-MOVED重定向指令告诉它去源节点去执行。如此就会形成重定向循环。ASKING指令的目标就是打开目标节点的选项,告诉它下一条指令必须处理,而且是要当成自己的槽位来处理;
从以上过程可以看出,迁移是会影响服务效率的,同样的指令在正常情况下一个ttl就能完成,而在迁移情况下需要3个ttl才能搞定;
3.6 容错
Redis Cluster可以为每个主节点设置若干个从节点,当主节点发生故障时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态;
Redis也提供了一个参数clusterrequire-full-coverage可以允许部分节点发生故障,其他节点还可以继续提供对外访问;
3.7 网络抖动
为解决这种问题,Redis Cluster提供了一种选项 cluster-node-timeout,表示当某个节点持续timeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换;如果没有这个选项,网络抖动会导致主从频繁切换(数据的重新复制〉;
还有另外一个选项cluster-slave-validity-factor作为倍乘系数放大这个超时时间来宽松容错的紧急程度。如果这个系数为0,那么主从切换是不会抗拒网络抖动的;如果这个系数大于1,它就成了主从切换的松弛系数;
3.8 可能下线与确定下线
因为Redis Cluster是去中心化的,一个节点认为某个节点失联了并不代表所有的节点都认为它失联了,所以集群还得经过一次协商的过程,只有当大多数节点都认定某个节点失联了,集群才认为该节点需要进行主从切换来容错;
Redis集群节点采用Gossip协议来广播自己的状态以及改变对整个集群的认知 。比如一个节点发现某个节点失联了PFail(Possibly Fail) ,它会将这条信息向整个集群广播,其他节点就可以收到这点的失联信息。如果收到了某个节点失联的节点数量 (CPFail Count)已经达到了集群的大多数,就可以标记该失联节点为确定下线状态(Fail),然后向整个集群广播,强迫其他节点也接受该节点已经下线的事实,并立即对该失联节点进行主从切换;
3.9 槽位迁移感知
如果Cluster中某个槽位正在迁移或者已经迁移完毕,那么客户端如何能感知到槽位的变化呢?客户端保存了槽位和节点的映射关系表,它需要及时得到更新,才可以正常地将某条指令发到正确的节点中?
这里需要借用2个特殊的error指令:
①MOVED:用来纠正槽位的,如果我们将指令发送到了错误的节点,该节点发现对应的指令槽位不归自己管理,就会将目标节点的地址随同MOVED指令回复给客户端通知客户端去目标节点去访问。这个时候客户端就会刷新自己的槽位关系表,然后重试指令,后续所有打在该槽位的指令都会转到目标节点;
②ASKING:用来临时纠正槽位的,如果当前槽位正处于迁移中,指令会先被发送到槽位所在的源节点。如果源节点存在数据,那就直接返回结果了,如果不存在数据,那么数据可能真的不存在,也可能在目标节点上,所以源节点会通知客户端去目标节点尝试拿数据,看看目标节点有没有。这时源节点会给客户端返回一个asking error 指令并携带上目标节点的地址。客户端收到这个asking error后,就会去目标节点尝试。客户端不会刷新槽位映射关系表,因为它只是临时纠正该指令的槽位信息,不影响后续指令;
注意:可能存在多次重试的场景,例如:
一条指令被发送到错误的节点,这个节点会先给你一个MOVED错误告知你去另外一个节点重试,所以客户端就去另外一个节点重试了,结果刚好这个时候运维人员要对这个槽位进行迁移操作,于是给客户端回复了一个ASKING指令告知客户端去目标节点去重试指令。所以这种情形下,客户端重试了2次,而在某些特殊情况下,客户端甚至会重试多次,所以客户端的源码里在执行指令时都会有一个循环,然后会设置一个最大重试次数, 当重试次数超过这个值时,客户端会直接向业务层抛出异常;
3.10 集群变更感知
当服务器节点变更时,客户端应该立即得到通知以实时刷新自己的节点关系表,那么客户端是如何得到通知的呢?这要分为2种情况:
①目标节点挂掉了,客户端会抛出一个ConnectionError,紧接着会随机挑一个节点来重试,这时被重试的节点会通过MOVED指令告知目标槽位被分配到的新的节点地址。
②运维手动修改了集群信息,将主节点切换到其他节点,并将旧的主节点移除出集群。这时打在旧的主节点上的指令会收到一个 ClusterDown错误,告知当前节点所在集群不可用,这时客户端就会关闭所有的连接,清空槽位映射关系表,然后向上层抛错。待下条指令过来时,就会重新尝试初始化节点信息;