redis篇-集群篇

一、主从同步

1. 分布式原理

网络分区定义:分布式系统节点往往分布在不同的机器上,这意味着必然会有网络断开的风险,网络断开的场景称之为网络分区

cap原理:cap是分布式存储的理论基石,含义如下:

  • C:Consisten,一致性。写操作之后进行读操作,必须可以读到修改的最新数据
  • A:Availablity,可用性。只要接收到用户的信息,服务器就必须响应
  • P:Partition tolerance,分区容忍性。例子:M1和M2是两台服务器,系统设计时,应当考虑到M1消息有可能M2接收不到,因此分布式系统中,P总成立
  • 网络分区发生后,分布式节点间无法通信,一个节点的数据变更,无法同步到另一个节点,此时一致性无法满足(数据出现不一致),除非牺牲可用性,即网络分区发生时,不提供修改数据的功能,直到网络状况完全恢复后再对外提供服务。
  • 总而言之,网络分区发生时,一致性和可用性两难全。

2. redis分布式策略

a. 最终一致

redis分布式系统最终一致性原则:

  • redis主从数据是异步同步的,所以redis分布式系统并不满足C(一致性)要求。
  • redis分布式系统满足AP原则,在网络分区的情况下,主节点依旧可以正常对外提供服务
  • redis满足最终一致性:一旦网络恢复,从节点会采用多种策略努力追赶主节点,尽量与主节点数据保持一致。

b. 主从同步与从从同步

redis同步支持主从同步与从从同步,从从同步是redis后续版本新增的功能,减轻主节点的同步负担

c. 增量同步

增量同步过程:

  • redis同步的是指令流,主节点会把对数据状态产生影响的指令记录在本地buffer中,然后异步将buffer中的指令同步到从节点
  • 从节点一边执行同步的指令流来达到和主节点一样的状态,一边向主节点反馈自己同步到哪儿了
  • redis的复制内存buffer是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。因此当网络情况不好时,redis主节点中没有同步的指令在buffer中可能会被覆盖,从节点无法通过指令流来进行同步,因此需要使用快照同步

d. 快照同步

同步时机:

  • 当从节点刚刚加入主节点时,必须进行一次快照同步,同步完后再进行增量同步
  • 若增量同步的指令在主节点复制buffer中被覆盖,无法进行增量复制,则会发起快照同步

同步流程:

  • 首先在主节点上执行bgsave,将当前内存中数据全部快照到磁盘中,将快照文件的内容全部传送给从节点
  • 从节点将快照接收完毕,立即执行一次全量加载,加载前将内存数据清空,加载完毕后通知主节点继续进行增量同步
  • 整个同步过程中,主节点的复制buffer不断往前移动,若快照同步时间过长,会导致同步期间增量指令在buffer中被覆盖,导致快照同步后无法进行增量复制,会再次发起快照同步,极有可能陷入快照同步的死循环,因此需要设置一个合适的复制buffer大小参数

无盘复制:

快照同步的问题:主节点在进行快照同步时,会执行十分耗时的IO操作,对系统的负载产生较大的影响;特别是若系统正在执行AOF的fsync操作,若发生快照同步,fsync会被推迟执行,严重影响主节点的服务效率。

无盘复制流程:无盘复制在redis2.8.18后被提出,流程如下:

  • 主节点直接通过套接字将快照内容发给从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一边将序列化的内容发送给从节点
  • 从节点流程不变,先将接受到的内容存储到磁盘,再进行一次性加载

e. wait指令

wait指令可以让redis同步复制指令,确保系统的强一致性

127.0.0.1:6379> set key value
OK
127.0.0.1:6379> wait 1 0

wait提供两个参数,第一个是等待N个从节点将数据同步完成;第二个是最多等待时间T(t=0,表示无限等待,若主从同步无法继续进行,wait会永远阻塞,redis服务器丧失可用性) 

二、sentinel

1. 概述

定义:sentinel类似zookeeper集群,是集群高可用的心脏,负责监控主从节点的健康。当主节点挂了,会自动选择一个最优从节点进行切换。

流程:

  • 客户端连接集群时,会首先连接sentinel,通过sentinel查询主节点地址,然后再连接主节点进行数据交互;
  • 当主节点发生故障,客户端会重新向sentinel获取地址,sentinel会将最新主节点地址告诉客户端,如此应用程序无需重启也可以自动完成节点切换

           

从上图可以看到,主节点挂了,原来的主从复制也断开,客户端和损坏的主节点也断开了。一个从节点被提升为新的主节点,其他从节点开始和新的主节点建立复制关系。客户端通过新的主节点进行交互,sentinel集群会持续监控已经挂了的主节点,若恢复,则调整集群结构如下 

 

消息丢失:redis采用主从异步复制,这意味着若主节点挂点,从节点可能没有收到全部的消息,这部分数据会丢失;如果主从同步延迟很大,丢失的数据则很多。sentinel集群无法保证消息完全不丢失,但可以尽量保证消息丢失少,其策略如下:

  • min-salves-to-write K:表示主节点必须至少K个从节点在进行正常复制,否则就停止对外写服务,丧失可用性
  • min-slaves-max-log N:表示Ns内若没有收到从节点反馈,则意味着从节点同步不正常

通过这两个选项可以限制主从延迟过大问题

2. sentinel基本用法

a. 搭建简单的redis-sentinel集群

单台服务器搭建sentinel集群:

wget http://download.redis.io/releases/redis-3.0.7.tar.gz
tar -zxvf redis-3.0.7.tar.gz
cd redis-3.0.7
make
make PREFIX=/usr/local/software/redis/redis-3.0.7 install
ln -s /usr/local/software/redis/redis-3.0.7 /usr/local/redis
mkdir -p /usr/local/redis/conf
# 复制配置文件
cp sentinel.conf /usr/local/redis/conf/sentinel-26379.conf
cp sentinel.conf /usr/local/redis/conf/sentinel-26380.conf
cp sentinel.conf /usr/local/redis/conf/sentinel-26381.conf
# 使用vim修改配置文件的port即可,此步骤省略



nohup ./redis-sentinel  ../conf/sentinel-26379.conf > redis_sentinel1.log 2>&1 &
nohup ./redis-sentinel  ../conf/sentinel-26380.conf > redis_sentinel2.log 2>&1 &
nohup ./redis-sentinel  ../conf/sentinel-26381.conf > redis_sentinel3.log 2>&1 &

 单台服务器部署redis主从:

nohup ./redis-server --port 6379 > redis1.log 2>&1 &
nohup ./redis-server --port 6380 --slaveof 127.0.0.1 6379 > redis2.log 2>&1 &
nohup ./redis-server --port 6381 --slaveof 127.0.0.1 6379 > redis3.log 2>&1 &

 查看日志可见,redis哨兵模式已经搭建成功

kill掉主节点,发现sentinel重新选举了master

 b. 使用程序操作sentinel集群

使用方法:

  • sentinel对象的discover_xxx(master/slaves)可以发现主从地址,主地址只有一个,从地址可以有多个
  • xxx_for(master/slave)方法可以从连接池中拿出一个连接,因为从地址有多个,redis客户端对从地址采用轮询方案
>>> from redis.sentinel import Sentinel
>>> sentinel = Sentinel([('localhost',26379)],socket_timeout=0.1)
>>> sentinel.discover_master('mymaster')
('127.0.0.1', 6380)
>>> sentinel.discover_master('mymaster')
('127.0.0.1', 6380)
>>> master=sentinel.master_for('mymaster',socket_timeout=0.1)
>>> salve=sentinel.slave_for('mymaster',socket_timeout=0.1)
>>> master.set("name","zzh")
True
>>> salve.get("name")
b'zzh'

redis-py原理:

  • python连接池在建立新连接时,会查询主节点地址,然后跟内存中主节点地址进行对比,若发生变更,则断开所有连接,重新使用新地址建立连接
  • 如果旧的主节点挂掉了:所有正在使用的连接都会断开,重连时使用新的地址
  • 若sentinel主动进行主从切换,主节点没有挂掉,原先的主节点变成只读:redis-py在处理命令时捕获一个特殊的异常(ReadOnlyError),在这个异常将所有旧连接关闭,后续指令会进行重连。主从切换后,之前的主节点会降级成从节点,所有修改性指令会抛出ReadOnlyError,如果程序没有修改性指令,即使不切换,程序也不会出现问题

三、codis

1. 概述

问题:在大数据高并发场景下,单个redis实例往往捉襟见肘。问题如下:

  • 单个redis实例rdb不宜过大,否则会导致主从同步时间过长,实例重启恢复时间长
  • 单个redis实例只能利用单核CPU,单核CPU要完成海量数据存储,压力较大

codis定义:

  • codis使用go语言开发,是代理中间件,和redis一样使用redis协议对外提供服务。当客户端向codis发送指令时,codis负责将指令转发到后面的redis实例来执行,并将结果返回客户端

  • codis上挂接所有redis实例,构成一个redis集群,当集群空间不足时,可以动态增加redis实例实现扩容
  • 客户端操作codis和操作redis没有区别,甚至可以使用redis客户端sdk
  • codis是无状态的,只是一个转发代理中间件,因此可以启动多个codis实例,供客户端使用,每个codis节点是对等的。通过启动多个codis代理可以显著增加整体的QPS,还能起容灾功能,挂掉一个codis没关系,还有多个codis可以提供服务

2. 原理 

a. codis分片原理

分片原理:

  • codis默认将所有的key划分为1024个槽(slot),首先对客户端传过来的key进行crc32运算计算hash值,再将hash后的整数值对1024取模得到余数,这个余数就是对应的key的槽位
  • 每个槽位会唯一映射到后面多个redis实例之一,codis会在内存中维护槽位和redis实例的映射关系。如此以来,有了上面key对应的槽位,数据该往哪个redis实例转发就十分明显了

  • 槽位默认数量是1024,当集群节点较多时,可以将改值设置大一些

伪代码:

hash = crc32(command.key)
slot_hash = hash % 1024
redis = slots[slot_hash].redis
redis.do(command)

b. 不同codis实例之间槽位信息同步

问题:如果codis的槽位关系只存储在内存中,那么不同redis实例之间的槽位关系就无法同步。codis同步方案:codis使用zookeeper(etcd也支持)进行数据同步:

  • codis将槽位关系存储在zookeeper中,并提供dashboard观察和修改槽位信息
  • 当槽位关系变化时,codis proxy会监听到变化并同步槽位关系,从而实现多个codis proxy之间共享槽位信息

c. 扩容

场景:codis扩容时,需要对槽位关系进行调整,将部分槽位划分到新节点上,这意味着需要对这部分槽位对应的key进行迁移,迁移到新的redis节点

迁移key流程:

  • codis对redis进行改造,增加了slotsscan指令,可以遍历slot下所有key,然后挨个迁移每个key到新的redis节点
  • 迁移过程中,codis会接收到新的请求打到正在迁移的slot上。当codis接收到位于正在迁移的slot中的key后,会立即强制对当前key进行迁移,迁移完成后,再将请求转发到新的redis实例

伪代码:

slot_index = crc32(command.key) % 1024
if slot_index in migrating_slots:
    do_migrating_key(command.key)
    redis = slots[slot_index].new_redis
else:
    redis = slots[slot_index].redis
redis.do(command)

其他:redis支持的所有scan指令都无法避免重复,同样道理,slotsscan也无法避免重复,不过这部影响迁移,因为单个key被迁移后,旧实例就会将其彻底删除,也就不会再次被扫描出来

d. 自动均衡

codis提供了自动均衡功能,自动均衡会在系统比较空闲时,观察每个redis实例对应的slot数量,如果不平衡,就自动进行均衡

e. mget指令原理

mget可以批量获取多个key的值,这些key可能发布在不同slot上,codis的策略是将key按照所分配的实例打散分组,然后依次对每个实例调用mget,最后将结果汇总成一个,返回客户端

3. codis优缺点

a. codis代价

  • codis中所有的key分布在不同redis实例上,不能再支持事务了,事务只能在单个实例完成;rename操作也很危险,它的参数是两个key,如果两个key(根据hash值计算得到的slot值)在不同的redis实例中,rename操作无法正确完成
  • 为了支持扩容,单个key对应的value值不宜太大,因为集群迁移的最小单位是key,对应hash,会一次性使用hgetall拉取所有内容,然后用hmset放置到另一个节点,如果hash内部key/value太多,会导致迁移卡顿(官方建议不要超过1M)
  • codis增加了proxy作为中转层,网络开销比单个redis大,相当于数据包多走了一个网络节点,整体性能比单个redis有所下降(可通过增加proxy数量来弥补性能不足)
  • codis集群配置中心使用zookeeper来实现,增加了zookeeper运维成本

b. codis的优点

  • 设计上比redis cluser简单
  • 有强大的dashboard功能,能便捷的对redis集群进行管理。Codis-fe可以同时对多个codis集群进行管理

四、cluster

1. 概述

特点:

  • redis cluster是去中心化的,集群由多个几点组成,每个节点负责的数据多少可能不一样
  • 节点间通过一种特殊的二进制协议交互集群信息
  • cluster将所有数据划分成16384个槽,每个节点负责一部分槽位,槽位信息存储于每个节点中
  • 当redis cluster的客户端连接集群时,会得到一份集群槽位信息,当客户端要查询某个key时,可以直接定位到目标节点(不同于cluster,不需要使用proxy来定位目标节点)
  • 客户端缓存了cluster槽位信息,以准确快速的定位到相应节点。同时存在客户端与服务器存储槽位的信息不一致的问题,需要使用纠正机制来实现槽位信息的校验调整
  • cluster每个节点会将集群的配置信息持久化到配置文件中,因此必须确保配置文件可写

2. cluster原理

a. 槽位定位算法

算法流程:

  • cluster会使用crc16算法对key进行hash,再对16384取模,确定具体的槽(定位槽方法与codis类似)
  • cluster允许用户强制将某个key挂到特定槽位上。通过在key字符串里面嵌入tag标志,可以强制key所挂的槽位等于tag所在槽位(redis在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,若{}里面的字符串是相同的,则这些key可以被计算出相同的槽)

模拟代码:

package redis.clusterLearn;

import sun.misc.CRC16;

public class slotAlgorithm {
    public static void main(String[] args) {
        System.out.println(hashSlot("sdjk{wsl}}gft"));
        System.out.println(hashSlot("dsdf{wsl}}gfk"));
        System.out.println(hashSlot("sdfsa{wsl}d}ddgf"));
        System.out.println(hashSlot("dfkjljsd"));
    }

    public static int hashSlot(String key) {
        // 定位tag字符
        int loc = key.indexOf("{");
        if (loc >= 0) {
            int locEnd = key.indexOf("}", loc + 1);
            if (locEnd >= 0 && locEnd != loc) {
                key = key.substring(loc + 1, locEnd);
            }
        }
        // 定位槽
        CRC16 crc16 = new CRC16();
        for (byte byt : key.getBytes()) {
            crc16.update(byt);
        }
        return crc16.value & 0x3FFF;
    }
}

运行结果: 

由结果可见,若在key中使用{}指定tag,则redis cluster只会使用tag中的值定位slot

b. 跳转

流程:

  • 当客户端向一个错误节点发出指令后,该节点会发现指令的key所在的槽位不归自己管理,此时节点会向客户端发送一个特殊的跳转指令,携带目标操作的节点地址,告诉客户端去连接这个节点以获取数据
  • 客户端接收到消息后,要立即纠正本地槽位映射表,后续所有key的访问将使用新的槽位映射表

MOVED指令:

GET x
-MOVED 3999 127.0.0.1:6381

MOVED指令第一个参数是key对应的槽号,后面是目标节点的ip地址。MOVED指令前有一个-,代表返回的消息是一条错误消息(参照redis协议)

c. 迁移

概述:redis cluster提供redis-trib以让运维工作人员手动调整槽位分配情况

迁移特点:

  • redis的迁移单位是槽,一个槽一个槽的进行迁移
  • 当一个槽进行迁移时,这个槽就处于中间状态(在源节点状态为 migrating,目标节点状态为importing)
  • 迁移工具 redis-trib 首先会在源节点和目标节点之间设置好中间过渡状态,然后一次性获取源节点槽位中所有key列表(keysinslot指令,可以部分获取),再挨个key遍历
  • 迁移过程是同步的,目标节点执行restore指令到源节点删除key之间,源节点主线程处于阻塞状态,直到key被成功删除
  • 迁移过程中,如果每个key很小,migrate会执行得很快,如果key内容很大,由于migrate是阻塞指令,会同时导致源节点和目标节点卡顿(在集群环境下,业务逻辑要尽可能避免产生大key)

单个key迁移过程:

  1. 每个key迁移过程是将源节点作为“客户端”,源节点对当前的key执行dump指令得到序列化内容;
  2. 通过“客户端”向目标节点发送restore指令携带序列化内容作为参数;
  3. 目标节点再进行反序列化将内容恢复到目标节点的内存中,然后给“客户端”返回OK
  4. 源节点收到ok后,将当前节点的key删除掉,即可完成单个key 的迁移过程
  5. 迁移过程中,若出现网络故障,整个槽只迁移了一半,这时两个节点依旧处于中间状态,等下次迁移工具重新连上时,会提示用户继续进行迁移

迁移过程中,客户端访问流程:

  • 由于新旧节点对应的槽位都存在部分key,客户端可以先尝试访问旧节点,若对应的数据还在,则正常返回;
  • 若数据不存在于旧节点,要么数据在新节点,要么数据根本不存在。旧节点不清楚是那种情况,于是会放回 -ASK targetNodeAddr重定向指令
  • 客户端收到重定向指令后,先去目标节点执行不带参数的ASKING指令,然后在目标节点重新执行原来的指令

发送ASKING指令原因:

  • 在迁移没有完成之前,该槽位不归属新节点管理,如果这时候直接向目标节点发送请求该槽位的指令,目标节点不接收,会向客户端返回 -MOVED 指令指向旧节点,进而形成重定向循环。
  • ASKING指令旨在告诉目标节点,即使槽位不归属其管理,也要进行处理

迁移对效率的影响:

正常指令一个ttl可以完成,在迁移时,需要3个ttl

d. 容错

redis cluster可以为每个主节点分配若干从节点,主节点故障后,集群会自动选择某个从节点升级为主节点。若没有配置从节点,当主节点故障后,集群会处于不可用状态(redis提供cluster-require-full-coverage参数以允许部分节点故障时,仍可以继续对外提供服务)

e. 网络抖动

redis cluster处理网络抖动的参数:

  • redis cluster提供cluster-node-timeout参数,表示某个节点持续timeout的时间失联后,才可认定该节点故障,需要进行主从切换(若没有该选项,网络抖动会导致主从频繁切换)
  • redis cluster还提供了cluster-slave-validity-factor参数,作为倍数系数放大超时时间来宽松容错的紧急程度(0表示主从切换不会抗拒网络抖动,系数大于1,该系数为主从切换松弛系数)

f. 可能下线与确定下线

场景:redis cluster是去中心化的,一个节点认为某个节点失联,并不代表所有节点任务该节点失联。

流程:

  1. redis使用Gossip协议来广播自己的状态,该表整个集群的认知
  2. 一个节点发现某个节点失联了(PFail),则会将改信息广播到整个集群,其他节点可以收到这条信息
  3. 若定义某个节点失联的节点数量(PFail Count)达到了集群的大多数,则可以标志该节点下线,并对该节点进行主从切换

g. 槽位迁移感知

槽位迁移状态感知方法:

  • MOVED指令:纠正槽位
  • ASKING指令:临时纠正槽位,客户端收到ASKING error指令后,会根据指令携带的目标节点地址尝试拿数据,此时,客户端不会刷新槽位关系映射表(只是临时纠正,不会影响后续指令槽位与节点的映射)

槽位迁移指令重试次数:

  • 重试一次:MOVED指令和ASKING指令会导致重试一次
  • 重试两次:一条指令被发送到错误节点,执行MOVED指令(重试一次);客户端去MOVED指令指向的节点重试,刚好这个节点在迁移,于是向客户端返回ASKING指令,让客户端使用asking指令访问新节点(重试两次)
  • 重试多次:存在这种可能,因此在编程时,需要在客户端设置最大重试次数的参数

h. 集群变更感知

场景:服务器节点变更时,客户端应该立即得到通知并更新槽位-节点映射表

客户端更新映射表方法:

  • 目标节点挂掉了,客户端会抛出ConnectionError的异常,紧接着会任意选择一个节点来重试,重试的节点会通过MOVED指令告知目标槽位所在的新的节点地址
  • 运维手段修改了集群信息,将主节点切换为其他节点,并将旧的主节点迁移出集群。此时,打在旧节点上的指令会收到一个clusterDown错误,告知当前节点所在集群不可用(当前节点已经孤立)。这是,客户端会关闭所有连接,清空槽位映射关系表,抛出一个异常。待下一条指令过来时,再重新尝试初始化节点信息

3. cluster使用

>>> from rediscluster import RedisCluster
>>> startup_nodes=[{"host":"127.0.0.1","port":"6379"}]
>>> rc=RedisCluster(startup_nodes=startup_nodes,decode_responses=True)

cluster使用特点:cluster是去中心化的,由多个节点组成,构造cluster实例时,我们可以只用一个节点地址,其他地址由改地址发现(用多地址会比较安全)

与单机版redis的区别:cluster不支持事务,mget方法比redis要慢很多(被拆分成多个get指令);rename方法不再是原子性的,它需要将数据从源节点转移到目标节点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值