宕机恢复
redis对数据读/写操作的速度快到令人发指,因此很多人把redis当作缓存系统来使用,用于提高系统的读取性能。然而,"快"是需要付出代价的:内存无法持久化,一旦断电或宕机,保存在内存中的数据会全部丢失。在这种情况下,没有了redis的支持,大量流量被发到mysql种可能带来更大的问题。因此,如果redis重启时从mysql数据库里面获取数据加载到内存中,当数据量巨大时,可能会给mysql数据库造成更大的压力。考虑到这种情况,redis本身可以实现数据持久化,做到宕机快速恢复------RDB快照和AOF。
RDB快照
当数据存储在内存中,redis会把内存中的数据写到磁盘上实现持久化,然后在重启时把磁盘的快照数据快速加载到内存中,这样就能实现重启后正常提供服务。
程序员通常把redis当作缓存系统来使用,对一致性的要求不高,不需要把所有的数据都保存下来,使用RDB快照的方式来实现宕机快速恢复。
在快速执行大量写命令的过程中,内存数据会一直变化。RDB快照指的就是Redis内存中某一刻到的数据,好比拍照,把某个瞬间的画面定格下来。把某一刻的数据以文件的形式"拍"下来,写到磁盘上。这个文件叫做RDB文件。因此,redis只需要定时执行RDB快照,就不必在每次执行写命令时都写入磁盘;即实现了快,又实现了持久化。当宕机重启数据恢复时,直接将磁盘的RDB文件读入内存即可。
-
RDB生成策略
redis是单线程模型执行读/写命令,所以需要尽可能避免阻塞RDB文件生成,以免阻塞主线程。有两种情况会触发RDB快照持久化。
-
手动触发:执行save或bgsave命令。
-
自动触发:一共又=有4种情况会自动触发执行bgsave命令生成RDB文件,后文细说。
手动触发
- save:主线程执行,会阻塞。
- bgsave:调用glibc函数fork产生一个子进程用于写入临时RDB文件,RDB快照持久化完全交给子进程来处理,完成后自动结束,父进程可以继续处理客户端请求,阻塞只发生在fork阶段,时间很短,生成RDB文件的默认配置使用的就是该命令。当子进程写完新的RDB文件后,会替换旧的RDB文件。
自动触发
- 在redis.conf中配置save m n:在m秒内至少有n个key更改,自动触发bgsave生成RDB文件。
- 主从复制:从节点需要从主节点进行全量复制时会触发bgsave操作,把生成的RDB文件发送给从节点。
- 执行debug reload命令重新加载redis会触发bgsave操作。
- 在默认情况下执行shutsown命令时,如果没有开启AOF持久化,那么也会触发bgsave操作。
如果配置为save '''',则表示关闭RDB快照功能。聪明的程序员可根据实际请求压力调整RDB快照周期执行策略。
其他配置
javascript#文件名称 dbfilename dump.rdb #文件保存路径 dir /opt/app/redis/data/ #当持久化出错时,主进程是否停止写入 stop-writes-on-bgsave-error yes #是否压缩 rdbcompression yes #导入时是否检查 rdbchecksum yes
- stop-writes-on-bgsave-error:在RDB文件生成的过程中,主线程依然可以接收客户端的写命令,但这是在RDB快照操作正常的前提下。如果生成RDB文件期间出现异常,例如操作系统权限不够、磁盘已满,该配置被配置为yes,就会禁止执行写操作。
- rdbcompression:如果启用LZF压缩算法对字符串类型的数据进行压缩,生成RDB文件,则配置为yes。
- rdbchecksum :从redis5.0开始,RDB文件的末尾会有一个64位的CRC校验码,用于验证整个RDB文件的完整性。这个功能大概会损失10%左右的性能,但是能获得更高的数据可靠性,追求极致性能可以将其配置为no。
-
-
写时复制
在实际生产环境中,程序员通常会给redis配置6gb的内存,将这么大的内存数据生成RDB文件落到磁盘的过程中需要较长时间。redis是如何做到在继续处理写命令请求时保证RDB文件与内存中的数据一致的?
redis使用了操作系统的多进程写时复制技术来实现RDB快照持久化。在持久化时会调用操作系统glibc函数fork产生一个子进程,RDB快照持久化完全交给子进程来处理,主进程继续处理客户端请求,因此redis对内存数据做RDB快照时,并不会暂停写操作(读操作不会造成数据的不一致)。
在子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这是linux操作系统的机制,为了节约内存资源,尽可能让它们共享。在进程分离的一瞬间,内存的增加几乎没有明显变化。
bgsave子进程可以共享父进程的所有内存数据,所以能读取父进程的数据并写入RDB文件。
如果父进程对这些数据进行读操作,那么父进程和bgsave子进程互不影响。当父进程要修改某个field-value pairs时,这个数据会把发生变化的数据复制一份,生成副本。
接着,bgsave子进程会把这个副本数据写入保存在磁盘的RDB文件中,从而保证数据一致性。如果在执行快照期间,redis崩溃了怎么办?
只要数据没有全部写道磁盘中,这次RDB快照就不算成功,崩溃恢复时只能将上一个完整的RDB文件作为恢复文件。注意:执行bgsave操作时不阻塞主进程,但是,如果频繁执行全量快照会带来两个方面的开销。
- 频繁生成RDB文件,磁盘压力过大。会出现一个RDB文件还未生成完,下一个又开始生成的情况,陷入死循环。
- bgsave子进程是由主进程fork出来的,虽然bgsave不会阻塞主进程,但是fork会阻塞主进程。内存越大,阻塞时间越长。
-
优缺点
RDB文件的优点如下。
- RDB文件采用二进制格式数据+数据压缩的方式写磁盘,文件体积远小于内存大小,适用于备份和全量复制。
- RDB文件加载恢复数据的速度远快于AOF文件。
RDB文件的缺点如下。
- 实时性不够,无法做到秒级持久化。
- 通过bgsave调用fork函数创建子进程,子进程属于重量级操作,频繁执行成本高。
AOF
AOF持久化记录的是服务器接收的每个写操作,在服务器启动,重放还原数据集。
AOF采用的是写后日志模式,即先写内存,后写日志。还有一个写前日志,即在实际写数据之前,将修改的数据写到日志文件,再修改数据。例如,Mysql InnoDB存储引擎,在实际修改数据前先记录修改redo log,再修改数据。
在默认情况下,我们并不会开启AOF持久化,程序员可以通过配置redis.conf文件将其开启。
javascript
appendonly yes
appendfilename "appendonly.aof"
dir ./ #设置aof文件的保存位置
-
日志格式
当redis接收到set key MageByte命令将数据写到内存后,会按照如下格式写入AOF文件。
-
写回策略
为了提高文件的写入效率,当系统调用write函数时,通常会将待写入的数据暂存在一个内存缓冲区里,当缓冲区的空间被填满或者超过了指定的时限后,才真正将缓冲区中的数据写入磁盘。
这种做法虽然提高了效率,但也为写入数据带来了安全问题,如果计算机宕机,那么保存在内存缓冲区里的数据将会丢失。
为此,系统提供了fsync和fdatasync两个同步函数,它们可以强制操作系统立即将缓冲区中的数据写入硬盘,从而确保写入数据的安全性。
Redis提供的AOF配置项appendfsync写回策略直接决定AOF持久化功能的效率和安全性。
◎ always:同步写回,写命令执行完毕立刻将aof_buf缓冲区中的内容刷写到AOF文件。
◎ everysec:每秒写回,写命令执行完,日志只会写到AOF文件缓冲区,每隔一秒就把缓冲区的内容同步到磁盘。
◎ no:操作系统控制,写命令执行完,把日志写到AOF文件内存缓冲区,由操作系统决定何时同步到磁盘。
没有两全其美的策略,我们需要在性能和可靠性上进行取舍。always可以做到数据不丢失,但是每个写命令都需要写入磁盘,性能最差。
everysec避免了同步写回的性能开销,发生宕机时可能有一秒未写入磁盘的数据丢失,在性能和可靠性之间做了折中。
no的性能最好,但是可能丢失很多数据。
-
AOF重写瘦身
为了解决AOF文件体积膨胀的问题,redis提供了重写机制,对文件进行瘦身。
例如,使用INCR counter实现一个自增计数器,初始值为1,递增1000次的目标是1000,在AOF中保存1000次命令。在重写时并不需要其中的999个写操作,重写机制有"多变一"功能,将旧日志中的多条命令重写后就变成了一条命令。
其原理是开辟一个子进程将内存中的数据转换成一系列redis的写操作命令,写到一个新的AOF日志文件中。再将操作期间新增的AOF日志追加到这个新的AOF日志文件中,追加完毕立即用其替代旧的AOF日志文件,瘦身工作就完成了。
javascriptauto-aof-rewrite-percentage 100 #如果当前aof文件的大小超过了上次重写后的aof文件大小,则开始重写aof。 auto-aof-rewrite-min-size 64mb #触发aof文件重写的最小值。如果aof文件的大小小于这个值,则不触发重写操作。
注意:程序员手动执行bgrewriteaof命令并不受这两个条件限制。aof重写通过主进程fork出一个bgrewriteaof子进程,把主进程的内存复制一份给bgrewriteaof子进程,子进程就能在不影响主进程的情况下,将内存中的数据生成写操作并记录到重写日志。因此,在aof重写时,阻塞主进程只发生在主进程fork子进程那一刻。
重写过程重写过程中会出现两份日志和一份数据拷贝。分别是旧的AOF文件、新的AOF文件和Redis数据拷贝。
Redis会将重写过程中接收到的写操作同时记录到旧的AOF缓冲区和新的AOF缓冲区,这样新的AOF日志也会保存最新的操作。等到复制数据的所有操作记录重写完成后,新的AOF缓冲区记录到最新操作也会写到新的AOF文件中。
每次AOF重写时,redis都会先执行内存复制操作,让bgrewriteaof子进程拥有此时的redis内存快照,子进程遍历redis内存快照中的全部数据,生成重写记录。
Mutli-Part AOF机制在AOF Rewrite过程中,主进程除了把写命令写入AOF缓冲区,还要把写命令写入AOF重写缓冲区。一份数据要写入两个缓冲区,还要写入两个AOF文件,产生两次磁盘I/O,太浪费了。
上述的AOF Rewrite操作是Redis 7.0之前的逻辑,俗话说得好,"只要思想不滑坡,办法总比困难多"。为了解决性能问题,7.0版本开始引入Multi-Part AOF机制。
除了这个问题,7.0之前版本的AOF Rewrite操作其实还有以下几点性能问题。
◎开辟AOF Rewrite缓冲区,存放AOF重写期间的所有日志,在写命令密集的场景中,AOF Rewrite缓冲区会占据大量的内存。
◎ AOF Rewrite结束后,由主线程把AOF Rewrite缓冲区的数据写入磁盘,缓冲区过大会阻塞命令执行,造成Redis耗时尖刺。
◎ AOF Rewrite需要主子进程进行复杂的通信,实现逻辑复杂。
Multi-Part AOF机制就是把单个AOF文件拆分成多个,包括三种不同的类型,不同类型的AOF文件有不同的职责。
◎ Base AOF文件:子进程执行AOF Rewrite操作时生成的文件,有且只有一个。
◎ Incr AOF文件:增量AOF文件,在AOF Rewrite开始时由主进程创建,用于保存在AOF重写期间收到的写操作,可能存在多个这样的文件。
◎ History AOF文件:历史版本的Base AOF文件和Incr AOF文件。AOF Rewrite操作执行完成后,原来的Base AOF文件和Incr AOF文件被标记成History AOF文件,并被Redis自动删除。
当进行AOF Rewrite操作时,Redis主进程会新建一个Incr AOF文件,用于保存整个AOF Rewrite操作期间的AOF文件,不再写入旧的Incr AOF文件。
接着,主线程fork出一个子进程,用于执行AOF Rewrite操作。子进程会生成一个新的Base AOF文件,执行一次内存拷贝,拥有此时的RDB文件,遍历Redis中的全部field-valuepairs,生成重写记录,写入Base AOF文件。
新生成的Base AOF文件与新建的Incr AOF文件结合在一起,就包含了当前Redis的所有数据。AOF Rewrite操作结束后,主线程会使用一个manifest文件来维护这些AOF文件的信息,其实就是记录新生成的Base AOF文件与新建的Incr AOF文件信息,同时把之前的Base AOF和Incr文件标记成History。
你会发现,在整个AOF Rewrite操作过程中,不再重复写AOF文件,也没有使用AOFRewrite缓冲区暂存日志,如图3所示。
-
AOF优缺点
AOF的主要优点如下。
- 持久化实时性高。
- 是一种追加日志,不会出现磁盘寻道问题,也不会在断电时出现损坏问题。即使由于某种原因(例如磁盘已满)中断,redis-check-aof工具也能够轻松修复它。
- 易于理解和解析的格式,包含所有操作的日志。
- 写操作执行成功才记录日志,避免了命令语法检查开销,同时不会阻塞当前写命令。
AOF的主要缺点如下。
- 由于AOF记录的是一个个命令,因此在故障恢复时要执行所有命令,如果文件太大,那么整个恢复过程会非常缓慢。
- 文件系统对文件大小有限制,不能保存过大的文件,文件变大,追加效率也会变低。
- 命令执行完成,如果在写日志之前宕机,那么会丢失数据。
- AOF避免了当前命令的阻塞,但是可能会给下一个命令带来阻塞的风险。AOF文件由主进程执行,在日志写入磁盘过程中,如果磁盘压力过大就会导致写入过程很慢,从而导致后续的写命令阻塞。
在重启redis时,我们很少使用RDB文件来恢复内存状态,因为会丢失大量数据。我们通常使用AOF文件重放,但是重放AOF文件的性能相对RDB文件要慢很多,在redis实例很大的情况下,这样启动需要花费很长时间。在redis4.0版本中提供了一个混合使用AOF文件和RDB快照的方法。简单来说,RDB文件以一定的频率执行,使用AOF文件记录两次快照之间的所有写操作。如此一来,就不需要频繁执行快照,避免了fork对主进程的性能影响,AOF文件也不再是全量的,而是生成RDB文件时间的增量AOF文件,这样日志就会很小,不需要重写。
如果一个服务器上既有RDB文件,又有AOF文件,那么当我们重启时,将优先选择AOF文件来恢复数据,因为它保存的数据更完整,如果没有AOF文件,则加载RDB文件。
主从复制架构
高可用有两个含义:一是数据尽量不丢失,而是尽可能提供服务。AOF和RDB快照保证了数据持久化尽量不丢失,而主从复制就是增加副本,将一份数据保存到多个实例上,即使有一个实例宕机,其他实例依然可以提供服务。
通过主从复制,将一份冗余数据复制到其他redis服务器,实现高可用。当只有一台服务器时,一旦宕机就无法提供服务,那么如果有多台服务器,是不是可以解决问题了?我们将前者称为master(主节点),后者称为slave(从节点),数据的复制是单向的,只能由master到slave。
在默认情况下,每台redis服务器都是master,且一个master可以有多个slave(或没有slave),但一个slave只能有一个master。
master和slave之间的数据如何保证一致性呢?
- 读操作:master和slave都可以执行。
- 写操作:master先执行,之后将写操作同步到slave。
为了保证副本数据的一致性,主从架构采用了读/写分离的方式,master在执行修改操作时,会把相应的写命令同步给slave,slave回收这些命令,就可以保证自己的主从数据一致。
主从复制还有其他作用吗?
- 故障恢复:当master宕机时,其他节点依然可以提供服务。
- 负载均衡:master提供写服务,slave提供读服务,分担压力。
- 高可用基石:哨兵和集群实施的基础。
主从数据同步原理
主从数据同步分为4种情况
- master和slave第一次全量同步。
- master和slave正常运行期间的数据同步。
- master和slave网络断开重连同步。
- 无盘复制。
在介绍实现原理之前,先看下如何配置主从复制
javascript
replicaof <masterip> <masterport> #建立主从关系命令,配置该节点为其他节点的slave
replicao-read-only yes #slave只读
repl-backlog-size 128mb #积压缓冲区大小,在master与slave断线重连后,如果是增量复制,master就从缓冲区里取出数据复制给slave
-
全量同步
主从库第一次复制过程大体可以分为3个阶段:建立连接阶段、同步数据阶段、发送同步期间接收的新写命令到slave阶段。
- 建立连接
第一阶段的主要作用是在master和slave之间建立连接,为数据全量同步做好准备。建立信任,才能开始同步。
slave和master建立连接后,根据配置信息得到master的连接地址,就发送psync命令并告诉master即将进行同步,主库确认回复后,主从库就进入下一阶段的同步了。
在slave的配置文件中的replicaof配置项中配置了master的IP地址和port,slave就知道自己要和哪个master进行连接了。slave内部维护了masterhost和masterport两个字段,用于存储master的IP地址和port信息。从库发送的psync命令包含主库的replid和复制进度offset两个参数。
javascriptPSYNC <runID> <offset> #runID:每个redis实例启动时都会自动生成一个唯一标识ID,在第一次主从复制时,slave还不知道主库runID,所以sunID会被配置为"?" #repl_offset:记录当前复制进度偏移量,slave记录的偏移量与master记录的偏移量之间的数据差,就是需要复制的增量数据。第一次主从复制,将repl_offset配置为-1,表示全量复制。
master收到slave的PSYNC命令后,一看是"?"和"-1",表示第一次要进行全量复制,并向slave回复+FULLRESYNC <runID> <repl_offset>。
-
同步阶段
进入第二阶段,redis master执行bgsave命令生成RDB文件,并将文件发送给slave。slave收到RDB文件并将其保存到磁盘,清空当前数据库的数据,再加载RDB文件数据到内存中,同时会把master的runID和master_offset记录下来。RDB文件保存的只是某一时刻redis的内存快照,此后master接收到的写命令没有传输到slave,所以master为每个slave开辟一块replication buffer缓冲区记录从生成RDB文件开始收到的所有写命令。
第三阶段,slave加载RDB文件后,master把replication buffer缓冲区的数据发送到slave,主从数据保持一致。redis会为每个连接到master的slave开辟一个replication buffer缓冲区,因为每个slave开始同步的时刻可能不一样,所以要分别配置缓冲区。只要slave和master建立好连接,对应的缓冲区就会被创建,断开连接,这个缓存区就会被释放。
一个在master端上创建的缓冲区,存放的数据是下面三个时间内所有的master数据写操作。
- master执行bgsave产生RDB文件期间的写操作。
- master发送RDB文件到slave网络传输期间的写操作。
- slave加载RDB文件把数据恢复到内存期间的写操作。
无论是和客户端通信,还是和slave通信,redis都分配一个内存buffer进行数据交互,客户端是一个client,slave也是一个client。每个client连上redis后,redis都会为其分配一个专门client buffer,所有数据交互都是通过这个buffer进行的。master先把数据写到这个buffer中,然后通过网络发送出去,这样就完成了数据交互。
无论主从在增量同步还是全量同步,master都会为其分配一个buffer,只不过这个buffer专门用来将写命令传播到从库,以保证主从数据一致。
- 建立连接
-
增量同步
在redis2.8之前,如果主从复制在命令传播时出现了网络闪断,slave就会和master重新进行一次全量复制,开销非常大。从redis2.8之后,在网络断开重连后,slave会尝试采用增量复制的方式继续同步。增量复制用于网络中断等情况后的复制,只将中断期间master执行的写命令发送给slave,与全量复制相比更加高效。
断开重连增量复制的实现奥秘就是repl_backlog缓冲区,它是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。不管在什么时候,master都会将写命令记录在repl_backlog中,它记录master接收的新的写请求数据的偏移量和新写命令,这样在slave重新连接后,就可以获取未同步的命令发送给slave了。
master使用master_repl_offset记录自己写到的位置偏移量,slave则使用slave_repl_offset记录已经读取到的偏移量。
master收到写操作,偏移量会增加。slave持续执行同步的写命令后,repl_backlog的已复制的偏移量slave_repl_offset也在不断增加。
在正常情况下,这两个偏移量基本相等,在网络断连阶段,master可能收到新的写操作命令,所以master repl offset会大于slave_repl_offset。
当主从断开重连后,slave回先发送psync命令给master,同时将自己之前保存的master的runID,slave_repl_offset发送给master。master只需要把master_repl_offset与slave_repl_offset之间的差异命令同步给从库即可。增量复制执行流程如下图所示。
需要注意的是,在进行主从复制时,master接收到的写操作在写入replication buffer的同时,还会写入repl_backlog的缓冲区。
- replication buffer:对于每个与master连接的slave,master都会开辟一个replicationbuffer给slave独占。
- repl_backlog是一个环形缓冲区,整个master进程中只会存在一个,由所有的slave共用。repl_backlog的大小通过repl-backlog-size参数配置,默认为1MB,可以根据每秒产生的命令,以及master执行rdb bgsave、master发送RDB文件到slave和slave加载RDB文件的时间之和来估算积压缓冲区的大小,repl-backlog-size值不小于这两者的乘积。
总的来说,replication buffer是主从在进行全量复制时,master用于和slave连接的客户端的buffer,master会为每个client开辟一块,用于传输命令。
repl_backlog_buffer是master专门用于持续保存写操作的buffer,目的是支持从库增量复制。
repl_backlog_buffer在Redis服务器启动后一直接收写操作命令,这是所有slave共享的。master和slave会各自记录自己的复制进度,所以,不同的slave在恢复时会把自己的复制进度(slave_repl_offset)发给master,master就可以和它独立同步了。
一旦被覆盖就会执行全量复制,我们可以调整repl_backlog_size这个参数用于控制缓冲区大小,计算公式如下:
javascriptrepl_backlog = second* write_size_per_second
- second:从服务器断开重连主服务器所需的平均时间。
- write_size_per_second:mater平均每秒产生的命令数据量大小(写命令和数据大小总和)。
安全起见,建议将缓冲区大小设定为2*second* write_size_per_second。
-
正常运行期间的同步
当主从完成了全量复制后,它们之间会一直维护一个网络连接,master通过这个连接将后续收到的命令再传播给slave,这个过程也被称为基于长连接的命令传播,使用长连接的目的就是避免频繁建立连接导致的开销。
-
缓冲区演化
无论是增量复制还是全量复制,当写请求到达master时,命令会分别写入所有slave的replication buffer以及repl_backlog,重复保存太浪费内存了。因此,redis7.0版本开始,采用了共享缓冲区的设计解决重复保存数据的问题------在写命令传播时,将这些写命令放在一个全局的复制缓冲区中,多个slave共享这份数据,不同slave引用缓冲区的不同内容。
-
如何缺点执行全量同步还是部分同步
-
slave根据当前状态,发送psync命令给master。
- 如果slave从未执行过replicaof,则发送psync?-1,向master发送全量复制请求。
- 如果slave之前执行过replicaof,则发送psync,runlD是上次复制保存的master runlD,offset是上次复制截止时slave保存的复制偏移量。
-
master根据接收到的psync命令和当前服务器状态,决定执行全量复制还是部分复制。
- 当master runlD与slave发送的runlD相同,且slave发送的slave_repl_offset之后的数据在repl_backlog_buffer缓冲区中都存在时,回复CONTINUE,表示进行部分复制,slave等待master发送其缺少的数据即可。
- 当master runlD与slave发送的runlD不同,或者slave发送的slave_repl_offset之后的数据已不在master的repl_backlog_buffer缓冲区中(在队列中被挤出了)时,回复slave FULLRESYNC<runlD><offset>,表示要进行全量复制,其中runlD表示master当前的runlD,offset表示master当前的offset,slave保存这两个值,以备使用。
当一个slave节点和master断连时间过长,导致master repl_backlog的slave_repl_offset位置上的数据被覆盖时,master和slave将进行全量复制。
主从同步的缺点
-
主从复制无限循环
replication buffer由client-output-buffer-limit slave配置,如果这个值太小则会导致主从复制连接断开。
- 当master-slave复制连接断开时,master会释放连接相关的数据,replication buffer中的数据也就会丢失了,此时主从之间重新开始复制过程。
- 主从复制连接断开会导致主从重新执行bgsave,以及RDB文件重传操作无限循环的情况。
当master数据量较大,或者master和slave之间的网络延迟较大时,可能导致该缓冲区的大小超过限制,此时master会断开与slave之间的连接。这种情况可能引起"全量复制------>replication buffer溢出导致链接中断------>重连"的循环。
-
内存过大
如果redis单机内存达到10gb,则单个slave的同步时间处于分钟级别,当slave较多时,恢复的速度会更慢。当数据量过大,全量复制阶段master fork子进程和保存RDB文件耗时过大时,slave长时间接收不到数据会触发超时,master和slave的数据同步同样可能陷入"全量复制------>超时导致复制中断------>重连"的循环。
-
主从不一致问题
主从模式下,建议将slave配置为read-only模式,也就是只读,否则有可能导致主从数据不一致。此外,通常还会给slave添加eplica-ignore-maxmemory no配置,不让slave执行内存淘汰的操作,而是由master来决定是否淘汰,field-value pairs失效master会发送DEL命令给slave。
哨兵集群
redis的主从复制时高可用的基石,某个slave宕机依然可以将请求发送给master或者其他slave,但是如果master宕机,则只能响应读操作,写请求无法再执行。所以,主从复制架构面临一个严峻问题:master宕机,无法执行写操作,无法自动选择将一个slave切换为master,也就是无法实现自动故障切换。
redis的哨兵集群高可用架构有3种角色,分别是master、slave和sentinel。
- sentinel之间互相通信,组成一个集群实现哨兵高可用,选举出一个leader执行故障迁移操作。
- master和slave之间通信,组成主从复制架构。
- sentinel与msater/slave通信,是为了对该主从复制架构进行管理,包括监视、通知、自动故障谢欢、配置提供者。
javascript
#sentinel.conf
sentinel monitor mymaster 127.0.0.1 6379 2
哨兵监控的master名字叫做mymaster,master的ip地址是127.0.0.1,端口是6379。quorum是关键参数,它的作用如下:
- 指定在标记master故障并尝试执行故障切换时需要一定数量达成一致意见的哨兵进程。即需要多少个哨兵进程认为master宕机,真正标记master宕机才能启动故障切换过程。
- 对于多个哨兵,需要选出一个leader来执行实际的故障自动转移操作,当某个哨兵的票数超过quorum时,就选举这个哨兵为leader,负责自动故障切换。quorum的值一般取哨兵个数的一半以上(n/2+1)比较合理。
哨兵只要配置master信息即可与三个角色建立联系。
- 哨兵可以通过master获取slave的信息,并与slave建立连接。master与slave是主从关系,通过info命令就可以将通过master获取slave的ip地址和port、runid等信息。
- 通过上面步骤,哨兵与master和所有的slave建立连接,哨兵之间的互相感知则通过redis的发布/订阅机制实现。每个哨兵通过发布/订阅master的__sentinel__:hello频道发布和接收信息,以此感知其他哨兵的存在并建立连接。
哨兵的任务
哨兵是redis的一种运行模式,它专注于对redis实例(master、slave)运行状态的监控,并能够在master发生故障时通过一系列的机制实现选主以及主从切换,实现自动故障切换,确保整个reids系统的可用性。
哨兵的主要职责如下:
- 监控:redis的哨兵不断检查master和slave实例是否按照预期工作,它监视实例的健康状态,包括master和所有的slave。
- 自动故障切换:如果master出现故障或不按照预期工作,redis的哨兵则启动自动故障切换流程。在此过程中,一个slave会被晋升为新的master。
- 通知:让slave执行replicaof命令与新的master同步数据;并且通知客户端与新的master建立连接。
- 配置提供者:哨兵充当客户都安服务发现的权威来源。客户都安连接到任何一个哨兵以获取新的master地址,确保能够连接到正确的实例。
哨兵也是一个redis进程,只是不对外提供读/写服务,哨兵通常被配置为单数,原因如下:
-
监控
-
主观下线
- sdown(主观不可用)是单个哨兵自己主观上检测到的关于Master的状态,从哨兵的角度来看,如果发送PING心跳后,在一定的时间内没有得到应有的回复,就达到了sdown的条件。
- 哨兵配置文件sentinel.conf中down-after-milliseconds属性设置了判断主观下线的回复时间。
javascript# sentinel down-after-milliseconds mymaster 30000 默认30s sentinel down-after-milliseconds <masterName> <timeout>
-
客观下线
Master是否下线不是单个Sentinel能够决定的,一般来说需要一定数量的哨兵,多个哨兵达成一致意见才能认为一个Master客观上已经宕机了。
上面的图可以看到,我们一般会有个Sentinel集群 ,这时候这个集群就发挥作用了,通过投票机制,超过指定数量(一般为半数)的Sentinel 都判断了『主观下线』 ,这时候我们就把 Master 标记为『客观下线』,代表它确实不可用了。
投票判定的数量是通过sentinel.conf配置的:javascript# sentinel monitor <master-name> <master-host> <master-port> <quorum> # 举例如下: sentinel monitor master 127.0.0.1 6379 2
这条配置项用于告知哨兵需要监听的主节点:
- sentinel monitor:监控标识
- mymaster:这边可以放上主节点的名称
- 192.168.11.128 6379:代表监控的主节点 ip,port。6379是redis常规端口。
- 2:判定的sentinel数量,果你有3个 Sentinel,并且 quorum 设置为 2,那么至少需要有2个 Sentinel 认定 Master 节点不可用时(sdown),才会触发故障转移,执行 failover 操作。
-
-
自动故障切换
哨兵的第二个任务是选择一个slave作为新的master,并对外提供服务,之后其他的slave会与新的master进行主从复制,这个过程叫做自动故障切换。
redis如何从众多的slave筛选出一个作为master?redis有自己的筛选规则,按照一定的筛选条件和打分策略,选出一个"节点"担任master。
筛选条件- 下线或网络断联的slave直接丢弃。
- 网络无异常:slave最后一次响应PING命令的时间不能超过5倍PING周期;slave INFO(每10s发送一次INFO命令)的信息更新时间不能超过3倍INFO刷新周期。
- 评估过往的网络状态:slave与master断开连接,断连时间不能超过(现在-master被标记为下线的时间)+(master的down-after-milliseconds配置项的值乘以10),单位毫秒。
打分
过滤掉不合适的slave之后,使用快速排序对slave列表进行打分,按照以下排序找出"玩者"。
- slave优先级:通过replica-priority 100 配置项。给不同的slave配置不同优先级,默认值是00,值越低,优先级越高,配置为0表示不会晋升为master。
- 更大复制偏移量:已复制的数据量越多,slave_repl_offset与master_repl_offset的差值越小。
- slave runID:在优先级和复制进度都相同的情况下,runID最小的slave得分越高,该slave会被选为新的master。
哨兵向筛选出来的slave发送slave no one 命令,使得该slave成为新的master,哨兵并不关心命令返回的结果,它会发送info命令给slave,并根据命令的回复内容确认slave是否成功转换为master。原master恢复正常,重新连接哨兵,这时集群已经有新的master,所以旧的master就降级为slave。
-
通知
新的master出现后,哨兵还有一件重要的事情要做------将新的master的连接信息通过slave命令发送给其他slave,通知slave执行replicaof 命令和新的master建立连接进行主从复制。接着,哨兵会定时给slave发INFO命令,从INFO命令的回复内容来确认slave是否与新的master成功建立连接。检测到所有slave都与新的master建立连接,自动故障切换就完成了。如果还有剩余slave没有连接上新的master,哨兵则会再次发起slave命令,要求它们与新的master建立连接。
-
配置提供者
redis客户端只需要跟哨兵打交道,就可以无感知连接新的master,最重要的原因是哨兵提供了一些api来检查主从节点的运行状况。
哨兵集群原理
如果只有一个哨兵就会存在单点故障问题。redis sentinel是一个分布式系统,由多个哨兵写作文组成集群实现高可用。
在配置哨兵集群的时候,哨兵配置中只设置了监控的 master IP 和 port,并没有配置其他哨兵的连接信息。
javascript
sentinel monitor <master-name> <ip> <redis-port> <quorum>
哨兵之间是如何知道彼此的?如何知道 slave 并监控他们的?由哪一个「哨兵」执行主从切换呢?------pub/sub 实现哨兵间通信和发现 slave。
哨兵与 master 建立通信,利用 master 提供发布/订阅机制发布自己的信息,比如IP、端口......
master 有一个 _sentinel _:hello 的专用通道,用于哨兵之间发布和订阅消息。这就好比是 sentinel:hello 微信群,哨兵利用 master 建立的微信群发布自己的消息,同时关注其他哨兵发布的消息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口,从而相互发现建立连接。
哨兵之间虽然建立连接了,但是还需要和 slave 建立连接,不然没法监控他们呀,如何知道 slave 并监控他们的?
的确,哨兵之间建立连接形成集群还不够,还需要跟 slave 建立连接,不然没法监控他们,无法对主从库进行心跳判断。除此之外,如果发生了主从切换也得通知 slave 重新跟新 master 建立连接执行数据同步。关键还是利用 master 来实现,哨兵向 master 发送 INFO 命令, master 自然是知道所有的 salve的。所以 master 接收到命令后,便将 slave 列表告诉哨兵。哨兵根据 master 响应的 slave 名单信息与每一个 salve 建立连接,并且根据这个连接持续监控哨兵。
如图所示,哨兵 2 向 Master 发送 INFO 命令,Master 就把 slave 列表返回给哨兵 2,哨兵 2 便根据 slave 列表连接信息与每一个 slave 建立连接,并基于此连接实现持续监控。剩下的哨兵也同理基于此实现监控。
Redis集群
当数据量大时,我们常常会遇到一个十分糟心的问题:Redis响应有时候会特别慢。这时我们可以使用INFO命令查看latest_fork_usec指标(最近一次fork耗时)。
- latest_fork_usec:fork子进程完成RDB文件持久化操作,其耗时与redis数据量正相关。fork子进程执行时会阻塞主进程,数据量过大会导致主进程阻塞时间过长,这样会导致redis响应慢的现象。
redis集群主要解决大数据量存储导致的响应变慢的问题,同时便于横向扩展。redis数据的两种扩展方案是垂直扩展和水平扩展。
- 垂直扩展:升级单个redis的硬件配置,例如增加内存容量、磁盘容量和使用更强大的cpu。
- 水平扩展:横向增加redis的实例个数,每个节点负责一部分数据。
在面向百万、千万级别规模的用户时,横向扩展redis集群会是一个非常好的选择。
Redis集群是什么
redis集群是一种分布式数据库方案,通过分片进行数据管理,并提供复制和自动故障切换功能。
redis集群并没有使用一致性哈希算法,而是将数据划分为16384个slot,每个节点负责一部分slot,slot,slot的信息存储在节点中,节点之间通过Gossip协议交互集群信息,最后每个节点都保存着其他节点的slots分配情况。
-
组建redis集群
一个redis集群通常由多个节点组成,在开始时,每个节点都是独立的,要组建一个真正可工作的redis集群,必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
连接各个节点的工作可以通过CLUSTER MEET命令完成:CLUSTER MEET <ip> <port>。向node节点发送CLUSTER MEET命令,可以让其与ip地址和端口所指定的节点进行握手,当握手成功时,node节点就会将ip地址和端口所指定的节点添加到node节点所在的集群中。
-
redis集群原理
各位读者可以参考下面这篇博客。
Redis Cluster集群运维与核心原理剖析【收获满满】 -
集群配置注意事项
-
降低节点间的通信开销
redis集群的实例每100ms就会扫描一次本地实例列表,当发现有实例最近一次收到PONG消息的时间大于(cluster-node-timeout)/2时,就立刻向这个实例发送PING消息,更新这个实例的状态信息。
集群规模变大会导致实例间网络通信延迟增加,可能引起频繁发送PING消息。
- 每个实例每秒发送一条PING消息,降低这个频率可能导致集群中实例的状态信息无法及时传播。
- 每100 ms检测实例接收PONG消息的时间是否超过(cluster-node-timeout)/2,这是Redis实例默认的周期性检测任务频率,我们不会轻易修改。只能修改cluster-node-timeout的值,这是Redis集群中判断实例是否故障的心跳时间,默认为15s。所以,为了避免过多的心跳消息占用Redis集群宽带,可以将cluster-node-timeout配置为20s或者30s,这样PONG消息接收超时的情况就会缓解。但是,也不能将这个值配置得太大,否则当实例发生故障时需要等待过长时间,影响集群正常服务。
-
cluster-migration-barrier
没有slave的master被称为孤儿master,这个配置用于防止出现孤儿master。
当某个master的slave宕机后,集群会从其他master中选出一个冗余的slave迁移过来,确保每个master至少有一个slave,以免孤儿master宕机时,没有slave可以升为master导致集群不可用。
默认配置为cluster-migration-barrier 1,这是一个迁移临界值。
-