Redis高可用的保障 - 持久化,主从,哨兵,集群机制

Redis是什么

Redis是现在领先基于内存存储key-value对的高速数据库,常被用来存储各种缓存信息,据官方数据可以完成约100000次操作/秒。在速度方面几乎是无可挑剔的,但是其在内存内保存全部数据和单线程的特点也带来了很大的数据可靠性与高可用的问题:

比如如果服务器突然崩溃,在内存内存储的数据将全部丢失;一旦服务器因为某种原因停止工作,Redis服务也将停止。Redis针对大部分常见的使用问题给出了保证高可用和可靠性的解决方案。

Redis持久化

由于内存存储信息具有很高的不可靠性,一旦服务器出现问题,重启后内存内的数据就会全部丢失,因此Redis提供了RDB和AOF两种数据持久化方式,用于将内存中的数据写入磁盘,这样即使内存重置数据也可以从硬盘读取恢复。

RDB持久化

RDB持久化产生对redis内数据的快照文件,并写入.rdb格式的二进制文件保存在磁盘。这样如果服务器崩溃内存内数据丢失,重新连接后redis会自动读取默认名称为dump.rdb(可以在redis.conf内修改为其他名称)的rdb备份文件,恢复其上次备份时redis存在的所有数据。

配置RDB持久化

Redis所有可配置的选项都存储在redis.conf文件,RDB持久化默认为开启状态。

默认配置:

save 9000 1
save 300 10
save 60 10000

语法规则为 save [间隔时间] [至少被改变的键数量]

如save 9000 1代表如果至少一个键的值被改变了,每隔9000秒将执行一次BGSAVE指令将redis内数据保存/更新至指定位置的.rdb文件。注释掉其他save语句并添加 save ""即可关闭自动RDB持久化。

手动进行RDB保存

通过Redis-cli客户端,管理员也可以通过输入savebgsave命令手动触发一次RDB保存。

BGSAVE 与 SAVE的区别

除了在redis配置文件内可以设置自动保存RDB文件的频率,管理员也可以通过save和bgsave命令手动执行这一操作。

SAVE表示在redis当前线程执行RDB保存,该操作阻塞其他指令的执行,直到保存完毕其他指令才能继续执行

BGSAVE (Background save) 表示异步执行RDB保存,不影响当前redis实例其他指令的执行,因此这也是Redis官方推荐使用的方式

在redis中配置的定时更新RDB使用的就是BGSAVE指令,不会影响到其他指令的执行。

AOF持久化

AOF持久化与RDB持久化的目的相同,都是为了将redis里可能丢失的数据记录到硬盘,但是方式不同。RDB持久化每次产生一个对Redis内数据的快照,并转换为二进制文件保存,而AOF持久化采取的是日志式添加记录每次修改操作的方式,每次执行修改后,AOF持久化只需要将新执行的操作添加到appendonly.aof文件的末尾,对文件进行简单的append操作的IO消耗很小,这种文件不像二进制存储的RDB文件,是可读的,也就意味着可以被手动修改,拥有更强的灵活性。

比如Redis不小心执行了flushall指令,清空了所有数据,只要是aof没有被rewrite,只需要复制一份aof文件,去掉最后的flushall命令,再重启redis,redis会自动读取aof文件进行恢复(即从头到尾依次执行记录的操作)。

AOF持久化默认不开启,需要在redis.conf配置文件中将appendonly no改为appendonly yes开启。可以设置为每秒进行一次,或每次修改都进行持久化。

# 每次发生数据变化会立刻写入到磁盘中。性能较差当数据完整性比较好(慢,安全)
# appendfsync always  
# 默认推荐设置,每秒异步记录一次(默认值)
appendfsync everysec

注:相同数据产生的AOF文件比RDB文件更大,而且开启AOF持久化对Redis主线程的性能影响也比RDB更大,但是可以更好保证数据完整性。

AOF的rewrite

因为AOF采取记录操作并append到文件末尾的方式,文件的长度很容易达到上限,这时就会触发重写操作,AOF会获取Redis内的数据,对应存在的数据编写一份最简易的操作记录写到appendonly.aof文件并替换原本的aof文件。比如原本记录了创建一个key和删除这个key两个操作,由于Redis中这个数据当前不存在,重写后的aof文件不会存在这两条任何一条操作记录,达到简化aof文件的目的。rewrite也会把多部操作合并写到一起来减小aof文件。

主从结构

虽然Redis本身的速度很快,也有RDB和AOF持久化来保证数据不会因为特殊原因丢失,实际业务中很少会仅使用一台服务器提供Redis服务的,原因有下:

1)如果业务并发量很大,单台服务器处理负载可能会过大,速度跟不上并发量
2)如果业务需要存储的数据量很大,单台服务器的内存可能不够使用
3)单台机器无法保证高可用,一旦该机器宕机,意味着业务停用

因此,Redis提出了主从架构,可以让多台服务器上的Redis实例构成一个高可用、负载相对均衡的架构。

Redis可以设置多个Redis实例为主从关系,一旦一个Redis实例执行slaveof <host> <ip>成为另一个Redis实例的slave,会自动触发全量同步,让slave上的数据和master的数据同步,具体步骤如下:

1)从redis向主redis发起全量同步(SYNC)请求主redis同步自己存储的所有键
2)主redis收到SYNC请求后,执行BGSAVE命令生成当前数据的RDB快照文件,同时使用缓冲区记录在生成文件后新执行的操作
3)BGSAVE完成后向所有从Redis发送快照文件
4)从Redis清空所有旧数据,根据RDB快照文件加载主Redis里的数据
5)主Redis发送完快照文件后发送缓冲区内刚刚执行的命令
6)从Redis加载完毕后执行相同命令完成同步

(图片从https://blog.csdn.net/weixin_42711549/article/details/83061052转载)

注:通过RDB快照的方式进行全量同步意味着主Redis需要保存RDB文件到磁盘,意味着额外的磁盘IO,这在Redis 2.8.18后被改善,可以通过设置repl-diskless-sync yes进行无硬盘复制,即生成快照直接传输不保存。

从Redis完成初始化后,每一个对主Redis的操作会被发送到从Redis执行相同操作,这被称为增量同步,以保证主从Redis数据一致性。

默认情况下,slave只读不写,所以一般对Redis的读操作都在slave上进行,以达到分担master压力的目的,master上只进行写操作。

主从从架构

因为主Redis需要负责所有从Redis的复制和同步,不应该让一个master有太多slave,容易造成Master压力过大,所以可以采用主从从架构。比如主Redis有一个Slave,第二个Slave接入时设定存在的Slave为主,形成链式的主从架构,这样每个Redis服务只需要负责一台Slave的复制和同步。

哨兵机制

主从架构很好的做到了均衡单一服务器压力的目的,但是一旦主服务器出了问题挂掉了,服务还是会挂掉,仅靠这种架构无法保证高可用,因此Redis设计了哨兵机制。

什么是哨兵机制

哨兵(Sentinel)本身也和其他的redis实例一样,是一个单独的服务,占用一个单独的port端口,但是这些redis sentinel不负责键值的存储,他们的存在是为了保证已经存在的Redis服务的高可用(High Availability)。

哨兵的职责

1)检测主从节点是否运转正常
2)收集主从节点信息,出错的时候可以通过API通知系统管理员
3)自动failover:当一个master节点出现了问题,多个哨兵需要沟通是否真的确认出现问题,确认出现问题要检查其他slave节点的情况,自动选举新的适合充当master节点的slave,改变其状态成为新的master,继续提供服务并管理其他slave

第三个职责自动failover是哨兵最重要的职责,配合主从架构很好的保证了系统的高可用性,前面讲过主从架构中所有master和slave的数据都是一致的,所以当提供主要服务的master宕机,可以立刻把拥有相同数据的slave推举为新的master继续提供服务,而不会造成业务的不可用。

哨兵监测原理

哨兵节点通过sentinel.conf配置文件配置监测的redis服务的端口(具体如何配置随便搜搜都能看到,如果英文过关强烈建议去跟着redis官方哨兵文档配置一遍,会加深理解),redis-server sentinel.conf开启哨兵服务,服务开启后会自动监测,读取同样在监测该redis节点的哨兵信息,和该节点的slave信息。

注:一个哨兵可以设置监测多个redis节点,但是没必要设置监测slave节点,监测master节点时会自动获取到slave节点的信息。

服务开始后,哨兵会定时发送ping至该redis节点,如果收到pong的消息代表该节点在线,服务正常,如果没有收到pong会持续发送ping,直到过了设置的down-after-milliseconds(该属性在sentinel.conf里设置,单位为毫秒,代表多少毫秒没有接收到pong可以认定该redis服务已经不可用),哨兵将该redis节点标注为sdown(subjective down)状态,也就是代表该哨兵主观地认定这个redis服务不可用,此时哨兵会去联系其他监测该redis服务的哨兵,确认是否其他哨兵也将该节点标注为了sdown状态。

如果超过规定数量的哨兵将该Redis节点标注成了sdown状态,即可确定这个服务确实成为了不可用状态,将该状态改为odown(Objective down),也就是客观的不可用状态,准备进行新master节点的推举来代替该redis节点继续提供服务。

为什么要多个哨兵达成共识?

如果一个哨兵确定该Redis服务不可用,不就可以直接判定不可用了吗?为什么要去征求其他哨兵的意见?

这是因为有时候并不是真的该服务不可用了,而是因为网络连接之类的原因,该哨兵暂时与redis服务断连,因此要去征求其他哨兵的意见,是否多个哨兵认为redis服务不可用了,达到指定数量哨兵主观地认为该节点不可用,才可以设置状态为odown(客观不可用)

示例配置:

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
开始Failover

failover是指多个哨兵选举一个slave成为新的master节点代替提供服务的过程。当一个redis服务被标记成了odown状态,哨兵们会选举一个哨兵出来执行failover操作。被选举的哨兵必须要得到在监测该节点的哨兵的半数以上的选票,才能够成功被选举执行failover。

比如一共有五个哨兵在监测一个redis服务,如果这个服务客观不可用了,至少要有三个哨兵还能被联系到,才能选举出一个哨兵执行failover(也就是可以接受少数哨兵服务挂掉,不影响failover的执行)

因此,在设置哨兵的时候,应该确保每一个哨兵都处在不同的服务器,最好在不同的网络上,尽量不会发生某个事故导致多个哨兵服务一起挂掉导致服务崩溃。通常,一个Redis服务所在的服务器会同时有一个哨兵服务开启

三个哨兵原则

一个高可用的架构应该至少包括三个部署在不同服务器上的哨兵,因为如果只有两个哨兵,有一台服务器挂了,哨兵也就只剩下了一个,即使能确定服务器上redis服务的odown状态,也没有超过半数(超过2/2 = 1个哨兵)的哨兵来选举出哨兵执行failover,因此一个合理的系统至少有三台服务器,三个哨兵来保证系统的高可用性(HA)。

集群架构

什么是集群

Redis从3.0版本后支持集群(Redis Cluster),有时当单台服务器只有几十GB内存,而业务需求在Redis中存储上百GB的数据时,仅仅依靠主从架构无法满足需求,因为master和slave存储的数据一致,在主从架构中所有服务器的数据都是相同的,无法满足存储额外数据的需求。集群就是来解决这一问题的,配合主从架构,在实现多台服务器均存储业务中的部分数据,组合起来提供业务服务的功能同时保证高可用

注:集群不支持单机Redis的多数据库,SELECT命令对于一个集群节点是不可用的,默认使用DB0。

如何知道需要的数据存储在哪台服务器?存储的时候又如何确定要存储在哪台服务器?

在存储时可以在O(1)时间完成存储,在读取时也可以在O(1)时间完成读取,聪明的读者一定会想到哈希表(Hashmap)这一神奇的数据结构,Redis的集群架构也正是采用相似的思路实现的:

对每个存入集群的键进行哈希值的计算,确定其存储的哈希槽(Hash slot),每个集群都有固定的16384个哈希槽,使用的哈希算法是HASH_SLOT = CRC16(key) mod 16384,相当于其中CRC16只使用了16位中的14位,以产生一个1-16384的哈希值。

CRC16代表其产出的值可以在16bit能保存的范围,也就是可以产生2^16=65536个可能值,那么为什么要选择对16384取余而不使用65536个哈希槽呢

这个问题在redis的github issue 2576中redis的作者著名的antirez给出了解答:

“The reason is:

  1. Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
  2. At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.

So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.“

如果已经看懂的小伙伴可以跳过下面的解读环节~

为什么仅使用16384个哈希槽

这是因为集群中每个节点都通过TCP连接通信,每个节点都有一个对应的cluster bus用来与其他节点通信,节点之间要互相定时发送ping命令并获取节点实时信息,每次传输的包(称为heartbeat packets)包含如下信息:

此图片从孤独烟微信公众号转载
节点通信
主要注意下面的红框,每次节点通信的传输包需要传送一个名为myslots的bitmap,其所占用的大小为 哈希槽数量/8/1024 KB,如果是16384个哈希槽就要有16384bit = 2KB大小,每一个bit如果是1则代表节点负责该哈希槽,如果使用65536个哈希槽就意味着每次传输要传输8KB数据,这是不可接受的。

其次,使用65536个哈希槽也几乎没有任何必要,因为均衡考虑其他的因素,基本上所有的集群都不应该超过1000个Redis节点,而在这种数量级16384个哈希槽完全足够。

并且,槽位越少bitmap的压缩比就越高。

综上所述,antirez选择16384个槽位是最合适的。

哈希槽的分配

这16384个哈希槽可以随意分配给主节点,只要所有主节点负责的哈希槽加起来正好16384个即可。

注:从节点(slave/replica)是不可以负责任何节点的,前面在主从架构里讲过了,从节点的数据和主节点完全一致,只不过是与主节点合作降低主节点的负载,并且在主节点崩溃断连的时候变成主节点继续提供服务维持高可用。

集群的高可用

前面讲过,非集群的主从架构的高可用性是依靠哨兵机制来维持的,在主节点断连的时候由哨兵执行failover操作推举slave成为master继续提供服务。由于集群的存在本身就是为了提供大型高可用的服务,集群里内置了failover机制,不再采用哨兵监测的方式,也就是不需要单独去配置。

下面部分内容参考文章:https://blog.csdn.net/qq_31720329/article/details/104068469

集群自身的高可用是依靠节点之间定时互相传输的ping和heartbeat packet保证的,一旦有一个master节点超过了cluster-node-timeout毫秒没有对兄弟们的ping返回pong,其他节点会把该节点标注为PFAIL状态。

一旦有一个节点将其标注成了PFAIL,并通过gossip协议传播给其他节点,内置函数markNodeAsFailingIfNeeded就会被每一个节点执行,统计认为PFAIL状态的个数

void markNodeAsFailingIfNeeded(clusterNode *node) {
    int failures;
    int needed_quorum = (server.cluster->size / 2) + 1;

    if (!nodeTimedOut(node)) return; /* 我自己能和目标节点收发消息 */
    if (nodeFailed(node)) return; /* 这个节点已经被标记成 FAIL 了 */

    failures = clusterNodeFailureReportsCount(node);
    /* 如果我自己也是 master, 那么把我自己也计入数目中 */
    if (nodeIsMaster(myself)) failures++;
    if (failures < needed_quorum) return; /* 把目标节点标记成 PFAIL 的 master 节点还不集群数目的一半, 不操作 */

    serverLog(LL_NOTICE,
        "Marking node %.40s as failing (quorum reached).", node->name);

    /* 把目标节点标记成 FAIL */
    node->flags &= ~CLUSTER_NODE_PFAIL;
    node->flags |= CLUSTER_NODE_FAIL;
    node->fail_time = mstime();

    /* 把目标节点被标记成 FAIL 这个信息广播给集群里的所有其他节点,
     * 让所有能与我自己建立连接的节点都把目标节点标记成 FAIL */
    if (nodeIsMaster(myself)) clusterSendFail(node->name);
    clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
}

可以看到如果要把该节点标记为FAIL状态,需要半数以上的master节点将其标记为了PFAIL,也就是半数以上的master节点已经超过了cluster-node-timeout毫秒没有收到来自该节点的pong信息。

标记为FAIL后slave节点们会发起对自己的投票,一旦有得到超过半数master节点的投票的slave,其就会被设置成新的master继续提供业务。

如果本文中讲到的地方有其他观点,欢迎评论指出~

码字不易,麻烦给个点赞关注,后续还会写关于Redis,Spring以及Java基础的深度文章~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值