• 技术文章 >数据库 >Redis

    聊聊redis中的那些高可用方案!

    青灯夜游青灯夜游2022-01-23 09:30:25转载154
    redis有哪些高可用方案呢?本篇文章给大家介绍一下redis中的那些高可用方案,希望对大家有所帮助!

    redis通常不会是部署单个的,不然不会造成单点故障,那么redis有哪些高可用方案呢?

    主从复制

    用户可以通过SLAVEOF命令或者配置,让一个服务器去复制另一个服务器。被复制的服务器称为主服务器,进行复制的服务器称为从服务器。这样你在主服务器上增加键值,同时可以在从服务器上读取。【相关推荐:Redis视频教程

    复制的过程又分为同步和命令传播两个步骤。

    同步

    同步将从服务器的数据库状态更新到主服务器当前的数据库状态。

    客户端向从服务器发送SLAVEOF命令时,从服务器会向主服务器发生SYNC命令进行同步,步骤如下:

    1.png

    命令传播

    同步操作完成之后,主服务器和从服务器的数据库状态是一致的,但主服务器又接收到客户端写命令后,主从数据库之间又产生了数据不一致,这时通过命令传播达到数据库一致。

    PSYNC同步的优化

    2.8之前的同步每次都是全量同步,而如果是从服务器只是断开连接了一会,事实上是不用从头开始同步的,只需要将断开连接这会的数据同步即可。所以2.8版本开始使用PSYNC来代替SYNC命令。

    PSYNC分成全量同步和部分同步两种情况,全量同步就是处理初次同步的状态,而部分同步就是处理断线重连这种情况。

    部分同步的实现

    部分同步主要使用了以下三部分:

    复制偏移量

    主服务器的复制偏移量:主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量+N从服务器的复制偏移量:从服务器每次收到主服务器传播的N个字节数据,就将自己的复制偏移量+N 如果主从服务器处于一致状态,那么它们的偏移量总是相同的,如果偏移量不相等,那么说明它们处于不一致状态。

    复制积压缓冲区

    复制积压缓冲区由主服务器维护的一个固定长度的FIFO队列,默认大小1MB,达到最大长度后,最先入队的会被弹出,给新入队的元素让位置。
    redis命令传播的时候不但会发送给从服务器,还会发送给复制积压缓冲区。

    2.png

    当从服务器重连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器根据复制偏移量来决定使用部分同步还是全量同步。 如果offset偏移量之后的数据还在复制积压缓冲区,那么使用部分同步,反之使用全量同步。
    (书上没说是怎么判断的,我猜测应该是拿主复制偏移量减去从复制偏移量,如果大于1MB就说明有数据不在缓冲积压区?)

    服务器的运行ID

    服务器启动时会生成一个40位随机的字符作为服务器运行ID。

    从服务器对主服务器初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器会将这个运行ID保存下来。从服务器断线重连的时候,会将保存的运行ID发送过去,如果从服务器保存的运行ID和当前主服务器的运行ID相同,那么会尝试部分同步,如果不同会执行全量同步。

    PSYNC的整体流程

    3.png

    心跳检测

    在命令传播阶段,从服务器会默认以每秒一次的频率,向主服务器发送命令:
    REPLICONF ACK <replication_offset>
    其中replication_offset就是从服务器当前的复制偏移量。 发送REPLICONF ACK命令对于主从服务器有三个作用:

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

    主从服务器可以通过发送和接收REPLICONF ACK命令来检查两者之间的网络连接是否正常:如果主服务器超过一秒钟没有收到从服务器发来的REPLICONF ACK命令,那么主服务器就知道主从之间出现问题了。

    辅助实现min-slaves选项

    redis的min-slaves-to-writemin-slaves-max-lag两个选项可以防止主从服务器在不安全的情况下执行写命令。

    min-slaves-to-write 3
    min-slaves-max-lag 10

    如果配置如上,就表示如果从服务器的数量少于3个,或者3个从服务器的延迟都大于或等于10秒时,那么主服务器就将拒绝执行写命令。

    检测命令丢失

    如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么从服务器向主服务器发送REPLICONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的偏移量,那么主服务器可以根据从服务器的复制偏移量,在复制缓冲区当中找到从服务器缺少的数据,将这些数据重写发送给从服务器。

    主从复制总结

    其实主从复制就是多备份了一份数据,因为即使有RDB和AOF进行持久化,但是可能主服务器上整个机器挂掉了,而主从复制可以将主从服务器部署在两台不同的机器上,这样即使主服务器的机器挂掉了,也可以手动切换到从服务器继续服务。

    sentinel

    主从虽然实现了数据的备份,但当主服务器挂掉时,需要手动的将从服务器切换成主服务器。而sentinel就可以实现当主服务器挂掉时,自动将从服务器切换成主服务器。

    4.png

    sentinel系统可以监视所有的主从服务器,假设server1现在下线。当server1的下线时长超过用户设定的下线时长上限时,sentinel系统就会对server1执行故障转移:

    初始化sentinel状态

    struct sentinelState {
        char myid[CONFIG_RUN_ID_SIZE+1]; 
        // 当前纪元,用于实现故障转移
        uint64_t current_epoch;
        // 保存了所有被这个sentinel监视的主服务器
        // 字典的键是主服务器的名字
        // 字典的值是指向sentinelRedisInstance结构的指针
        dict *masters;
        // 是否进入了TILT模式
        int tilt;         
        // 目前正在执行的脚本数量
        int running_scripts;   
        // 进入TILT模式的时间
        mstime_t tilt_start_time;   
        // 最后一次执行时间处理器的时间
        mstime_t previous_time;     
        // 一个fifo队列,包含了所有需要执行的用户脚本
        list *scripts_queue;            
        char *announce_ip;  
        int announce_port; 
        unsigned long simfailure_flags; 
        int deny_scripts_reconfig;
        char *sentinel_auth_pass;   
        char *sentinel_auth_user;    
        int resolve_hostnames;      
        int announce_hostnames;     
    } sentinel;

    初始化sentinel状态的masters属性

    masters记录了所有被sentinel监视的主服务器的相关信息,其中字典的键是被监视服务器的名字,而值是被监视服务器对应着sentinelRedisInstance结构。sentinelRedisInstance被sentinel服务器监视的实例,可以是主服务器、从服务器或其他sentinel实例。

    typedef struct sentinelRedisInstance {
        // 标识值,记录实例的类型,以及该实例的当前状态
        int flags;  
        // 实例的名字
        // 主服务器名字在配置文件中设置
        // 从服务器和sentinel名字由sentinel自动设置,格式是ip:port
        char *name; 
        // 运行id
        char *runid;   
        // 配置纪元,用于实现故障转移
        uint64_t config_epoch;  
        // 实例的地址
        sentinelAddr *addr; /* Master host. */
        // 实例无响应多少毫秒之后,判断为主观下线
        mstime_t down_after_period; 
        // 判断这个实例为客观下线所需的支持投票数量
        unsigned int quorum;
        // 执行故障转移,可以同时对新的主服务器进行同步的从服务器数量
        int parallel_syncs; 
        // 刷新故障迁移状态的最大时限
        mstime_t failover_timeout;  
        // 除了自己外,其他监视主服务器的sentinel
        // 键是sentinel的名字,格式是ip:port
        // 值是键对应的sentinel的实例结构
        dict *sentinels;  
        // ...
    } sentinelRedisInstance;

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

    初始化sentinel的最后一步是创建连向被监视主服务器的网络连接,会创建两个连向主服务器的连接。

    5.png

    命令连接:专门向主服务器发送命令,并接收命令回复。
    订阅连接:专门用于订阅主服务器的_sentinel_:hello频道。

    获取主服务器信息

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

    根据这些信息可以更新sentinelRedisInstance下的name字典和runid字段。

    获取从服务器信息

    sentinel也会创建连接到从服务器的命令连接和订阅连接。

    6.png

    sentinel默认会每10秒,通过命令连接向从服务器发送INFO命令,并通过回复获取从服务器当前的信息。回复如下:

    7.png

    根据info的回复信息,sentinel可以更新从服务器的实例结构。

    向主服务器和从服务器的订阅连接发送信息

    默认情况下,sentinel会每2秒一次,向被监视的主服务器和从服务器发送命令。

    8.png

    s_ip:sentinel的ip地址
    s_port:sentinel的端口号
    s_runid:sentinel的运行id
    s_epoch:sentinel当前的配置纪元
    m_name:主服务器的名字
    m_ip:主服务器的ip地址
    m_port:主服务器的端口号
    m_epoch:主服务器当前的配置纪元
    向sentinel_:hello频道发送信息,也会被监视同一个服务器的其他sentinel监听到(包括自己)。

    创建连向其他sentinel的命令连接

    sentinel之间会互相创建命令连接。监视同一个嘱咐其的多个sentinel将形成相互连接的网络。

    9.png

    sentinel之间不会创建订阅连接。

    检测主观下线状态

    sentinel会每秒一次向所有与它创建了命令连接的实例(主服务器、从服务器、其他sentinel)发送ping命令,通过实例的回复来判断实例是否在线。
    有效回复:实例返回+PONG、-LOADING、-MASTERDOWN其中一种。
    无效回复:以上三种回复之外的其他回复,或者指定时长内没回复。
    某个实例在down-after-milliseconds毫秒内,连续向sentinel返回无效回复。那么sentinel就会修改这个实例对应的实例结构,在结构的flags属性中打开SRI_S_DOWN标识,标识该实例进入主观下线状态。(down-after-milliseconds可以在sentinel的配置文件中配置)

    检测客观下线状态

    当sentinel将一个主服务器判断为主观下线后,为了确认这个主服务器是否真的下线,还会想其他同样监视这个主服务器的其他sentinel询问,看其他sentinel是否也认为该主服务器下线了。超过一定数量就将主服务器判断为客观下线。

    询问其他sentinel是否同意该服务器下线

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

    通过SENTINEL is-master-down-by-addr命令询问,参数意义如下图:

    10.png

    接收SENTINEL is-master-down-by-addr命令

    其他sentinel接收到SENTINEL is-master-down-by-addr命令后,会根据其中主服务器的ip和端口,检查主服务器是否下线,然后返回包含三个参数的Multi Bulk的回复。

    10-2.png

    sentinel统计其他sentinel同意主服务器已下线的数量,达到配置的数量后,则将主服务器的flags属性的SRI_O_DOWN标识打开,表示主服务器已经进入客观下线状态。

    选举领头sentinel

    当一个主服务器被判断成客观下线时,监视这个下线主服务器的各个sentinel就会协商选举一个新的领头sentinel,由这个sentinel进行故障转移操作。

    11.png

    确认主服务器进入客观下线状态后,会再次发送SENTINEL is-master-down-by-addr命令来选举出领头sentinel。

    选举规则

    故障转移

    故障转移包含以下三个步骤:

    选出新的主服务器

    已下线的主服务器下所有从服务器里,挑选出一个从服务器,向这个从服务器发送SLAVEOF no one命令,将这个从服务器转换成主服务器。

    挑选新主服务器的规则

    领头的sentinel会将已下线主服务器的所有从服务器保存到一个列表里面,然后对这个列表进行过滤,挑选出新的主服务器。

    发送slaveof no one 命令之后,领头sentinel会每秒一次向被升级的从服务器发送info命令(平常是每10秒一次),如果返回的回复role从原来的slave变成了master,那么领头sentinel就知道从服务器已经升级成主服务器了。

    修改从服务器的复制目标

    通过SLAVEOF命令来使从服务器复制新的主服务器。当sentinel监测到旧的主服务器重新上线后,也会发送SLAVEOF命令使它成为新的主服务器的从服务器。

    sentinel总结

    sentinel其实就是一个监控系统,,而sentinel监测到主服务器下线后,可以通过选举机制选出一个领头的sentinel,然后由这个领头的sentinel将下线主服务器下的从服务器挑选一个切换成主服务器,而不用人工手动切换。

    集群

    哨兵模式虽然做到了主从自动切换,但是还是只有一台主服务器进行写操作(当然哨兵模式也可以监视多个主服务器,但需要客户端自己实现负载均衡)。官方也提供了自己的方式实现集群。

    节点

    每个redis服务实例就是一个节点,多个连接的节点组成一个集群。

    CLUSTER MEET <ip><port>

    向另一个节点发送CLUSTER MEET命令,可以让节点与目标节点进行握手,握手成功就能将该节点加入到当前集群。

    启动节点

    redis服务器启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器集群模式。

    12.png

    集群数据结构

    每个节点都会使用一个clusterNode结构记录自己的状态,并为集群中其他节点都创建一个相应的clusterNode结构,记录其他节点状态。

    typedef struct clusterNode {
        // 创建节点的时间
        mstime_t ctime; 
        // 节点的名称
        char name[CLUSTER_NAMELEN];
        // 节点标识
        // 各种不同的标识值记录节点的角色(比如主节点或从节点)
        // 以及节点目前所处的状态(在线或者下线)
        int flags;     
        // 节点当前的配置纪元,用于实现故障转移
        uint64_t configEpoch;
        // 节点的ip地址
        char ip[NET_IP_STR_LEN];  
        // 保存建立连接节点的有关信息
        clusterLink *link;          
        
        list *fail_reports;  
        // ...
    } clusterNode;

    clusterLink保存着连接节点所需的相关信息

    typedef struct clusterLink {
        // ...
        // 连接的创建时间
        mstime_t ctime;           
        // 与这个连接相关联的节点,没有就为null
        struct clusterNode *node;   
        // ...
    } clusterLink;

    每个节点还保存着一个clusterState结构,它记录了在当前节点视角下,集群目前所处的状态,例如集群在线还是下线,集群包含多少个节点等等。

    typedef struct clusterState {
        // 指向当前节点clusterNode的指针
        clusterNode *myself;  
        // 集群当前的配置纪元,用于实现故障转移
        uint64_t currentEpoch;
        // 集群当前的状态,上线或者下线
        int state;           
        // 集群中至少处理一个槽的节点数量
        int size;      
        // 集群节点的名单(包括myself节点)
        // 字典的键是节点的名字,字典的值为节点对应的clusterNode结构
        dict *nodes; 
    } clusterState;

    CLUSTER MEET 命令的实现

    CLUSTER MEET <ip><port>

    13.png

    槽指派

    集群的整个数据库被分为16384个槽,每个键都属于16384个槽的其中一个,集群中每个节点处理0个或16384个槽。当所有的槽都有节点在处理时,集群处于上线状态,否则就是下线状态。

    CLUSTER ADDSLOTS

    CLUSTER ADDSLOTS <slot>...
    通过CLUSTER ADDSLOTS命令可以将指定槽指派给当前节点负责,例如:CLUSTER ADDSLOTS 0 1 2 3 4 可以将0至4的槽指派给当前节点

    记录节点的槽指派信息

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

    typedef struct clusterNode {
             
        unsigned char slots[CLUSTER_SLOTS/8];
        
        int numslots;
        // ...
    } clusterNode;

    slots:是一个二进制数组,一共包含16384个二进制位。当二进制位的值是1,代表节点负责处理该槽,如果是0,代表节点不处理该槽numslots:numslots属性则记录节点负责处理槽的数量,也就是slots中值为1的二进制位的数量。

    传播节点的槽指派信息

    节点除了会将自己负责的槽记录在clusterNode中,还会将slots数组发送给集群中的其他节点,以此告知其他节点自己目前负责处理哪些槽。

    typedef struct clusterState {
        clusterNode *slots[CLUSTER_SLOTS];
    } clusterState;

    slots包含16384个项,每一个数组项都是指向clusterNode的指针,表示被指派给该节点,如果未指派给任何节点,那么指针指向NULL。

    CLUSTER ADDSLOTS命令的实现

    14.png

    在集群中执行命令

    客户端向节点发送与数据库有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查该槽是否指派给了自己。
    如果指派给了自己,那么该节点直接执行该命令。如果没有,那么该节点会向客户端返回一个MOCED的错误,指引客户端转向正确的节点,并再次发送执行的命令。

    15.png

    计算键属于那个槽

    16.png

    CRC16(key)是计算出键key的CRC16的校验和,而 & 16383就是取余,算出0-16383之间的整数作为键的槽号。

    判断槽是否由当前节点负责处理

    计算出键所属的槽号i后,节点就能判断该槽号是否由自己处理。
    如果clusterState.slots[i]等于如果clusterState.myself,那么由自己负责该节点可以直接执行命令。
    如果不相等,那么可以获取clusterState.slots[i]指向如果clusterNode的ip和端口,向客户端返回MOVED错误,指引客户端转向负责该槽的节点。

    集群模式下不会打印MOVED错误,而是直接自动转向。

    重新分片

    redis集群重新分配可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,相关槽所属的键值对也会从源节点移动到目标节点。
    重新分片操作是在线进行的,在重新分片的过程中,集群不用下线,源节点和目标节点都可以继续处理命令请求。 redis集群的重新分片操作是由redis-trib负责执行。重新分片执行步骤如下:

    17.png

    CLUSTER SETSLOT IMPORTING 命令实现

    typedef struct clusterState {
        // ...
        clusterNode *importing_slots_from[CLUSTER_SLOTS];
    
    } clusterState;

    importing_slots_from记录了当前节点正在从其他节点导入的槽。importing_slots_from[i]不为null,则指向CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,<source_id>所代表的clusterNode结构。

    CLUSTER SETSLOT MIGRTING 命令实现

    typedef struct clusterState {
        // ...
        clusterNode *migrating_slots_to[CLUSTER_SLOTS];
    
    } clusterState;

    migrating_slots_to记录了当前节点正在迁移至其他节点的槽。migrating_slots_to[i]不为null,则指向迁移至目标节点所代表的clusterNode结构。

    ASK错误

    在重新分片期间,源节点向目标节点迁移槽的过程中,可能属于这个槽的一部分键值对一部分保存在源节点当中,而另一部分保存在目标节点当中。
    客户端向源节点发送一个与数据库键有关的命令,恰好这个槽正在被迁移。
    源节点现在自己的数据库中查找指定的键,如果找到,直接执行。
    如果没有找到,节点会检查migrating_slots_to[i]查看键是否正在迁移,如果在迁移就返回一个ask错误,引导客户端转向目标节点。

    ASKING

    客户端收到ask错误之后,会先执行ASKING命令,再向目标节点发送命令。ASKING命令就是打开发送该命令的客户端的REDIS_ASKING标识。一般来说客户端发送的键如果不属于自己负责会返回MOVED错误(槽只迁移部分,这时槽还不属于目标节点负责),但还会检查importing_slots_from[i],如果显示节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING标识,那么它就会破例执行一次该命令。

    18.png

    集群的故障转移

    集群的故障转移效果和哨兵模式类似,也是将从节点升级成主节点。旧的主节点重新上线后将会成为新主节点的从节点。

    故障检测

    集群中每个节点会定期的向集群中其他节点发送PING消息,检测对方是否在线,如果指定时间内没有收到PONG消息,那么就将该节点标记为疑似下线。clusterState.nodes字典中找到该节点的clusterNode结构,将flags属性修改成REDIS_NODE_PFAIL标识。
    集群中各个节点会互相发送消息来交换集群中各个节点的状态,例如:主节点A得知主节点B认为主节点C进入了疑似下线状态,主节点A会在clusterState.nodes字典中找到节点C的clusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports链表当中。
    每一个下线报告由一个clusterNodeFailReport结构表示

    typedef struct clusterNodeFailReport {
        struct clusterNode *node; 
        // 最后一次收到下线报告的时间
        mstime_t time;            
    } clusterNodeFailReport;

    如果一个集群当中,半数以上负责处理槽的主节点都将某个主节点X报告为疑似下线。那么这个主节点X将被标记为已下线。将主节点X标记成已下线的节点会向集群广播一条关于主节点X的FAIL消息。所有收到这条FAIL消息的节点都会将主节点X标记成已下线。

    故障转移

    当一个从节点发现自己正在复制的主节点进入了已下线状态,从节点将开始对下线主节点进行故障转移。

    选举新的主节点

    新的主节点通过选举产生

    主节点选举的过程和选举领头sentinel的过程非常相似。

    数据丢失

    主从复制数据丢失

    主从复制之间是异步执行的,有可能master的部分数据还没来得及同步到从数据库,然后master就挂了,这时这部分未同步的数据就丢失了。

    脑裂

    脑裂就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着。此时哨兵可能就会认为master 宕机了,然后开启选举,将其他slave切换成了master,这个时候,集群里面就会有2个master,也就是所谓的脑裂。
    此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续向旧master的写数据。
    master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据将会清空,重新从新的master复制数据,导致数据丢失。

    减少数据丢失的配置

    min-slaves-to-writ 1
    min-slaves-max-lag 10

    上述配置表示,如果至少有1个从服务器超过10秒没有给自己ack消息,那么master不再执行写请求。

    主从数据不一致

    当从数据库因为网络原因或者执行复杂度高命令阻塞导致滞后执行同步命令,导致数据同步延迟,造成了主从数据库不一致。

    都看到这了,点个赞再走了吧:)

    更多编程相关知识,请访问:编程入门!!

    以上就是聊聊redis中的那些高可用方案!的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    专题推荐:redis 高可用方案
    上一篇:浅析Redis为什么快?快在哪里? 下一篇:深入解析Redis中的数据结构,聊聊应用场景

    相关文章推荐

    • 一文聊聊Redis中的限流策略• 一文了解Redis中的哨兵模式• 如何解决docker redis 连不上的问题• PHP+Redis解决缓存击穿的实际问题• 聊聊redis中多样的数据类型,以及集群相关的知识• 浅析Redis挖矿原理,看看如何预防(技巧分享)

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网