39:Redis的持久化与主从同步

377 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


Redis的高效在于其纯内存运算,但是有得就有失,数据全部存在内存中意味着一旦宕机,数据将会全部丢失,因此必须需要一种机制来保证Redis中的数据不会因为故障而丢失,这就需要Redis拥有数据持久化的能力。

1 持久化

Redis的持久化机制有两种,一种是快照,也就是RDB(Redis DataBase),一种是AOF(Append Only File)日志。

1.1 快照(RDB)

快照是一次性的全量备份,将某一时刻的全量数据以二进制序列化的形式存储,在空间上非常紧凑,能大大缩小存储所用的空间。

Redis是单线程,而文件IO操作是不支持多路复用的。这难道意味着在进行内存快照时Redis需要停止服务?这当然是不行的,那有指令时服务,没指令时持久化这样边持久化边服务?可是这样的话持久化的同时内存数据还在被指令修改,如果在持有化一个大的Hash字典时,过来一个指令把这个字段删了,这个可怎么办? 显然这样也不行。

为此,Redis使用操作系统的多进程COW(Copy On Write)机制来实现快照持久化。

Redis在持久化时会调用glibc的函数fork产生一个子进程,快照持久化交给子进程处理,父进程继续提供服务。子进程生成时和父进程共用代码段和数据段。也就是说这时间父子进程共享内存数据,因此在分离的一瞬间,内存消耗几乎没有。接下来子内存进行数据持久化,他仅仅是读取,不会修改内存。而父进程对外提供服务,修改数据,但是操作系统的COW机制会进行数据段页面的分离,数据段由操作系统的页面组合而成,父进程修改数据时,COW机制就将数据所在页复制一份出来,父进程在这个复制出来的数据也修改,此时原数据页也就是子线程访问的数据页还是原样,也就是子进程所看到的数据在子进程产生的一瞬间就已经凝固了,可以安心复制,这也是为什么这种持久化方法称为快照原因。 随着父进程的修改,会有越来越多的页面被赋值,但是最多也就是全复制,达到原内存空间的二倍,但是这在大数据量情况下很难发生,因为总会有冷数据存在,而且可能占据多数,所以复制的一般只会是其中的一部分。另外提一下:一个页面的大小是4K。

1.2 AOF日志

AOF日志是连续性的增量备份,记录的是修改内存数据的指令记录文本。这样就可以通过对一个空的Redis实例顺序执行记录的命令,也就是重放,来复原实例。Redis在收到修改指令后,会先进行校验,如果没问题,会首先把指令追加记录磁盘上的AOF日志中,然后再执行指令,这样即使突发宕机,重放时也能重放到这个指令。

AOF日志随着运行时间的增长会变的越来越庞大,Redis重启时需要加载AOF日志进行指令重放所需的时间也会更加漫长,所以需要定期对AOF重写,进行瘦身。

1.2.1 AOF重写

AOF重写原理就是开辟一个子进程,然后将内存数据遍历并转换成指令,再记录到一个新的AOF文件中,完毕后再将期间发生的增量AOF日志追加到新的AOF日志中,替换旧的AOF文件,就完成了AOF重写的工作,完成了瘦身。

1.2.2 fsync

AOF日志是以文件方式存在的,程序对AOF日志进行操作时,实际上是先将内容写到内核为文件描述符分配的一块内存缓存上,然后内核异步将数据写入磁盘的。

但是如果机器突然宕机,内存缓存中的数据还没来的及写入磁盘,就会出现日志的丢失。Linux的gilbc提了了fsync(int fd)函数来强制把指定文件的内存缓存数据写入到磁盘中,实时使用fsync就能保证AOF日志不丢失。但是fsync涉及到磁盘写入,相较于内存操作会慢很多,如果每一个指令都fsync一次,Redis纯内存操作所带来的优势就不存在了。

因此目前主流的做法是Redis每隔1s执行一个fsync,1s是可配置的,可以根据需要配置。这样就在保持高效能的同时尽可能的减少日志丢失。Redis也提供了另外两种策略:一种是永不fsync,由操作系统决定什么时间将内存缓存同步到磁盘,这样无法掌控,很不安全。另一种是一次指令fsync一次,然后不会丢日志,单缺点上面也说过了,生产并不推荐。

Redis4.0新增了异步模型,可以打开fsync的异步处理开关,此时主线程不进行fsync,而是生成任务放到专门的fsync队列中去,由专门的fsync异步线程处理。

1.3 持久化选在从节点

无论是快照还是AOF,都比较消耗资源。快照需要遍历整个内存,大块磁盘读写加重系统负载。AOF的fsync是一个耗时的IO操作,也会影响Redis性能,加重系统IO负担。因此Redis的持久化一般并不安排在主节点,而是在从节点进行,从节点没有客户端请求的压力,资源比较充足。但是如果出现网络分区,从节点连不上主节点,而主节点又宕机了,就会出现数据丢失产生数据一致性的问题。因此生产环境需要做好网络连通性检测,保证出现问题时能快速修复,除此之外可以再挂一个从节点,这样只要有一个从节点数据同步正常,数据就不会丢失。

1.4 Redis4.0的混合持久化

Redis重启时,很少使用RDB来恢复数据,因为会丢失最后一次快照之后的数据。但是使用AOF日志重放,效率上又会慢很多。因此Redis4.0提供了混合持久化的策略,就是RDB和AOF同时使用。RDB正常持久化,而AOF不在记录全量指令,而是记录每次RDB快照之后的增量AOF,这样Redis重启时就可以先加载RDB的内容,然后再重放AOF日志,效率大大提升。

1.5 为什么不直接使用Redis做数据库

  • 内存重启需要重放,这个需要时间,而数据库重启可以直接使用,但是这个算不上什么问题,毕竟有了混合持久化后重放效率提升很大,而且都重启了,也不差那一点时间。
  • 持久化一般在从节点,主从同步需要时间,所以还是会导致数据丢失,虽然很少。而即使是主节点做持久化,数据持久化使用AOF模式,也要fsync always才能实现实时持久,但是这会使得redis纯内存操作的优势荡然无存,而不使用fsync always则必然要承担无法实时持久化带来的数据丢失可能性。
  • Redis的数据结构比较简单,无法存储复杂的关系结构(比如外键),或者实现起来较为麻烦。
  • 查询机制较为简单,无法数据库那样完善的查询引擎,不能高效的完成复杂查询。
  • 事务上没有数据库强大。虽然有WATCH,但是也仅仅算是一个锁定作用。MULTI/EXEC也并不是严格意义上的事务,因为其没有原子性,且并没有数据库那样多样的隔离性,关于事务下面会有一章单独写。
  • 内存容量有限,存储的数据也有限。而且内存贵!1G的内存和1G的磁盘价格根本没有可比性。冷数据放在内存里可太浪费了。

2 主从同步

主从同步能够在master挂掉之后,让从节点接管,从而快速恢复。避免因重启所需要的时间较长而影响业务。主从同步是Redis分布式的基石,Redis失去主从复制,高可用也就无法进行了,Redis的集群模式也都依赖于主从复制。如果涉及到Redis的持久化,那主从复制是必不可少的,需要认真对待。但是如果仅仅仅仅是用来做缓存,那主从复制就不是必要的了,挂了就重新加载一遍就行了。

2.1 网络分区与CAP原则

CAP原则是现代分布式系统的理论基石。

分布式系统的节点往往都是分布在不同机器上的,这意味着必然会有网络断开的风险,这个网络断开的场景就是网络分区。

CAP原则:CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)最多能实现2个,不能3者兼顾。

  • 一致性(Consistency):所有数据节点在任意时刻数据都是一致的。
  • 可用性(Availability):在任何时间都处于可用状态,总是能对请求进行响应,而不阻塞或报错。
  • 分区容忍性(Partition tolerance):系统中分区间通信出现问题时或部分分区宕机时,活动的分区依然能够独立提供服务。

这三要素最多只能实现两点,不可能三者兼顾。但是对于分布式系统来说P是必须的,因为如果要保证AC,那么只能单机运行,就不属于分布式系统了。 因此分布式系统也就分为了AP和CP系统。

AP系统放弃了一致性,保证可用性,即网络分区发生后即使各个分区数据已经不一致依然提供服务。

CP系统放弃了可用性,保证了一致性,即网络分区发生后停止服务,以此来保证数据的一致性。

提高分区容忍性的方法就是复制更多的节点,但是节点越多一致性就越难保证。

2.2 最终一致

Redis的主从同步是异步的,所以分布式的Redis系统并不满足一致性要求。客户端在Redis主节点修改完成后可以立即获得结果,即使主从网络已经断开,所以Redis是满足可用性的。

Redis会保证最终一致性,也就是说从节点会努力追赶主节点,力图与其保持一致,当网络分区出现并结束后,Redis会采用多种策略追赶主节点,继续努力保持与主节点一致。

Redis即支持主从同步,也支持从从同步。

2.2.1 增量同步

主节点将修改性指令记录在本地的buffer中,然后异步同步给从节点,从节点读取buffer中的指令进行同步,并向主节点反馈同步到的位置(偏移量)。

Redis的同步buffer是一个定长的环形数组,大小是有限的,如果数组满了就会覆盖前面的内容从头开始。如果出现网络分区,那么恢复后的buffer可能已经有指令被覆盖掉了,丛节点无法获得这些指令就会出现数据差异,此需要更复杂的同步机制:快照同步

2.2.2 快照同步

主节点进行一次bgsave(就是做一次RDB),然后将RDB文件传输给从节点,从节点清空内存,按照主节点传过来的RDB文件完成一次全量加载。此过程中,增量同步继续进行,从节点按照RDB重加载之后,就走增量同步路子进行追赶。但是如果快照同步时间过长或者buffer过小,就会导致从节点加载完RDB后发现又有覆盖情况了,继续执行快照同步,成了死循环。 因此需要合理设置buffer的大小避免这种情况发生。

当一个新节点加入集群之后,必须先完成一次快照同步,完成之后才能继续走增量同步。

2.2.3 无盘复制

无盘复制是Redis 2.8.18开始支持的。主要原因是快照同步需要将RDB文件写入磁盘,这是一个很重的IO操作,对系统负载的影响比较大,对主节点的服务效率产生影响。无盘复制就是不再生成RDB文件,而是直接将快照内容发送给从节点,也就是主节点一边遍历内容,一边将序列化的内容发送给从节点,从节点将接收到的内容写到磁盘上,接收完毕后再一次性加载。 总的来说就是将磁盘写入的压力由主节点转移到了从节点上面。

8.2.4 wait指令

Redis是异步同步的,一致性也是保持的最终一致性。wait指令则是尝试完成一次强一致同步。有两个参数,第一个参数是从节点数量,第二个几点是最大等待时间,单位是ms:

wait nodeNum waitTime: 就是完成nodeNum个从节点的强一致同步,完成同步则结束阻塞,否则阻塞到最大等待时间waitTime,如果超过waitTime还没完成就不管了。如果waitTime设置为0,则表示无限等待,此时如果出现网络分区导致无法同步,主节点的就会一直阻塞,失去可用性。


开发成长之旅 [持续更新中...]
欢迎关注…

参考资料:
《Redis深度历险》