redis集群设计方案及原理


设计集群方案时,至少要考虑以下因素:
(1)高可用要求:根据故障转移的原理,至少需要3个主节点才能完成故障转移,且3个主节点不应在同一台物理机上;每个主节点至少需要1个从节点,且主从节点不应在一台物理机上;因此高可用集群至少包含6个节点。
(2)数据量和访问量:估算应用需要的数据量和总访问量(考虑业务发展,留有冗余),结合每个主节点的容量和能承受的访问量(可以通过benchmark得到较准确估计),计算需要的主节点数量。
(3)节点数量限制:Redis官方给出的节点数量限制为1000,主要是考虑节点间通信带来的消耗。在实际应用中应尽量避免大集群;如果节点数量不足以满足应用对Redis数据量和访问量的要求,可以考虑:

  a.业务分割,大集群分为多个小集群;

  b.减少不必要的数据;

  c.调整数据过期策略等。
(4)适度冗余:Redis可以在不影响集群服务的情况下增加节点,因此节点数量适当冗余即可,不用太大。

集群的原理:

集群最核心的功能是数据分区,因此首先介绍数据的分区规则;然后介绍集群实现的细节:通信机制和数据结构;最后以cluster meet(节点握手)、cluster addslots(槽分配)为例,说明节点是如何利用上述数据结构和通信机制实现集群命令的。

数据分区方案:
  数据分区有顺序分区、哈希分区等,其中哈希分区由于其天然的随机性,使用广泛;集群的分区方案便是哈希分区的一种。
  哈希分区的基本思路是:对数据的特征值(如key)进行哈希,然后根据哈希值决定数据落在哪个节点。常见的哈希分区包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。
  (1)哈希取余分区
    哈希取余分区思路非常简单:计算key的hash值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。
  (2)一致性哈希分区
    一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,范围为0-2^32-1;对于每个数据,根据key计算hash值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。

 

   与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。如果在node1和node2之间增加node5,则只有node2中的一部分数据会迁移到node5;如果去掉node2,则原node2中的数据只会迁移到node4中,只有node4会受影响。
   一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉node2,node4中的数据由总数据的1/4左右变为1/2左右,与其他节点相比负载过高。
  (3)带虚拟节点的一致性哈希分区
    该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis集群使用的便是该方案,其中的虚拟节点称为槽(slot)。槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,

   数据的映射关系由数据hash->实际节点,变成了数据hash->槽->实际节点。

    在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有4个实际节点,假设为其分配16个槽(0-15); 槽0-3位于node1,4-7位于node2,

     以此类推。如果此时删除node2,只需要将槽4-7重新分配即可,例如槽4-5分配给node1,槽6分配给node3,槽7分配给node4;可以看出删除node2后,数据在其他节点的分布仍然较为均衡。槽的数量一般远小于2^32,远大于实际节点的数量;

    在Redis集群中,槽的数量为16384

  下面这张图很好的总结了Redis集群将数据映射到实际节点的过程:

       

  (1)Redis对数据的特征值(一般是key)计算哈希值,使用的算法是CRC16。 Crc16(key) = hash
  (2)根据哈希值,计算数据属于哪个槽。 Hash % 16384
  (3)根据槽与节点的映射关系,计算数据属于哪个节点。

 

Redis集群搭建:

一、主/从(master/slave)(缺点: 数据冗余,浪费内存 (ping - pong)

  1.主节点读写,从节点只能读,不能写
  2.当主节点读写数据变化时,会直接同步到从节点
  3.一个master可以拥有多个slave,但是一个slave只能对应一个master

主从复制工作机制:
  当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照文件和命令后加载快照文件和缓存的执行命令。复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性。

主从配置:
  redis默认是主数据,所以master无需配置,我们只需要修改slave的配置即可。
  a.设置需要连接的master的ip端口:
    slaveof 192.168.0.107 6379
  b.如果master设置了密码。需要配置:
    masterauth
  c.连接成功进入命令行后,可以通过以下命令行查看连接该数据库的其他库信息:
    info replication

 

二、哨兵模式(sentinel)

  哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。(ping - pong)

  

  

这里的哨兵有两个作用:
  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
故障切换(failover)的过程:
  假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的

配置:

  配置Redis的主从服务器,修改redis.conf文件如下
  # 使得Redis服务器可以跨网络访问
  bind 0.0.0.0
  # 设置密码
  requirepass "123456"
  # 指定主服务器,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
  slaveof 192.168.11.128 6379
  # 主服务器密码,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
  masterauth 123456

  配置哨兵,在Redis安装目录下有一个sentinel.conf文件,copy一份进行修改
  # 禁止保护模式
  protected-mode no
  #配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
  sentinel monitor mymaster 192.168.11.128 6379 2
  # sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
  # sentinel auth-pass <master-name> <password>
  sentinel auth-pass mymaster 123456

启动:
  # 启动Redis服务器进程
  ./redis-server ../redis.conf
  # 启动哨兵进程
  ./redis-sentinel ../sentinel.conf

需要特别注意的是:

  客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得:即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者。选举的具体过程这里不做详细描述,一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。

  故障转移:选举出的领导者哨兵,开始进行故障转移操作,该操作大体可以分为3个步骤:

    • 在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。

    • 更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。

    • 将已经下线的主节点(即6379)设置为新的主节点的从节点,当6379重新上线后,它会成为新的主节点的从节点。

三、集群

集群,即Redis Cluster,是Redis 3.0开始引入的分布式存储方案。

集群由多个节点(Node)组成,Redis的数据分布在这些节点中。集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。

集群的作用,可以归纳为两点:

  1、数据分区:数据分区(或称数据分片)是集群最核心的功能。

集群将数据分散到多个节点,一方面突破了Redis单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。

Redis单机内存大小受限问题,在介绍持久化和主从复制时都有提及;例如,如果单机内存太大,bgsave和bgrewriteaof的fork操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……。

  2、高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似);当任一节点发生故障时,集群仍然可以对外提供服务。

集群的搭建:
  这一部分我们将搭建一个简单的集群:共6个节点,3主3从。方便起见:所有节点在同一台服务器上,以端口号进行区分;配置从简。3个主节点端口号:7000/7001/7002,对应的从节点端口号:8000/8001/8002。


集群的搭建有两种方式:

  (1)手动执行Redis命令,一步步完成搭建;

  (2)使用Ruby脚本搭建。二者搭建的原理是一样的,只是Ruby脚本将Redis命令进行了打包封装;在实际应用中推荐使用脚本方式,简单快捷不容易出错。下面分别介绍这两种方式。
集群的搭建可以分为四步:(1)启动节点:将节点以集群模式启动,此时节点是独立的,并没有建立联系;(2)节点握手:让独立的节点连成一个网络;(3)分配槽:将16384个槽分配给主节点;(4)指定主从关系:为从节点指定主节点。

第一种搭建: 手动
(1)启动节点
  集群节点的启动仍然是使用redis-server命令,但需要使用集群模式启动。下面是7000节点的配置文件(只列出了节点正常工作关键配置,其他配置(如开启AOF)可以参照单机节点进行):

  #redis-7000.conf
  port 7000
  cluster-enabled yes
  cluster-config-file "node-7000.conf"
  logfile "log-7000.log"
  dbfilename "dump-7000.rdb"
  daemonize yes
  其中的cluster-enabled和cluster-config-file是与集群相关的配置。

  cluster-enabled yes:Redis实例可以分为单机模式(standalone)和集群模式(cluster);cluster-enabled yes可以启动集群模式。在单机模式下启动的Redis实例,如果执行info server命令,可以发现redis_mode一项为standalone
  cluster-config-file:该参数指定了集群配置文件的位置。每个节点在运行过程中,会维护一份集群配置文件;每当集群信息发生变化时(如增减节点),集群内所有节点会将最新信息更新到该配置文件;当节点重启后,会重新读取该配置文件,获取集群信息,可以方便的重新加入到集群中。也就是说,当Redis节点以集群模式启动时,会首先寻找是否有集群配置文件,如果有则使用文件中的配置启动,如果没有,则初始化配置并将配置保存到文件中。集群配置文件由Redis节点维护,不需要人工修改。
编辑好配置文件后

使用redis-server命令启动该节点:
  redis-server redis-7000.conf
(2)节点握手
  节点启动以后是相互独立的,并不知道其他节点存在;需要进行节点握手,将独立的节点组成一个网络。
  节点握手使用cluster meet {ip} {port}命令实现
  例如在7000节点中执行cluster meet 192.168.72.128 7001,可以完成7000节点和7001节点的握手;注意ip使用的是局域网ip而不是localhost或127.0.0.1,
是为了其他机器上的节点或客户端也可以访问
同理,在7000节点中使用cluster meet命令,可以将所有节点加入到集群,完成节点握手:
  1 cluster meet 192.168.72.128 7002
  2 cluster meet 192.168.72.128 8000
  3 cluster meet 192.168.72.128 8001
  4 cluster meet 192.168.72.128 8002
(3)分配槽
  在Redis集群中,借助槽实现数据分区,具体原理后文会介绍。集群有16384个槽,槽是数据管理和迁移的基本单位。当数据库中的16384个槽都分配了节点时,集群处于上线状态(ok);如果有任意一个槽没有分配节点,则集群处于下线状态(fail)。
分配槽使用cluster addslots命令,执行下面的命令将槽(编号0-16383)全部分配完毕:
  1 redis-cli -p 7000 cluster addslots {0..5461}
  2 redis-cli -p 7001 cluster addslots {5462..10922}
  3 redis-cli -p 7002 cluster addslots {10923..16383}
(4)指定主从关系
集群中指定主从关系不再使用slaveof命令,而是使用cluster replicate命令;参数使用节点id。
通过cluster nodes获得几个主节点的节点id后,执行下面的命令为每个从节点指定主节点:
  1 redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
  2 redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
  3 redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1

第二种搭建: 脚本
  使用Ruby脚本搭建集群
  在{REDIS_HOME}/src目录下可以看到redis-trib.rb文件,这是一个Ruby脚本,可以实现自动化的集群搭建。
  (1)安装Ruby环境
  以Ubuntu为例,如下操作即可安装Ruby环境:
  1 apt-get install ruby #安装ruby环境
  2 gem install redis #gem是ruby的包管理工具,该命令可以安装ruby-redis依赖
  (2)启动节点
  与第一种方法中的“启动节点”完全相同。
  (3)搭建集群
  redis-trib.rb脚本提供了众多命令,其中create用于搭建集群,使用方法如下

  ./redis-trib.rb create --replicas 1 192.168.72.128:7000 192.168.72.128:7001 192.168.72.128:7002 192.168.72.128:8000 192.168.72.128:8001 192.168.72.128:8002

 

  其中:--replicas=1表示每个主节点有1个从节点;后面的多个{ip:port}表示节点地址,前面的做主节点,后面的做从节点。使用redis-trib.rb搭建集群时,要求节点不能包含任何槽和数据。

  执行创建命令后,脚本会给出创建集群的计划;计划包括哪些是主节点,哪些是从节点,以及如何分配槽

 

集群扩展:

1. 集群伸缩
  实践中常常需要对集群进行伸缩,如访问量增大时的扩容操作。Redis集群可以在不影响对外服务的情况下实现伸缩;伸缩的核心是槽迁移:修改槽与节点的对应关系,实现槽(即数据)在节点之间的移动。例如,如果槽均匀分布在集群的3个节点中,此时增加一个节点,则需要从3个节点中分别拿出一部分槽给新节点,从而实现槽在4个节点中的均匀分布。
  增加节点、
  假设要增加7003和8003节点,其中8003是7003的从节点;步骤如下:
  (1)启动节点:方法参见集群搭建
  (2)节点握手:可以使用cluster meet命令,但在生产环境中建议使用redis-trib.rb的add-node工具,其原理也是cluster meet,但它会先检查新节点是否已加入其它集群或者存在数据,避免加入到集群后带来混乱。
  1 redis-trib.rb add-node 192.168.72.128:7003 192.168.72.128 7000
  2 redis-trib.rb add-node 192.168.72.128:8003 192.168.72.128 7000
  (3)迁移槽:推荐使用redis-trib.rb的reshard工具实现。reshard自动化程度很高,只需要输入redis-trib.rb reshard ip:port (ip和port可以是集群中的任一节点),然后按照提示输入以下信息,槽迁移会自动完成:
    ○ 待迁移的槽数量:16384个槽均分给4个节点,每个节点4096个槽,因此待迁移槽数量为4096
    ○ 目标节点id:7003节点的id
    ○ 源节点的id:7000/7001/7002节点的id
  (4)指定主从关系:方法参见集群搭建
  减少节点、
  假设要下线7000/8000节点,可以分为两步:
  (1)迁移槽:使用reshard将7000节点中的槽均匀迁移到7001/7002/7003节点
  (2)下线节点:使用redis-trib.rb del-node工具;应先下线从节点再下线主节点,因为若主节点先下线,从节点会被指向其他主节点,造成不必要的全量复制。
  1 redis-trib.rb del-node 192.168.72.128:7001 {节点8000的id}
  2 redis-trib.rb del-node 192.168.72.128:7001 {节点7000的id}


2. 故障转移
  集群只实现了主节点的故障转移;从节点故障时只会被下线,不会进行故障转移。因此,使用集群时,应谨慎使用读写分离技术,因为从节点故障会导致读服务不可用,可用性变差。
这里不再详细介绍故障转移的细节,只对重要事项进行说明:
节点数量:在故障转移阶段,需要由主节点投票选出哪个从节点成为新的主节点;从节点选举胜出需要的票数为N/2+1;其中N为主节点数量(包括故障主节点),但故障主节点实际上不能投票。因此为了能够在故障发生时顺利选出从节点,集群中至少需要3个主节点(且部署在不同的物理机上)。
  故障转移时间:从主节点故障发生到完成转移,所需要的时间主要消耗在主观下线识别、主观下线传播、选举延迟等几个环节;具体时间与参数cluster-node-timeout有关,一般来说:
故障转移时间(毫秒) ≤ 1.5 * cluster-node-timeout + 1000
  cluster-node-timeout的默认值为15000ms(15s),因此故障转移时间会在20s量级


3. 集群的限制及应对方法
  由于集群中的数据分布在不同节点中,导致一些功能受限,包括:
  (1)key批量操作受限:例如mget、mset操作,只有当操作的key都位于一个槽时,才能进行。针对该问题,一种思路是在客户端记录槽与key的信息,每次针对特定槽执行mget/mset;另外一种思路是使用Hash Tag,将在下一小节介绍。
  (2)keys/flushall等操作:keys/flushall等操作可以在任一节点执行,但是结果只针对当前节点,例如keys操作只返回当前节点的所有键。针对该问题,可以在客户端使用cluster nodes获取所有节点信息,并对其中的所有主节点执行keys/flushall等操作。
  (3)事务/Lua脚本:集群支持事务及Lua脚本,但前提条件是所涉及的key必须在同一个节点。Hash Tag可以解决该问题。
  (4)数据库:单机Redis节点可以支持16个数据库,集群模式下只支持一个,即db0。
  (5)复制结构:只支持一层复制结构,不支持嵌套。


4. Hash Tag
Hash Tag原理是:当一个key包含 {} 的时候,不对整个key做hash,而仅对 {} 包括的字符串做hash。
Hash Tag可以让不同的key拥有相同的hash值,从而分配在同一个槽里;这样针对不同key的批量操作(mget/mset等),以及事务、Lua脚本等都可以支持。不过Hash Tag可能会带来数据分配不均的问题,这时需要:(1)调整不同节点中槽的数量,使数据分布尽量均匀;(2)避免对热点数据使用Hash Tag,导致请求分布不均。

下面是使用Hash Tag的一个例子;通过对product加Hash Tag,可以将所有产品信息放到同一个槽中,便于操作。

 

5. 参数优化
  cluster_node_timeout
  cluster_node_timeout参数在前面已经初步介绍;它的默认值是15s,影响包括:
  (1)影响PING消息接收节点的选择:值越大对延迟容忍度越高,选择的接收节点越少,可以降低带宽,但会降低收敛速度;应根据带宽情况和应用要求进行调整。
  (2)影响故障转移的判定和时间:值越大,越不容易误判,但完成转移消耗时间越长;应根据网络状况和应用要求进行调整。
  cluster-require-full-coverage
  前面提到,只有当16384个槽全部分配完毕时,集群才能上线。这样做是为了保证集群的完整性,但同时也带来了新的问题:当主节点发生故障而故障转移尚未完成,原主节点中的槽不在任何节点中,此时会集群处于下线状态,无法响应客户端的请求。
  cluster-require-full-coverage参数可以改变这一设定:如果设置为no,则当槽没有完全分配时,集群仍可以上线。参数默认值为yes,如果应用对可用性要求较高,可以修改为no,但需要自己保证槽全部分配。


6. redis-trib.rb
  redis-trib.rb提供了众多实用工具:创建集群、增减节点、槽迁移、检查完整性、数据重新平衡等;通过help命令可以查看详细信息。在实践中如果能使用redis-trib.rb工具则尽量使用,不但方便快捷,还可以大大降低出错概率。

 

缓存穿透
  缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为��,则不放进缓存。
  解决方案:如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒, 最大不超过5min

缓存雪崩
  缓存雪崩,是指在某一个时间段,缓存集中过期失效。
  产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
  解决方案:一般是采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源

缓存击穿
  缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
小编在做电商项目的时候,把这货就成为“爆款”。
  其实,大多数情况下这种爆款很难对数据库服务器造成压垮性的压力。达到这个级别的公司没有几家的。所以,务实主义的小编,对主打商品都是早早的做好了准备,让缓存永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。mutex key互斥锁

相关内容