这是《Redis设计与实现》第三部分--多机数据数据库的实现。
15 复制
在多机情况下,从服务器要与主服务器保持数据一致,就需要进行数据复制。在Redis2.8之前Redis复制策略使用是全量复制,从2.8版本之后,则采用部分复制的策略进行数据复制。
15.1 旧版复制功能的实现
旧版的复制功能分为同步( sync)和命令传播( command propagate )两个操作。所谓的同步操作则是全量的将主服务器的数据复制到从服务器,已达到主从一致。一般用在主从服务器连接时使用。命令传播操作则用于当主服务器的数据库被修改,通过命令传播也修改从服务器的数据,从而保证主从服务器一致。
对于同步操作,主从服务器做了以下操作。
虽然同步操作可以保持主从服务器一致,但当主服务器执行了一个写命令之后,这种一致状态就会被打破。对于这种情况,则需要用到命令传播。对于命令传播,主要操作为主服务器会将自己执行的写命令,也就是造成主从服务器不一致的那条命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。
15.2 旧版复制功能的缺陷
旧版复制功能缺陷主要集中在同步操作的场景。同步操作会发送在两个场景。初次复制和断线后重复制。对于初次复制,旧版的复制功能可以很好的完成。但对于断线后重复制,同步操作则会显得效率很低。毕竟只需要复制断线之前增量的写操作即可。
初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同。断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重新连上了主服务器,并继续复制主服务器。
15.3 新版复制功能的实现
新版的复制功能将同步操作分为完整重同步(full resynchronization )和部分重同步(partial resynchronization)。
完整重同步和旧版的复制功能一样,用于处理初次复制情况。部分重同步则是用于断线后重复制的情况,如果条件允许,主服务器可以将断开期间执行的写命令发送给从服务器,从服务器只需要执行这些写命令就可以更新至语主服务器一样的状态。
15.4 部分重同步的实现
部分重同步的实现有三个关键点:1. 主/从服务器的复制偏移量 2. 主服务器的复制积压缓冲区,3.服务器的运行ID
对于复制偏移量,主服务器和从服务器都会维护一个复制偏移量,这个偏移量代表了同步传播了多少字节的数据。当主服务器和从服务器的复制偏移量一样时,说明数据一致。当从服务器断开重连时,只需要和主服务器比较偏移量就可以知道要同步哪些增量数据。
除此之前,主服务器也维护了一个复制积压缓冲区,它是一个先进先出固定大小的队列。每次有写命令时,除了将写命令传播到从服务器,也会将写命令存入缓冲区中,并记录每个命令的复制偏移量。
当从服务器重连主服务器时,从服务器会将复制偏移量发给主服务器,主数据若在缓冲器中找到此偏移量,则会执行部分重同步。若在缓冲区中没有找到,则会执行完整的同步操作。
对于服务器运行ID的使用,每个服务器都有自己的运行id,当从服务器初次连接主服务器时,主服务器会将自己的运行id发送给从服务器。当重连时,从服务器会将主服务器id发送给主服务器。当发送的id和主服务器id相同,则会继续尝试部分重同步。若两个id不同,则会直接执行完整的同步操作。
15.5 心跳检测
默认情况下,从服务器会以每秒一次的频率向主服务器发送命令:replconf ack <offset>
心跳检测有三个作用:
-
检测主从服务器的网络连接状态。
-
辅助实现min-slaves选项。
-
检测命令丢失。
主服务器中保存各个从服务器最后向主服务器发送心跳命令的时间。若主服务器超过1s没有收到从服务器发送的心跳命令,则会判定为连接出现了问题。
在redis中可以配置最少的从服务器数量(min-slaves-to-write = 3)和从服务器的延迟(min-slaves-max-lag=10),可以防止主服务器在不安全的情况下执行写命令。当从服务器的数量少于3个,或者三个从服务器的延迟( lag )值都大于或等于10秒时,主服务器将拒绝执行写命令,
此外通过心跳服务中的offset参数可以判定主服务器的命令传播是否存在丢失的情况,若存在的话,可以通过偏移量尝试部分重同步。
16 Sentinel--哨兵
Sentinel系统是一种特殊的redis服务,用来支持Redis 的高可用性( high availability)。其原理为:
由一个或多个Sentinel 实例( instance)组成的Sentinel系统( system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
主服务器下线执行故障转移操作
1. Sentinel系统会挑选server1属下的其中一个从服务器,并将这个被选中的从服务器升级为新的主服务器。
-
Sentinel系统会向server1属下的所有从服务器发送新的复制指令,让它们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。
-
Sentinel还会继续监视已下线的server1,并在它重新上线时,将它设置为新的主服务器的从服务器。
16.1 启动并初始化Sentinel
当一个Sentinel启动时,它需要执行
1)初始化服务器。
-
将普通Redis服务器使用的代码替换成Sentinel专用代码。
3)初始化Sentinel状态。
4)根据给定的配置文件,初始化 Sentinel的监视主服务器列表。
5)创建连向主服务器的网络连接。
16.1.1 初始化服务器
Sentinel相对于Redis服务器启动,不会有载入RDB文件或者AOF文件还原数据库状态。
16.1.2 使用Sentinel专用代码
16.1.3 初始化Sentinel状态
16.2 获取主服务器信息
Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。
Sentinel通过回复可以获取两方面的内容,一方面可以获取到主服务器本身的信息,如run_id等信息。另一方面也可以获取到主服务器下面的从服务器的信息。Sentinel根据返回来的内容更新主服务器的实例结构(在Sentinel初始化过程中会给每个主服务器创建一个实例结构),也会根据从服务器的信息更新实例结构中的从服务器字典。
16.3 获取从服务器信息
当Sentinel 发现主服务器有新的从服务器出现时,还会创建连接到从服务器的命令连接和订阅连接。在默认情况下,会以每十秒一次的频率通过命令连接向从服务器发送 INFO命令,根据回复更新从服务器实例信息。
16.4 订阅连接
当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送订阅命令。当有多个Sentinel监视同一个服务器时,一个Sentinel发送的信息会被其他Sentinel接收到。
当其他Sentinel接收到发送信息的Sentinel的数据后,其他Sentinel会对这条信息进行分析,提取出信息中的Sentinel IP地址,端口号等参数,然后更新主服务器字典中的其他Sentinel信息。
当Sentinel通过频道信息发现一个新的Sentinel时,它还会创建一个连向新Sentinel的命令连接,而新Sentinel也会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络。
16.5 检测服务器的下线状态
这里下线状态的主体是主服务器和从服务器(若是主服务器下线之后后续会还会涉及到故障转移的操作,即重新选举主服务器)。
下线状态分为主观下线和客观下线。主观下线更倾向于Sentinel认为主服务器或者从服务器下线。而客观下线则是真正物理意义上的下线。
对于主观下线,需要先在配置文件中配置一个主观下线所需要的时间长度。在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(主服务器,从服务器,其他Sentinel)发送ping命令,若连续返回无效回复(即除了pong,loading,masterdown三种以外的回复),则标识这个实例进入主观下线状态。
值得注意的时候,由于个主观下线所需要的时间长度是每个Sentinel单独配置的,所有不同的Sentinel判定主观下线的条件可能不同。
客观下线:当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。
16.6 选举领头Sentinel
Sentinel设置局部领头Sentinel的规则是先到先得。所有在线的Sentinel都有被选为领头 Sentinel的资格。
当Sentinel发现主服务器进人客观下线时,都可以要求其他Sentinel将自己设置为局部领头Sentinel。当一个Sentinel接收到另一个Sentinel成为领头Sentinel的请求时,就不再接收其他请求。
如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。如果在给定时限内,没有一个 Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel 为止。
16.7 故障转移
故障转移分三步
选出新的主服务器的过程可以分为两步:
- 过滤数据比较老的从服务器
- 删除下线或者断线状态的从服务器、
- 删除五秒内没有对主服务器回应的从服务器(主服务器会每隔1s向从服务器发送info命令)
- 删除与已下线主服务器连接
- 断开超过阈值的从服务器(阈值=判断主服务器下线所需的时间*10)
- 选出优先级最高的从服务器
- 先根据从服务器优先级slave-priority 排序,选择优先级最高。
- 根据赋复制偏移量排序,选择服务复制偏移量最大的(复制偏移量大说明与旧 master差距小)
- 选择运行ID 号最小的从库。 (根据 runID 的创建时间来判断);
17 集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片( sharding)来进行数据共享,并提供复制和故障转移功能。
17.1 节点
节点通过握手来将其他节点添加到自己所处的集群当中。一个Redis集群通常由多个节点(( node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中。可以通过某个节点node发送cluster meet <ip><port>来将node节点和相应节点相连接。
17.2 槽指派
Redis 集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0个或最多 16384 个槽。
当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。
一个节点除了会将自己负责处理的槽记录在 clusterNode 结构的 slots 属性和numslots 属性之外,它还会将自己的 slots 数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽我负责处理。因此,集群中的每个节点都会知道数据库中的 16384 个槽分别被指派给了集群中的哪些节点。
17.3 在集群中执行命令
17.4 重新分片
Redis 集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点 ),并且相关槽所属的键值对也会从源节点被移动到目标节点。
17.5 ASK错误
背景:
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时
17.6 复制与故障转移
集群中的每个节点都会定期地向集群中的其他节点发送 PING 消息,以此来检测对方是否在线,如果接收 PING 消息的节点没有在规定的时间内,向发送 PING 消息的节点返回 PONG 消息,那么发送 PING 消息的节点就会将接收 PING 消息的节点标记为疑似下线( probable fail,PFAIL )
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线那么这个主节点x将被标记为已下线(FAIL ),将主节点x标记为已下线的节点会向集群广播条关于主节点x的 FAIL 消息,所有收到这条 FAIL 消息的节点都会立即将主节点x标记为已下线。
故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
1) 从节点选出新的主节点
2)新的主节点会撤销所有对已下线的主节点的槽指派,并将这些槽全部指派给自己。
3)新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
4)新的主节点开始接收和自己负处理的槽有关的命令请求,故障转移完成。
17.7 消息
集群中的节点通过发送和接收消息来进行通信,常见的消息包括 MEET、PING、PONG、PUBLISH、FAIL 五种。