Redis笔记(3)多数据库实现

1.前言

  本章介绍redis的三种多服务实现方式,尽可能简单明了总结一下。

2.复制

  复制也可以称为主从模式。假设有两个redis服务,一个在127.0.0.1:6379,一个在127.0.0.1:12345。我们登陆12345端口的redis,输入命令slaveof 127.0.0.1:6379就设置好了复制模式。此时6379就是主服务器,12345就是从服务器。

  复制模式下,主从服务器保存相同的数据,概念上称之为数据库状态一致。redis2.8版本前后复制模式有些不同。

2.1 旧版复制

  redis的复制功能分为两块:同步和命令传播。同步指的是将从服务器的数据库状态更新至主服务器当前所处的数据库状态,从与主保持一致。命令传播则用于主服务器的数据库状态被修改了,将命令传递给从服务器,保证数据库状态一致。

2.1.1 同步

  从服务器发送slaveof命令,要复制主服务器时,从服务器首先需要执行同步操作,即将从服务器的数据库状态更新至主服务器当前所处的数据库状态。同步操作是由从服务器对主服务器发送SYNC命令完成的,具体步骤如下:

  1.从服务器向主服务器发送SYNC命令

  2.主服务器接收到命令执行BGSAVE,生成一个RDB文件,使用一个缓冲区记录从现在开始执行的所有写命令(BGSAVE是fork了一个子进程,当前内存的副本,后续修改丢失,所以在BGSAVE之后的写操作要记录在缓冲区)

  3.BGSAVE执行完毕后,将RDB文件发送给从服务器,从服务器载入整个RDB文件,更新成主服务器一致的状态。

  4.BGSAVE执行之间的缓冲区写命令发送给从服务器,从服务器同步到当前的主服务器状态。

2.1.2 命令传播

  同步完成后,主服务器的数据库状态并不是保持不变的,随时会发生变化。在发生变化的时候,主服务器就需要对从服务器执行命令传播操作,会将写命令发送给从服务器,这样就又保持了一致的状态了。

2.1.3 旧版功能的缺陷

  上面的操作步骤简单明了,但是实际运用中就会产生一个问题。如果是一个新的从服务器没有复制过主服务器的,从零开始拷贝主服务器的内容没有太大问题,但是如果从服务器因为网络中断,丢失了主服务器的命令传播,导致再连上需要再进行一次同步,那无疑是糟糕的选项,意味着从零开始再来一次。

  SYNC命令又是一个非常消耗资源的操作,如果主服务器有大量的数据,因为网络中断重新拷贝数据那太糟糕了。新版的复制解决了这个问题。

2.2 新版复制

  redis2.8版本开始使用PSYNC命令取代了SYNC,该命令支持部分同步的能力,用于处理断线重连。当然也支持完整同步,这个和SYNC命令没有太大区别。

2.2.1 部分重同步实现

  部分重同步由三个部分构成:主从服务器的复制偏移量,主服务器的复制积压缓冲区,服务器的运行ID。

  复制偏移量:

    主从服务器都留存了一个复制偏移量,通过这个值可以知道复制到了什么地步。

    主服务器向从服务器发送N个字节的时候,自己的复制偏移量就+N

    从服务器接受到主服务器的N个字节的时候,从服务器的复制偏移量+N

    这样通过对比两者的复制偏移量就能够很清楚的知道是否状态一致了。

    从服务器断线重连后只需要发送自己的复制偏移量给主服务器,主服务就能够决定是否有新的数据发送给从服务器了,将丢失的数据发送给从服务器,增量操作而非全量。

  复制积压缓冲区:

    复制偏移量的方式是一个简单有效的处理手段,但是这里面存在一个问题,如果从服务器迟迟没有连上,这个时候主服务器产生了大量的数据,还要全部缓冲下来吗?这是一个致命的问题,可能导致内存爆炸。如果不缓冲下来,就存在缓冲区起始偏移量就超过了从服务器的丢失位置,从服务器无法更新丢失的数据。

    复制积压缓冲区是主服务器维护的一个固定长度先进先出队列,默认大小为1MB。这个结构的含义是大小固定,超过大小再放入数据,队列头的数据会被移除,比如大小为3的队列,放入hello,先存放了hel,l进来的时候挤掉了h变成了ell。主服务器进行命令传播的时候会将命令传递给从服务器,并写入这个复制积压缓冲区。

    从服务器连上的时候,会发送PSYNC命令将自己的偏移量发送给主服务器,主服务器会根据这个偏移量来决定执行哪种同步:

      如果从服务器的偏移量后面的数据都在主服务器中,主服务器执行部分重同步。

      如果不在,重连太慢,缓冲区被挤出数据,这个时候只能进行完整同步才能恢复数据了。

    就像上面说的,缓冲区如果挤掉了要从服务器要同步的偏移量,那么只能执行完整同步,这个就没有发挥部分同步的价值了,所以设置缓冲区大小是一个很重要的事情。大小可以通过second * write_size_per_second进行估算,为了安全起见可以增加至2倍。修改缓冲区的配置是reply-backlog-size。

  服务器运行ID:

    每个redis服务器都有自己的运行ID,由40个随机十六进制字符组成。从服务器初次复制主服务器时,主服务器会将自己的ID传递给从服务器,从服务器会保存这个值。当从服务器断线并重新连上一个主服务器时,从服务器会将之前保存的ID发送给现在连接的主服务器。如果ID相同则说明是同一台,可以执行部分重同步,相反地不同意味着连接的不是同一个主服务器,需要进行完整重同步操作。

2.2.2 PSYNC命令的实现

  PSYNC命令调用方法有两种:

    1.从服务器没有复制过主服务器,或者执行过slave no one命令重置。那么从服务器在开始新的复制时将发送PSYNC ? -1命令,请求完整重同步。

    2.复制过了再次同步时发送PSYNC <runid> <offset>命令,runid是上一次复制的主服务器运行ID,offset是当前从服务器的复制偏移量。

  主服务器应对PSYNC命令会返回三种可能:

    1.+FULLRESYNC <runid> <offset>表明是一个完整重同步,runid是主服务器得到id,offset是主服务器当前的偏移量,从服务器用此值作为初始值

    2.+CONTINUE,执行部分重同步,主服务器会将后续的数据发给从服务器。

    3.-ERR 表面主服务器版本低于2.8,不支持PSYNC命令,从服务器将向主服务器发送SYNC命令,执行完整重同步。

2.3 复制的完整过程

  1.设置主服务器的地址和端口

    SLAVEOF 127.0.0.1 6379

    从服务器首先会保存主服务器的ip和端口在redisServer的masterhost和masterport属性中。SLAVEOF是一个异步命令,保存成功后会返回一个ok给客户端,复制在这之后进行。

  2.建立套接字连接

    SLAVEOF执行之后,从服务器会创建连向主服务器的套接字连接。创建连接成功,就会关联一个专门用于处理复制工作的文件事件处理器,而主服务器接收到连接请求,会把其看成一个客户端,从服务器是主服务器的一个客户端,执行相关操作。

  3.发送PING命令

    套接字连接成功后,从服务器会首先发送一个PING命令给主服务器。该命令有两个作用:一是保证套接字读写状态正常,二是检测主服务器是否工作正常,能够正常返回应答。

    从服务器发送PING命令后可能遇到3种情况:

      主服务器返回了一个命令回复,但从服务器不能在规定时间内timeout,读取回复内容,意味着网络不佳,将会断开连接并重新创建连接。

      主服务器返回了一个错误,表面主服务不能处理从服务器的请求,从服务器断开并重新创建主服务器的套接字。比如主服务器在处理一个超时运行脚本,那么从服务器发送PING命令时,会接受到BUSY Redisis busy running...

      从服务器读取到PONG回复,表示连接正常,将继续执行复制工作。

  4.验证身份

    接收到PONG应答后,就要决定是否进行身份验证了。从服务器设置了masterauth选项就会发送AUTH命令。有以下几种情况:

      主服务器没有设置requirepass选项,从服务器也没设置masterauth,就不需要认证,命令正常执行。

      从服务器的AUTH与主服务器的requirepass相同,正常工作,不同返回invalid password

      主服务器设置了requirepass选项,从服务器没有设置masterauth,返回NOAUTH错误。此外,主服务器没设置,从服务器设置了返回no password is set。

    所有的错误都会让从服务器中止目前的复制工作,并从创建套接字开始重新执行复制,知道身份验证通过,或者从服务器放弃执行复制位置。

  5.发送端口信息

    身份验证之后,从服务器会发送REPLCONF listening-port <port-number>消息,将自己监听的端口告诉主服务器,主服务器会存入redisClient的slave_listening_port属性中。

    向主服务器执行INFO REPLICATION就能看见相关信息了。

  6.同步

    从服务器发送PSYNC命令,将数据更新至主服务器数据库当前的状态。执行同步之前,只有从服务器是主服务器的客户端,执行同步之后,从服务器与主服务器互为客户端。因为主服务器要将相关数据发送给从服务器执行,只有客户端才能执行命令

  7.命令传播

    完成同步后,主服务器会进入命令传播阶段,这时只要一直将自己执行的命令发送给从服务器,从服务器一直接收写命令就可以保证主从一致。

2.4 心跳检测

  在命令传播阶段,从服务器会以每秒1次的频率向主服务器发送命令REPLCONF ACK replication_offset,这个命令有3个作用:1.检测网络连接状态。2.辅助实现min-slaves选项。3.检测命令丢失。

  检测主从服务器的网络连接状态:

    主服务器可以通过发送接收REPLCONF ACK命令来检查两者之间的网络连接是否正常,如果超过1秒没有接收到命令,则认为从服务器的连接出现了问题。INFO replication命令在从服务器列表的lag一栏中,可以看见从服务器最后一次向主服务器发送REPLCONF ACK命令过去了多久。

  辅助实现min-slaves配置选项:

    redis有两个参数:

      min-slaves-to-write 3

      min-slaves-max-lag 10

    意味着从服务器小于3个,或者3个延迟都大于等于10秒,主服务器会拒绝执行写命令。

  检测命令丢失:

    如果网络故障,主服务器传播给从服务器的写命令在半路丢失,可以通过REPLCONF ACK命令感知到丢失,主服务器就会补发丢失的数据了。

    这个和部分重同步很相似,不同之处在于此时主服务器没有与从服务器断开连接,部分重同步是在从服务器断线重连时进行的。

2.5 适用范围

  主从复制的方式很显然适用了写少读多的情况,读写分离。使用上有所局限,并非是一个高可用的方案,主服务器挂掉会产生很多问题。

3. Sentinel(哨兵模式)

  上面说到复制模式的使用范围。Sentinel是redis提供的一个高可用解决方案:由一个或多个Sentinel实例组成的Sentinel系统监视多个主服务器,以及主服务器下的从服务器状态,并在主服务器下线时,将从服务器器升级为新的主服务器,由新的主服务器代替下线的主服务器继续处理命令请求。

   总体上来说,就是Sentinel系统监控所有节点的状态,在主服务器挂掉后,挑选一个从服务器升级为主服务器的工作,然后其它从服务器以这个新主服务器为主。就是这么一个工作完成复制模式在主节点挂掉了之后的尴尬局面。Sentinel多个实例也是为了避免一个Sentinel挂掉无法运行整个监控体系的尴尬局面。

3.1 启动初始化Sentinel

  redis-sentinel sentinel.conf 或者 redis-server sentinel.conf --sentinel

  当一个sentinel启动时,它需要执行以下步骤:

    1.初始化服务器

    2.将普通redis服务器使用的代码替换成sentinel专用代码

    3.初始化sentinel状态

    4.根据给定的配置文件,初始化sentinel的监视主服务器列表

    5.创建连向主服务器的网络连接。

  初始化服务器:

    sentinel是一个特殊的redis服务器,但是其不执行任何数据库键值对相关命令或者是加载RDB、AOF文件。其复制命令只针对sentinel系统内部使用,客户端不能使用。

  使用专用代码:

    因为特殊,所以不能使用普通redis的代码,会进行替换。比如普通使用redis.c/redisCommandTable作为服务器命令列表,sentinel使用sentinel.c/sentinelcmds。服务器没有载入不相关的命令。

  初始化sentinel状态:

    sentinelState中记录了以下内容,来维护sentinel运行:

      current_epoch 当前纪元,用于实现故障转移

      dict *master 保存了所有被这个sentinel监视的主服务器,键是主服务器的名字,字典的值指向了一个sentienlRedisInstance结构

      int tilt 是否进入了TILT模式

      int running_scripts 目前正在执行的脚本数量

      mstime_t tilt_start_time 进入TILT模式的时间

      mstime_t previous_time 最后一次执行时间处理器的时间

      list *scripts_queue 一个FIFO队列,包含了所有需要执行的用户脚本

    监控的主机列表sentinelRedisInstance的结构如下:

      int flags 标识值,记录了实例的类型,以及该实例的当前状态

      char *name 实例的名称,在配置文件中设置,从服务器的名称由sentinel设置,为ip:port

      char *runid 实例运行的id

      uint64_t config_epoch 配置纪元,用于实现故障转移

      sentinelAddr *addr 实例的地址,包括ip 端口

      mstime_t down_after_period 实例无响应多少毫秒后判断下线,主观下线

      int quorum 判断这个实例下线的支持投票数量,客观下线

      int parallel_syncs 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量

      mstime_t failover_timeout 刷新故障迁移状态的最大时限

    比如配置:

      sentinel monitor master1 127.0.0.1 6379 2(quorum)

      sentinel down-after-milliseconds master1 30000

      sentinel parallel-syncs master1 1

      sentinel failover-timeout master1 900000

  创建网络连接:

    Sentinel将对被监视的主服务器创建网络连接,成为其客户端,发送命令,并从命令回复中获取相关的信息。

    对于每个主服务器而言,sentinel会创建两个连向主服务器的异步网络连接:

      一个是命令连接,专门用于向主服务器发送命令,接收命令回复。

      一个是订阅连接,用于订阅主服务器的_sentinel_: hello频道。

    这么设计的原因在于,sentinel需要发送命令,所以需要一个命令连接。订阅连接在于,redis的发布订阅功能中,被发送的信息不会保存在redis服务器里面,如果客户端不在线,就会丢失这些信息,所以需要一个专门的订阅连接。sentinel会与多个redis实例连接,所以使用异步模式。

3.2 获取主服务器信息

  sentinel默认以每10秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过INFO命令的回复来获取主服务器的当前信息。

  可以获取以下两个方面的信息:

    1.主服务器本身的信息,包括run_id和role服务器角色

    2.主服务器属下所有从服务器的信息,ip、port等内容。

  sentinel会对这些信息创建或者更新。

3.3 获取从服务器信息

  sentinel发现新的从服务器的时候,除了会创建相应的结构之外,还会创建连接到从服务器的命令连接和订阅连接。创建命令连接之后,sentinel在默认情况下,会以每10秒一次的频率发送INFO命令。根据回复消息,sentinel会提取出以下信息:

  从服务器运行的ID,从服务器的role,主服务器的IP地址master_host和master_port,主服务器的连接状态master_link_status,从服务器的优先级slave_priority和从服务器的复制偏移量slave_repl_offset。

3.4 向主从服务器发送信息

  Sentinel会默认以2秒一次的频率,通过命令连接向所有的主从服务器发送以下格式的命令:

    PUBLISH _sentinel_: hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>”

  这条命令向服务器的_sentinel_:hello频道发送了一条消息,s_开头的是sentinel本身的信息,m_开头的是主服务器的信息。

3.5 接收来自主从服务器的频道信息

  sentinel会订阅频道,但是3.4中却又往频道里面推送了消息,这个设计可能会产生疑惑。但是实际上很好理解,因为sentinel系统中不一定只有一个sentinel实例啊。每个sentinel实例都会接收到其它sentinel的状态,如果判断是自己发的就丢弃信息,如果是其它sentinel发送的,就需要根据各个参数,对相应主服务器的实例结构进行更新了。

  sentinel通过频道信息发现一个新的sentinel时,它不仅会为新的sentinel在字典中创建相应的实例结构,还会创建一个连向新sentinel的命令连接,新的sentinel也会同样创建一个连上这个sentinel的命令连接,多个sentinel形成了相互连接的网络。使用命令连接可以发送命令请求进行信息交换。sentinel之间不会创建订阅连接,因为主从服务器的订阅频道已经足够用于sentinel相互之间进行发现了。命令连接足够用于进行通信了。

3.6 检测主观下线状态

  默认情况下以每秒一次的频率向所有与它创建了命令连接的实例(主从,sentinel)发送ping命令,并通过是否回复判断实例是否在线。有效回复有:+PONG、-LOADING、-MASTERDOWN。如果在设置的down-after-milliseconds毫秒内连续没有返回有效信息,判断为主观下线,flags打开SRI_S_DOWN标识。

  主观下线的设置不只是作用于该主服务器,其所有从服务器,以及所有监控该主服务器的sentinel都会用这个值作为判断标准。每个sentinel监控相同的主服务器配置的下线时长可能不一样,各自使用各自的判断标准即可。

3.7 检测客观下线状态

  当一个sentinel判断一个主服务器下线了,其会询问其它的sentinel是否下线,接收足够数量的下线判断后,就会判断成客观下线,开始执行故障转移。

  发送命令询问其它sentinel的判断:

    SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

  接收响应:

    <down_state> 判断结果,1下线 0 未下线

    <leader_runid> 可以是*或者目标sentinel局部领头的sentinel的运行ID,*表示用于检测主服务器下线状态,id用于选举领头的sentinel

    <leader_epoch> 目标sentinel的局部领头sentinel的配置纪元,用于选举领头的sentinel,仅在leader_runid不为*时有效

  不同的sentinel的配置不同,所以判断客观下线的票数也可能不同

3.8 选举领头sentinel

  当一个主服务器被判断成客观下线时,监视这个下线主服务器的sentinel就要选出一个领头的sentinel,由其对下线主服务器进行故障转移操作了。选择规则如下:

    1.所有的sentinel都有可能成为leader

    2.每次选举无论成功失败,epoch纪元都是+1

    3.在一个纪元内,所有sentinel都有一次将某个sentinel设置为局部领头的机会,一旦设置,本次纪元就不能修改

    4.每个sentinel都会要求其它sentinel设置自己为头

    5.当sentinel发送sentinel is-master-down-by-addr时发送的runid不为*表明发送的sentinel要求接收的sentinel选其为局部领头

    6.目标sentinel应答命令时,回复其当前选举的leader

    7.源sentinel接收到回复,会判断配置纪元是否相同,相同取出leader_runid,如果与自己一致,意味着目标sentinel选它作为leader节点

    8.如果某个源sentinel被半数以下的sentinel设置成局部领头sentinel,则其成为领头。因为半数以上,所以一个纪元只会出现一个leader

    9.一段时间内没有选出来,会再次进行选举,直到选出为止。

  简单通俗的说就是:若干个sentinel确定了主服务器进入了主观下线状态,向其它sentinel确认是否下线,一旦确定进入客观下线。确认的sentinel就会再次发送检测命令,不过这次会带上自己的id,其它sentinel接收到带有id的就会明白要选举了,如果没有leader就认其为leader,有leader就返回告诉它,有sentinel更早确认了这一事实,你来晚了。在一个配置纪元内只会有一个leader,所以票选过半之后就自动升级为leader,可以开始进行故障转移了。如果选不出来,就再进行一次。

  这里有一个问题就是,如何确保每个sentinel的纪元相同呢?不相同的话就无法判断是否是同一次选举了,比如A问C一次,B也问C一次,这两个sentinel问的怎么判断是同一次的。另外选不出来是怎么判断的?是通过判断是否有对主服务器进行下线处理吗?

  选举算法时Raft算法的领头选举方法的实现。这篇文章可以理解以下上面的疑问:这里

3.9 故障转移

  选出leader之后,领头的sentinel对下线的主服务器进行执行故障转移,主要有3个步骤:

    1.挑选一个从服务器,设置成主服务器

    2.让其余从服务器改为复制新的主服务器

    3.将下线的主服务器设置为新的主服务器的从服务器

  挑选主服务器:

    sentinel有所有的主服务器的从服务器列表,从中过滤掉:下线或者断线状态的从服务器,5秒内没有回复leader sentinel的INFO命令的从服务器,与已下线的主服务器断线超过down-after-milliseconds * 10的从服务器,保证剩余的从服务器没有过早与主服务器断线,数据较新。之后根据优先级排序,相同优先级的选复制偏移量最大的,表明数据最新,这个指标也相同选runid最小的。

    选出来后执行SLAVEOF no one。

    这个之后,sentinel会每秒发送一次INFO命令,观察role角色,如果变成master代表升级正常。

  修改从服务器的复制目标:

    向所有从服务器发送SLAVEOF 新主服务器地址完成。

  将旧的主服务器变为从服务器:

    旧的主服务器已下线,当前上线时,sentinel就会发送SLAVEOF命令,让其成为新的主服务器的从服务器。

4.集群

  哨兵模式可以看作是对复制模式的一种改进,让复制模式可以自动恢复正常工作。其算是解决了高可用性,但是还有一个很糟糕的问题没有解决,那就是资源消耗了。每个主从服务器里面的数据完全一致,有大量的冗余,这对数据量很大的应用而言使用哨兵模式就有些浪费资源了。

  redis集群是redis提供的分布式数据库方案,集群通过分片sharding来进行数据共享,并提供复制和故障转移功能。

4.1 节点

  一个redis集群由多个node组成,一开始每个node都是一个集群,相互独立,必须连接起来构成一个真正可用的工作集群。

  连接节点通过命令:CLUSTER MEET <ip> <port>完成

  假设有3个redis服务,7000,7001,7002:

  首先登陆7000查看状态 CLUSTER NODES,只会看见一个节点,现在将7001添加到7000集群中:CLUSTER MEET 127.0.0.1 7001

  再此查看节点状态 CLUSTER NODES,就可以看见2个节点了,7002添加方法相同。

  启动节点:

    一个节点就是在redis集群中运行的一台redis服务器,redis服务器会在启动时根据cluster-enabled配置选项是否为yes决定是否开启服务器的集群模式。

    节点会继续使用单机模式中使用的服务器组件,比如文件事件处理器,serverCron函数,保存键值对,RDB持久化或者AOF持久化模块,PUBLISH等命令,复制模块来进行节点的复制。

  集群数据结构:

    每个节点都会创建集群中所有节点的clusterNode的结构:

      mstime_t ctime 创建节点的时间

      char name[REDIS_CLUSTER_NAMELEN] 节点的名称,由40个十六进制字符组成

      int flags 节点标识,角色或者状态

      unit64_t configEpoch 节点当前配置的纪元,用于实现故障转移

      char ip[REDIS_IP_STR_LEN] 节点的ip地址

      clusterLink *link 保存连接节点所需的有关信息

    clusterLink结果如下:

      mstime_t ctime 连接创建的时间

      int fd TCP套接字描述符

      sds sndbuf 输出缓冲区,保存着等待发送给其他节点的消息

      sds rcvbuf 输入缓冲区,保存着从其他节点接收到的消息

      struct clusterNode *node 与这个连接相关联的节点

  CLUSTER MEET命令的实现:

    通过节点A发送该命令,将节点B添加进来。收到命令的节点A将与节点B进行握手,确认彼此的存在。

    节点A会为节点B创建一个clusterNode结构,添加到自己的clusterState.nodes字典里面,之后根据命令给的IP端口向B发送meet消息。节点B接收到消息,创建一个A的clusterNode结构,同样添加到自己的clusterState.nodes字典。节点B返回一条PONG消息。节点A接收到PONG消息,发送一条PING消息,节点B接收到PING消息,握手成功。

4.2 槽指派

  redis集群通过分片的方式保存数据库的键值对:整个数据库被分为16384个槽slot。每个键都在这些槽之中,每个节点最多处理0~16384个槽。所有的槽都有节点处理,集群就处于上线状态,否则,处于下线状态。

  7000服务上执行CLUSTER INFO可以看见当前集群状态,这就是因为没有分配槽。

  执行CLUSTER ADDSLOTS <slot> [slot...]可以分配槽,比如CLSUTER ADDSLOTS 0 1 2 .. 5000

  所有的槽分配完成之后,集群就处于上线状态了。

  记录节点的槽指派信息:

    clusterNode的slots属性和numslots记录了节点负责处理哪些槽:

      unsigned char slots[16384/8]

      int numslots

    slots数组在索引i上的二进制为1,那么节点负责处理槽i。比如slot[0]为11111111则代表节点处理0~7槽。取出和设置操作的复杂度都是O(1)

  传播节点的槽指派信息:

    一个节点除了记录自己的槽位信息,也会将其发送给其他的节点。当接收到其他槽位信息的时候,就会更新相关结构。

  记录集群所有槽的指派信息:

    clusterState中有个clusterNode *slot[16384]指向一个数据结构,不为NULL就是指派了节点。这样可以很快确定某个槽由哪个节点处理。上面clusterNode中的slots信息也是有必要的,这个在传播槽位指派信息中会很方便。

  CLUSTER ADDSLOTS命令的实现:

    遍历所有的设置槽位,如果有一个被指派了节点,返回错误信息。只能为未分配的操作分配节点。

    设置槽位信息。设置完毕后通知其他节点。

4.3 集群中执行命令

  集群上线后,就可以执行命令了。客户端向节点发送命令,节点会计算出命令要处理的数据库是哪个槽的,并检查该槽指派的节点,如果是自己,直接执行返回。如果是其他节点,返回一个MOVED命令,指引客户端转到正确的节点,并再次发送执行命令。

  计算键所属的槽:

    CRC16(key) & 16383 用于计算键key的CRC16校验和,再缩小到0~16383之间

    使用CLUSTER KEYSLOT ”xxx" 查看键属于哪个槽

  判断槽是否是当前节点处理:

    检查自己的clusterState.slots数组,如果是自己就执行,不是返回MOVED错误

  MOVED错误:

    格式为 MOVED <slot> <ip>:<port>

    redis-cli -c -p 7000集群模式redis-cli中不会打印MOVED错误信息,使用redis-cli -p 7000单机模式的redis-cli客户端就可以打印出来

  节点数据库的实现:

    与之前的单机数据库实现完全相同,唯一的区别在于只能使用0号数据库

4.4 重新分片

  redis可以在线进行槽位的重新指派,这个过程可以继续处理命令请求。 比如添加一个7003节点。将原本的15001~16383指派给节点7003。

  重新分片的原理如下:

    通过集群管理软件redis-trib负责执行,向源节点和目标节点发送命令来进行重新分片操作。

    1.对目标节点发送CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,让目标节点准备好从源节点导入槽slot的键值对

    2.对源节点发送CLUSTER SETSLOT <slot> MIGRATING <target_id>命令,让源节点准备好将属于槽slot的键值对迁移到目标节点

    3.向源节点发送CLUSTER GETKEYSINSLOT <slot> <count>命令,获得最多count个属于槽slot的键值对的键名

    4.对于3获取的键名,redis-trib向源节点发送一个MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>命令,将被选中的键原子地从源节点迁移至目标节点

    5.重复3,4步骤,直到迁移完成。

    6.向任一节点发送CLUSTER SETSLOT <slot> NODE <target_id>,指派槽,会通过消息传至整个集群,最终所有节点都直到了。

4.5 ASK错误

  重新分片的过程中会导致一部分键在原节点,一部分在目标节点,客户发送请求的键正好处于迁移中的槽位时:源节点先检查键,找到就执行执行。没找到就可能存在迁移的节点了,源节点会返回一个ASK错误,指向正在导入的节点,再此发送之前的命令。和MOVED错误相似,不过一个是正常情况下没有命中,一个是在重新分片过程中没有命中。

  CLUSTER SETSLOT IMPORTING命令的实现:

    clusterState中的clusterNode *importing_slots_from[16384]的i下标不为null,意味着当前节点在从指向的节点导入槽i。

  CULSTER SETSLOT MIGRATING命令的实现:

    clusterState中的clusterNode *migrating_slots_to[16384]的i下标不为null,意味着当前节点正在将槽i导入给指向的节点。

  ASK错误:

    如果节点收到一个key,且该key在此节点上,该节点会尝试找key,找到了执行执行,没找到会检查migrating_slots_to[i]查看是否正在迁移,是就会返回ASK错误。

    客户端转向的时候会先发一个ASKING命令,再发送具体的命令。

  ASKING命令:

    接收到的唯一要做的就是打开客户端的REDIS_ASKING标识,下次接收到指定的命令时,会判断这个标识,是否执行命令,否则由于该槽位还没有指向该节点被拒绝执行。

4.6 复制与故障转移

  redis集群中的节点分为主节点和从节点,主节点用于处理槽,从节点用于复制某个主节点,在主节点下线时,选出一个从节点替代下线主节点继续处理命令请求。

  设置从节点:

    CLUSTER REPLICATE <node_id>

    可以让接收命令的节点成为node_id的从节点,并开始对主节点进行复制。

    接收到命令的节点会在clusterState.nodes中找到node_id的结构,将clusterState.myself.slaveof指针指向这个结构。之后修改标识myself.flags,表明自己是一个从节点。最后进行复制操作。这个过程会通知集群中所有的节点,有一个节点变成了从节点,并且正在复制。

  故障检测:

    集群中每个节点都会定期向集群中的其他节点发送PING消息,来检测对方是否在线。没有在规定时间内返回PONG消息,那么会被标记成疑似下线。集群各个节点会通过交换节点状态信息的方式,更新相关的下线标识。用个链表记录所以判断某个节点下线的节点。如果超过半数,这个节点就会被标记成下线,标记成下线的节点会广播给所有节点某个节点x下线了。之后所有的节点都直到了节点x被半数判断下线了。

  故障转移:

    当一个从节点发现自己正在复制的主节点进入了已下线的状态,从节点开始对下线主节点进行故障转移,具体执行步骤如下:

      选择一个从节点作为新的主节点。

      被选择的从节点执行SLAVEOF no one命令,成为新的主节点

      新的主节点会撤销所有对已下线主节点的槽指派,指派给自己

      广播一条PONG消息,让其他节点直到自己变成了主节点,并接管了下线主节点的槽。

      新的主节点开始处理相关槽请求命令。

  选择新的主节点:

    新的主节点是通过选举产生的,集群的配置纪元是一个自增计数器,初始值为0。当集群里的某个节点开始一次故障转移操作时,配置纪元就会被+1。每个配置纪元中,集群里的所有节点都有一次投票机会,而第一个向主节点要求投票的从节点将获得投票。

    从节点直到主节点下线后会广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求有投票权的主节点投票给他。

    其他主节点没有投票过,就会设置该从节点为主节点,并返回ACK消息。

    每个参选的从节点都会统计自己接收到多少个ACK,一个配置纪元内超过半数赞同即当选主节点。没有选出进入下一个配置纪元,重新投票。

4.7 消息

  集群中各个节点通过发送和接收消息来进行通信,消息主要有5种:

    MEET 添加一个节点进入集群

    PING  每秒随机选出5个节点,对这5个节点中最长时间没有发送过PING消息的节点发送消息,检测是否在线。此外,节点A接收到节点B发送的PONG消息时间,距当前时间已经超过了节点A的cluster-mode-timeout选项设置的时长一半,节点A也会向节点B发送PING消息

    PONG,接收到MEET和PING消息的应答消息。故障转移后从节点变主节点也广播PONG消息刷新其他节点对其的认知

    FAIL,一个节点判断另一个节点进入FAIL状态,广播给所有节点这个事实

    PUBLISH,节点执行这个命令,并广播一条PUBLISH,所有节点都会执行相同的命令。

  集群中各个节点通过Gossip协议交换各自关于不同节点的状态信息,Gossip协议由MEET、PING、PONG三种消息实现。

  Gossip协议要传递所有节点在节点数多的情况下会有一定的延迟,所以FAIL这种要尽快处理的消息不是这么实现的。

  PUBLISH命令为什么要通过一个节点广播给所有节点都去执行一下这个命令呢?而不直接向所有节点广播PUBLISH命令,实际上这样做更简单,但是不符合Redis集群的"各个节点通过发送和接收消息来进行通信”这一规则,所以节点没有采取广播PUBLISH命令的做法。

posted @ 2018-07-02 08:56  dark_saber  阅读(1473)  评论(0编辑  收藏  举报