一、前言
在之前的文章已经详细介绍了redis入门基础已经持久化相关内容包括redis4.0所提供的混合持久化。
通过持久化功能,Redis保证了即使在服务器宕机情况下数据的丢失非常少。但是如果这台服务器出现了硬盘故障、系统崩溃等等,不仅仅是数据丢失,很可能对业务造成灾难性打击。为了避免单点故障通常的做法是将数据复制多个副本保存在不同的服务器上,这样即使有其中一台服务器出现故障,其他服务器依然可以继续提供服务。当然Redis提供了多种高可用方案包括:主从复制、哨兵模式的主从复制、以及集群。
本文将详细介绍Redis从2.6以到4.0提供复制方案的演进,也包括:主从复制、复制原理以及相关实践。
二、主从复制
简介
在主从复制中,数据库分为两类,一类是主库(master),另一类是同步主库数据的从库(slave)。主库可以进行读写操作,当写操作导致数据变化时会自动同步到从库。而从库一般是只读的(特定情况也可以写,通过参数slave-read-only指定),并接受来自主库的数据,一个主库可拥有多个从库,而一个从库只能有一个主库。这样就使得redis的主从架构有了两种模式:一类是一主多从如下图1,二类是“链式主从复制”--主->从->主-从如下图2。
对于一主多从的复制架构不必多说,这里解释下链式主从复制:如上图2,主库A的数据会同步到从库B和从库C,而B又是从库D和从库E的主库,所以B的数据会同步到从库D和从库E。如果向B中写数据,数据只能同步到D和E中,所以对于这种架构数据的一致性将不能保持,也不推荐这种架构。
搭建配置主从
由于没有过多的机器,这里将使用一台机器上启动多个redis实例实现主从复制。
对于redis来说搭建主从非常容易,引用官网一句话来说:there is a very simple to use and configure leader follower (master-slave) replication。
本次实践分别以 10.1.210.68:6379 作为主,两个从服务器分别是 10.1.210.69:6380 和 10.1.210.69:6381。
搭建步骤:
- 将redis.conf文件拷贝三份,名称最好有显示的区别,我这里采用名字为 6379.conf、 6380.conf、 6381.conf;
- 分别修改三个文件的ip(默认127.0.0.1可以不用修改)、端口(尽量和配置文件一致)、pid文件,日志文件,持久化数据目录(dir)、后台运行(daemonize yes);
- 使用启动命令脚本启动每个redis服务;
- 设置主从关系、验证主从同步;
示例:
步骤一:
#建立三个redis目录 mkdir -p /opt/db/{redis6379,redis6380,redis6381} #从源码中拷贝配置文件 cp redis-stable/redis.conf /opt/db/redis6379/6379.conf cp redis-stable/redis.conf /opt/db/redis6380/6380.conf cp redis-stable/redis.conf /opt/db/redis6381/6381.conf
步骤二:
修改配置项如下:找到对应的参数修改即可,下面是每个配置文件修改部分、本机器IP地址是10.1.210.69;
daemonize yes #修改redis为后台运行模式 pidfile /var/run/redis_6379.pid #修改运行的redis实例的pid,不能重复 logfile "/opt/db/redis6379/6379.log" #指明日志文件 dir "/opt/db/redis6379" #工作目录,存放持久化数据的目录 bind 10.1.210.69 #监听地址,如果是单机多个示例可以不用修改 port 6379 #监听端口,保持和配置文件名称端口一致
daemonize yes #修改redis为后台运行模式 pidfile /var/run/redis_6380.pid #修改运行的redis实例的pid,不能重复 logfile "/opt/db/redis6380/6380.log" #指明日志文件 dir "/opt/db/redis6380" #工作目录,存放持久化数据的目录 bind 10.1.210.69 #监听地址,如果是单机多个示例可以不用修改 port 6380 #监听端口,保持和配置文件名称端口一致
daemonize yes #修改redis为后台运行模式 pidfile /var/run/redis_6381.pid #修改运行的redis实例的pid,不能重复 logfile "/opt/db/redis6379/6381.log" #指明日志文件 dir "/opt/db/redis6381" #工作目录,存放持久化数据的目录 bind 10.1.210.69 #监听地址,如果是单机多个实例可以不用修改使用127.0.0.1 port 6381 #监听端口,保持和配置文件名称端口一致
步骤三:
启动每个redis实例
redis-server /opt/db/redis6379/6379.conf redis-server /opt/db/redis6380/6380.conf redis-server /opt/db/redis6381/6381.conf
步骤四:
设置主从关系,当然你可以直接指明从库配置文件直接使用slaveof <masterip> <masterport>指定,这里我在用客户端修改,分别使用客户端redis-cli命令连入端口为6380、6381的redis。
连入6380数据库,使用redis-cli -h 10.1.210.69 -p 6380,其中-h代表ip地址,-p代表端口,并执行slaveof 10.1.210.69 6379,并写入配置文件config rewrite,如下:
同样我们在从库6381执行相同操作:
此时我们在使用info Replication 查看相关主从信息:
同时,还可以测试主从功能,在6379上创建key,在从库查看:
主库:
从库:
三、复制原理
了解redis复制原理对日后运维有很大帮助,包括如何规划节点,如何处理节点故障,redis复制过程可分为三个阶段:
- 复制初始化阶段
- 数据同步阶段
- 命令传播阶段
复制初始化阶段
当执行完slaveof masterip port 命令时候,从库根据指明的master节点ip和port向主库发起socket连接,主库收到socket连接之后将连接信息保存,此时连接建立;
当socket连接建立完成以后,从库向主库发送ping命令,以确认主库是否可用,此时的结果返回如果是PONG则代表主库可以用,否则可能出现超时或者主库此时在处理其他任务阻塞那么此时从库将断开socket连接,然后进行重试;
如果主库连接设置了密码,则从库需要设置masterauth参数,此时从库会发送auth命令,命令格式为“auth + 密码”进行密码验证,其中密码为masterauth参数配置的密码,需要注意的是如果主库设置了密码验证,从库未配置masterauth参数则报错,socket连接断开。
当身份验证完成以后,从节点发送自己的监听端口,主库保存其端口信息,此时进入下一个阶段:数据同步阶段。
数据同步阶段
主库和从库都确认对方信息以后,便可开始数据同步,此时从库向主库发送psync命令(需要注意的是redis4.0版本对2.8版本的psync做了优化,后续会进行说明),主库收到该命令后判断是进行增量复制还是全量复制,然后根据策略进行数据的同步,当主库有新的写操作时候,此时进入复制第三阶段:命令传播阶段。
命令传播阶段
当数据同步完成以后,在此后的时间里主从维护着心跳检查来确认对方是否在线,每隔一段时间(默认10秒,通过repl-ping-slave-period参数指定)主节点向从节点发送PING命令判断从节点是否在线,而从节点每秒1次向主节点发送REPLCONF ACK命令,命令格式为:REPLCONF ACK {offset},其中offset指从节点保存的复制偏移量,作用一是汇报自己复制偏移量,主节点会对比复制偏移量向从节点发送未同步的命令,作用二在于判断主节点是否在线,从库接送命令并执行,最终实现与主库数据相同。
乐观复制
redis采用量乐观复制策略,容忍在一定时间内主从数据内容是不同的,但是两者的数据最终会同步。
四、redis复制演进
sync&psync1&psync2
从redis2.6到4.0开发人员对其复制流程进行逐步的优化,以下是演进过程:
- redis版本<=2.6<2.8,复制采用sync命令,无论是第一次主从复制还是断线重连进行复制都采用全量复制;
- 2.8<=redis版本<4.0,复制采用psync,从redis2.8开始,redis复制从sync过渡到psync,这一特性主要添加了redis在断线重新时候可使用部分复制;
- redis版本>=4.0,也采用psync,相比与2.8版本的psync优化了增量复制,这里我们称为psync2,2.8版本的psync称为psync1。
以下将分别说明各个版本的复制演进。
sync
在redis2.6以及以前的版本,复制采用sync命令,当一个从库启动后,会向主库发送sync命令,主库收到sync命令后执行bgsave后台保存RDB快照(该过程在上一篇已经详细介绍),同时将保存快照的将快照保存期间接受的写命令保存到缓冲队列。当快照完成以后,主库将快照文件已经缓存的所有命令发送给从库,从库接受到快照文件并载入,再将执行主库发送的命令,也就是上面我们介绍的复制初始化阶段和数据同步阶段,其后就是命令增量同步,最终主库与从库保持数据一直。
当从库在某些情况断线重连(如从库重启、由于网络原因主从连接超时),重复上述过程,进行数据同步。由此可见,redis2.6版本以及2.6以前复制过程全部采用全量复制。
sync虽然解决了数据同步问题,但是在数据量比较大情况下,从库断线从来依然采用全量复制机制,无论是从数据恢复、宽带占用来说,sync所带来的问题还是很多的。于是redis从2.8开始,引入新的命令psync。
psync1
在redis2.8版本,redis引入psync命令来进行主从的数据同步,这里我们称该命令为psync1。psync1实现依赖以下三个关键点:
1.offset(复制偏移量):
主库和从库分别各自维护一个复制偏移量(可以使用info replication查看),用于标识自己复制的情况,在主库中代表主节点向从节点传递的字节数,在从库中代表从库同步的字节数。每当主库向从节点发送N个字节数据时,主节点的offset增加N,从库每收到主节点传来的N个字节数据时,从库的offset增加N。因此offset总是不断增大,这也是判断主从数据是否同步的标志,若主从的offset相同则表示数据同步量,不通则表示数据不同步。以下图示分别代表某个时刻两个主从的同步情况(以下是4.0版本截图):
2.replication backlog buffer(复制积压缓冲区):
复制积压缓冲区是一个固定长度的FIFO队列,大小由配置参数repl-backlog-size指定,默认大小1MB。需要注意的是该缓冲区由master维护并且有且只有一个,所有slave共享此缓冲区,其作用在于备份最近主库发送给从库的数据。
在主从命令传播阶段,主节点除了将写命令发送给从节点外,还会发送一份到复制积压缓冲区,作为写命令的备份。除了存储最近的写命令,复制积压缓冲区中还存储了每个字节相应的复制偏移量(如下图),由于复制积压缓冲区固定大小先进先出的队列,所以它总是保存的是最近redis执行的命令。
3.run_id(服务器运行的唯一ID)
每个redis实例在启动时候,都会随机生成一个长度为40的唯一字符串来标识当前运行的redis节点,查看此id可通过命令info server查看。
当主从复制在初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来,当断线重连时,从节点会将这个runid发送给主节点。主节点根据runid判断能否进行部分复制:
- 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会更具offset偏移量之后的数据判断是否执行部分复制,如果offset偏移量之后的数据仍然都在复制积压缓冲区里,则执行部分复制,否则执行全量复制;
- 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的redis节点并不是当前的主节点,只能进行全量复制;
介绍完三个概念以后,接下来就可以介绍redis2.8提供的psync命令实现过程,如下图:
图文说明:
- 如果从服务器以前没有复制过任何主服务器,或者之前执行过SLAVEOF no one命令,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC ? -1命令,主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步);
- 相反地,如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC <runid> <offset>命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作,如何判断已经在介绍runid时进行详细说明。
根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:
- 如果主服务器返回+FULLRESYNC <runid> <offset>回复,那么表示主服务器将与从服务器执行完整重同步操作:其中runid是这个主服务器的运行ID,从服务器会将这个ID保存起来,在下一次发送PSYNC命令时使用;而offset则是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量;
- 如果主服务器返回+CONTINUE回复,那么表示主服务器将与从服务器执行部分同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了;
- 如果主服务器返回-ERR回复,那么表示主服务器的版本低于Redis 2.8,它识别不了PSYNC命令,从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作。
由此可见psync也有不足之处,当从库重启以后runid发生变化,也就意味者从库还是会进行全量复制,而在实际的生产中进行从库的维护很多时候会进行重启,而正是有由于全量同步需要主库执行快照,以及数据传输会带不小的影响。因此在4.0版本,psync命令做了改进,以下说明。
psync2
redis4.0新版本除了增加混合持久化,还优化了psync(以下称psync2)并实现即使redis实例重启的情况下也能实现部分同步,下面主要介绍psync2实现过程。psync2在psync1基础上新增两个复制id(可使用info replication 查看如下图):
-
master_replid: 复制id1(后文简称:replid1),一个长度为41个字节(40个随机串+’0’)的字符串,每个redis实例都有,和runid没有直接关联,但和runid生成规则相同。当实例变为从实例后,自己的replid1会被主实例的replid1覆盖。
-
master_replid2:复制id2(后文简称:replid2),默认初始化为全0,用于存储上次主实例的replid1。
在4.0之前的版本,redis复制信息完全丢失,所以每个实例重启后只能进行全量复制,到了4.0版本,任然可以使用部分同步,其实现过程:
第一步:存储复制信息
redis在关闭时,通过shutdown save,都会调用rdbSaveInfoAuxFields函数,把当前实例的repl-id和repl-offset保存到RDB文件中,当前的RDB存储的数据内容和复制信息是一致性的可通过redis-check-rdb命令查看。
第二步:重启后加载RDB文件中的复制信息
redis加载RDB文件,会专门处理文件中辅助字段(AUX fields)信息,把其中repl_id和repl_offset加载到实例中,分别赋给master_replid和master_repl_offset两个变量值,特别注意当从库开启了AOF持久化,redis加载顺序发生变化优先加载AOF文件,但是由于aof文件中没有复制信息,所以导致重启后从实例依旧使用全量复制!
第三步:向主库上报复制信息,判断是否进行部分同步
从实例向主库上报master_replid和master_repl_offset+1;从实例同时满足以下两条件,就可以部分重新同步,否则执行全量同步:
- 从实例上报master_replid串,与主实例的master_replid1或replid2有一个相等,用于判断主从未发生改变;
- 从实例上报的master_repl_offset+1字节,还存在于主实例的复制积压缓冲区中,用于判断从库丢失部分是否在复制缓冲区中;
psync2除了解决redis重启使用部分同步外,还为解决在主库故障时候从库切换为主库时候使用部分同步机制。redis从库默认开启复制积压缓冲区功能,以便从库故障切换变化master后,其他落后该从库可以从缓冲区中获取缺少的命令。该过程的实现通过两组replid、offset替换原来的master runid和offset变量实现:
- 第一组:master_replid和master_repl_offset:如果redis是主实例,则表示为自己的replid和复制偏移量; 如果redis是从实例,则表示为自己主实例的replid1和同步主实例的复制偏移量。
- 第二组:master_replid2和second_repl_offset:无论主从,都表示自己上次主实例repid1和复制偏移量;用于兄弟实例或级联复制,主库故障切换psync。
判断是否使用部分复制条件:如果从库提供的master_replid与master的replid不同,且与master的replid2不同,或同步速度快于master; 就必须进行全量复制,否则执行部分复制。
以下常见的主从切换都可以使用部分复制:
- 一主一从发生切换,A->B 切换变成 B->A ;
- 一主多从发生切换,兄弟节点变成父子节点时;
- 级别复制发生切换, A->B->C 切换变成 B->C->A;
用一句redis开发者话来说psync2,尽管它不是非常完美,但是已经非常适用。
五、马上实践
为了演示4.0的psync2功能,这里做实践演示。主从实例采用上述搭建的主从架构,主库:10.1.210.69:6379 、从库:10.1.210.69:6380和10.1.210.69:6381。首先关闭一台从节点10.1.210.69:6380:
查看日志从库执行了RDB快照:
查看RDB文件,里面记录了相关复制信息:
此时我们在启动从库,查看对应日志,此时启用部分复制来恢复数据:
之前提到过,当从库开启来AOF持久化时候,重启时加载文件从AOF文件中加载,但是AOF文件中没有对应的复制信息,所以从实例依旧采用全量复制。以下是从库开启AOF持久化后,重启日志,可以看到是全量同步:
六、总结
复制演进中解决的问题
早起版本才用的sync同步方法,虽然实现了简单的主从同步,但是在从库断线或重启时无法实现部分同步,由此在2.8版本推出psync1,redis2.8的部分同步机制,有效解决了网络环境不稳定、redis执行高时间复杂度的命令引起的复制中断,从而导致全量同步。但在应对从库重启和主库故障切换的场景时,psync1还是需进行全量同步。于是在4.0新的psync得到了加强,redis4.0通过在关闭时候执行RDB快照,将复制信息保存在RDB中等到重新启动时加载RDB文件载入复制信息,通过对比复制信息启用部分复制,有效的解决了psync1情形下从库重启复制信息丢失而导致全量同步的问题。同时引入两组replid、offset,主从切换时交换两组值来实现主从故障切换时候依旧采用部分复制。
再次强调,在上述的过程的实现是从库不开启AOF持久化情况下,如果从库开启的AOF持久化,重启时候依然使用全量复制。
故障切换
在实际生产环境中,在没有哨兵的主从架构里如果要重启从库,比较好的方式是先动态调配主库中的复制积压缓冲队列,调整大小应大于某个N值,N值计算公式:backlog_size = 重启从实例时长 * 主实例offset每秒写入量,这样好处在于,即使从库重启断线一段时间后再启动任然保持部分复制。调整方式通过config set repl-backlog-size <字节数>,当我们重启完成后又可以将
repl-backlog-size重新调回原有值。当然在数据量小或者重启时间短情况下,也可以直接重启从节点。
当主库宕机时候,在没有哨兵情况下,需要现将从节点中的某一台提升为主库,我们需要在所有从节点中找到当前的offset最大值的从库(这样代表该库相对其他从库数据较全),然后执行slaveof no one将该从库提升为主库,最后将所有其他重库依次执行slaveof ip port(ip和port是新主库的IP地址和端口),最后完成故障切换。此外,redis4.0中这种切换任然采用部分复制进行数据同步。
主从配置参数
########从库############## slaveof <masterip> <masterport> #设置该数据库为其他数据库的从数据库 masterauth <master-password> #主从复制中,设置连接master服务器的密码(前提master启用了认证) slave-serve-stale-data yes # 当从库同主库失去连接或者复制正在进行,从库有两种运行方式: # 1) 如果slave-serve-stale-data设置为yes(默认设置),从库会继续相应客户端的请求 # 2) 如果slave-serve-stale-data设置为no,除了INFO和SLAVOF命令之外的任何请求都会返回一个错误"SYNC with master in progress" slave-priority 100 #当主库发生宕机时候,哨兵会选择优先级最高的一个称为主库,从库优先级配置默认100,数值越小优先级越高 slave-read-only yes #从节点是否只读;默认yes只读,为了保持数据一致性,应保持默认 ####主库############## repl-disable-tcp-nodelay no #在slave和master同步后(发送psync/sync),后续的同步是否设置成TCP_NODELAY假如设置成yes,则redis会合并小的TCP包从而节省带宽,但会增加同步延迟(40ms),造成master与slave数据不一致假如设置成no,则redis master会立即发送同步数据,没有延迟 #前者关注性能,后者关注一致性 repl-ping-slave-period 10 #从库会按照一个时间间隔向主库发送PING命令来判断主服务器是否在线,默认是10秒 repl-backlog-size 1mb #复制积压缓冲区大小设置 repl-backlog-ttl 3600 #master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。单位为秒。 min-slaves-to-write 3 min-slaves-max-lag 10 #设置某个时间断内,如果从库数量小于该某个值则不允许主机进行写操作,以上参数表示10秒内如果主库的从节点小于3个,则主库不接受写请求,min-slaves-to-write 0代表关闭此功能。