2

缓存数据丢了,原来是Redis持久化没玩明白 - Redis - dbaplus社群:围绕Data、Blockch...

 11 months ago
source link: https://dbaplus.cn/news-158-5282-1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

缓存数据丢了,原来是Redis持久化没玩明白 - Redis - dbaplus社群:围绕Data、Blockchain、AiOps的企业级专业社群。技术大咖、原创干货,每天精品原创文章推送,每周线上技术分享,每月线下技术沙龙。 我们都知道Redis是微服务架构中重要的基础数据库中间件,通过Redis可以将数据库中的数据缓存到内存中,当服务端有数据查询请求的时候,可以直接从内存中获取数据。如此,一方面服务端可以获得比较快的数据请求响应,另一方面降低了后端关系数据库的业务请求压力。但是正所谓尺有所短,寸有所长,Redis最大的优势就是内存数据也是最大的劣势,因为一旦服务器宕机或者服务器重启,内存中缓存的数据也会丢失。针对这样的场景,Redis提供了三种数据持久化机制,分别是AOF、RDB以及混合持久化来应对这种异常情况。本文主要从Redis实现持久化遇到的问题出发,站在设计者的角度思考相关问题的解决思路。

图片

一、AOF持久化

AOF持久化方式,即Append Only File,Redis通过记录执行修改操作命令这种记小本本的方式进行内存数据持久化。当需要通过AOF日志进行恢复数据时,Redis服务端启动后可以从日志文件中回放执行命令来实现内存数据恢复。当然了,AOF日志中记录的都是修改的命令,查询命令不会修改数据所以不需要进行记录。

可能大家都比较熟悉WAL(Write Ahead Log),即日志预写机制,它是数据库非常常用的确保数据操作原子性以及持久性的技术手段。拿Mysql举栗子,Mysql的WAL体现在undo log以及redo log等这些日志文件中,数据库在执行修改操作的时候并不是立刻将数据更新到磁盘上,而是先记录在日志中,主要目的是如果出现异常,可以直接从redo log中进行数据恢复,也就是说让Mysql知道上次意外发生的时候操作到底有没有成功,另外还可以将Mysql的随机写转换为顺序写,提升IO性能。但是AOF却不同,它是在Redis将数据写入内存之后,再将相关的操作命令写入AOF文件中。

图片

那么问题来了,为什么Redis要采取这种独特的数据记录方式,而不是业界常用的WAL的方式呢?其实可以从以下两个层面思考原因。

(1)AOF文件中保存了执行缓存的命令,以便于保证在需要恢复数据的时候可以进行命令重放恢复数据,因此需要保证执行命令的合法性,而通过先缓存数据再进行命令追加日志的方式可以确保追加到AOF文件中的的命令都是合法有效的,redis在恢复数据的时候不需要再去检查命令是否有效,进一步提升内存数据恢复的效率。

(2)另外由于是在修改操作命令之后进行日志记录,日志记录的时候需要进行磁盘IO操作,因此不会阻塞当前的修改命令。

AOF文件内容是什么?

在搞清楚Redis为什么采用AOF文件记录修改命令之后,我们再来看看AOF文件中到底包含了哪些内容。

redis> SET mufeng handsomeOK

Redis客户端与服务端之间采用RESP协议进行通信,它是一种应用层协议,对于Redis这种以效率为追求目标的中间件,通信协议必定要简单高效。就上面一条缓存操作命令来说:set mufeng handsome 对应的RESP报文就是*3$3set$6mufeng$8handsome,为了方便查看进行了手动换行。

图片

我们来拆解下报文中各个属性的含义,“*3”代表本次操作命令将由三个分布组成,每一部分都是通过"$数字"的形式作为起始,后面为对应的命令、键或者值。如此处的"$6"就表示后面的命令是一个6个字节的键值。所以,appenonly.aof文件中实际保存的就是这种格式的内容。

图片

AOF有没有丢数据的风险?

上文说到Redis通过AOF文件实现内存数据持久化,那么是不是就代表缓存数据保存就万无一失了?这样的持久化方式还有没有数据丢失的风险呢?大家可以设想一下假设在操作完Redis之后,还没来得及将命令写入AOF文件就宕机了,那么这个操作命令就会丢失,对应的缓存数据最新值也会丢失。因为即便宕机异常恢复之后,也没办法从AOF文件中执行丢失的操作命令了。因此,写入AOF缓冲区的数据什么时候进行持久化落盘,直接决定着AOF持久化方式缓存数据丢失的风险大小。

三种AOF落盘策略:

针对AOF缓存中的数据在什么时机写入磁盘,Redis提供了三种AOF日志写入策略供用户进行选择,通过后台线程执行不同时机的AOF文件数据同步操作,在redis.conf配置文件中的配置项appendfsync可以进行配置。

  • 【appendfsync:no】

Redis不用管AOF缓冲区的数据什么时候写入磁盘,将AOF缓冲区同步数据的操作权交给操作系统,操作系统决定什么时候将缓冲区的数据写入磁盘中。

  • 【appendfsync:everysec】

当Redis将数据写入AOF缓冲区后,每隔1s将缓冲区的数据进行磁盘写入。

  • 【appendfsync:always】

每执行一个修改命令,都需要将修改的命令进行落盘操作。

虽然Redis提供了这三种AOF日志落盘策略供用户进行选择,但是这三种策略实际上各有优缺点。

【appendfsync:no】如果设置了由操作系统进行AOF缓冲区数据写入,那么就相当于写数据的时机完全交由操作系统来决定,此时redis对于缓冲区数据并不可以控制。

【appendfsync:everysec】如果设置成每隔一秒进行缓存数据写入,虽然不会像同步写入那样存在一定的性能消耗,但是由于存在一秒的时间间隔,如果在此期间出现服务器宕机,那么就会损失这一秒的缓存数据。

【appendfsync:always】虽然可以基本实现数据不丢失,但是由于每次进行内存数据修改都要进行落盘操作,因此在一定程度上会影响主线程性能。

具体采取怎样的配置策略还是要根据实际的业务场景来决定,一般推荐使用第二种配置策略【appendfsync:everysec】,在可靠性以及性能方面相对平衡一点。

AOF文件会越来越大吗?

在了解了AOF日志磁盘写入时机之后,我们继续来思考下一个问题。无论采取什么样的同步数据策略,最终都是要将修改命令写入AOF文件中,因此随着时间的推移,这个文件必定会越来越大。那么如果文件变得很大之后,无论是文件数据新写入还是Redis通过AOF文件进行数据恢复,大文件的操作都会造成IO性能损耗。假如你是Redis的设计者,如果遇到这种情况你会怎么进行设计优化呢?我想无非有两个优化思路,一个是化整为零,一个是想办法缩小大文件。

化整为零

当单个文件过大时,我们很容易想到的优化方法就是将这个大文件拆分为若干个小文件。这就好比系统中一旦出现过千万数据库表的时候,我们就要结合实际的业务场景考虑要不要进行分库分表了。所以如果单个AOF文件太大,那么是不是可以考虑将其按照固定大小进行拆分,这样可以避免单个AOF文件过大的问题。那么Redis小于7.0版本为什么没有采用这种方案呢?主要是这种方案并不符合Redis追求简单高效的设计思想。假设采用了这种数据分块的方式,那必定需要实现文件大小检测、文件创建、文件索引维护等等一系列技术细节问题,对于低版本的Redis来说这些都太繁琐了,还不如一个AOF文件来的爽快。

PS:在最新的Redis 7.0版本中,Redis已经支持多AOF文件分片机制,原始的单个AOF文件会被拆分为一个基础文件以及多个增量文件。新版本中之所以开始支持多文件存储,我想也是随着业务发展内存数据可能会很庞大,Redis设计者发现如果还是使用单文件存储,大AOF文件操作以及数据恢复都是一个挑战。

图片

AOF重写

既然进行文件切割太繁琐了,那么就单个AOF文件来说怎么才能减小文件大小呢?那就要从AOF文件的记录内容入手,通过上文我们了解到AOF文件中实际存储了修改内存数据的操作命令,因此我们在分析完这些操作命令之后发现,当多条命令操作同一个key的时候,实际我们需要的是最新的一条操作命令,除此之外的历史操作命令我们并不需要关心。比如【set mufeng handsome】、【set mufeng cool】,如果先后执行了这两个命令,那么在最终恢复数据的时候,只要恢复【set mufeng cool】即可。因此AOF重写的本质就是合并命令,也就是说将多条对同一key进行操作的命令进行合并,实际就是使用最新的key值操作命令来代替之前所有关于这个key值的命令。

Redis通过fork子进程来完成AOF文件重写,因此在讲AOF重写过程之前,我们需要先了解下什么是fork子进程的原理,这样更加有利于我们后面了解AOF文件重写的过程。

什么是fork?

fork函数是linux内核提供给用户创建进程的API,应用程序通过调用fork函数创建子进程,这个子进程可以和原来父进程干同样的事情,也可以和原来主进程干不同的事情,这主要取决于对应的参数。这个过程就好比孙悟空拔了一根自己的猴毛变出来一个和自己一模一样的孙悟空。

因此在fork子进程的过程之中,子进程复制了父进程的代码段、数据段、堆栈、页表等,同时子进程拥有独立的虚拟内存空间(当然是从父进程那里复制过来的)。如下所示,实际上fork()最终调用的是内核copy_process方法复制进程。

图片

父进程fork子进程的时候,子进程拥有独立的虚拟内存空间,那么对应的物理内存空间是不是也是独立的呢?我们都知道在计算机中,内存属于非常宝贵的系统资源,所以大佬们在设计的时候都尽可能的减少内存空间占用从而提高系统资源利用率。fork子进程过程中用到的Copy-On-Write就是典型的内存资源管理优化机制,如果子进程只是读取数据不进行任何的数据写入,那么就和父进程公用内存空间。当子进程需要进行数据写入的时候,发现没有内控空间可以写入,此时会触发一个系统中断来分配内存空间给子进程进行数据写入。

图片

什么时机触发AOF重写?

执行bgrewriteaof 命令

当我们在客户端手动执行bgrewriteaof 命令后,可以触发AOF文件进行重写,对应Redis源码中进行重写的bgrewriteaofCommand 函数会检测检测是否满足进行重写的条件,主要检测以下两个条件:

【Condition1】:检测当前是否存在已经在执行的AOF重写子进程,如果存在的话Redis将不再执行AOF文件重写。

【Condition2】:检测当前是否存在已经在创建RDB文件的子进程,如果存在的话Redis将AOF文件重写任务置为待调度状态,后续如果满足了重写条件,则继续执行AOF文件重写任务。

也就是说,Redis检测到当前既没有AOF重写子进程也没有RDB文件创建子进程,那么就可以进行AOF文件重写。对应源码如下:

//of_child_pid(aof rewrite进程pid)、rdb_child_pid(rdb dump进程pid)void bgrewriteaofCommand(redisClient *c) {    if (server.aof_child_pid != -1) {        //如果正在aof rewrite,返回错误信息        addReplyError(c,"Background append only file rewriting already in progress");    } else if (server.rdb_child_pid != -1) {        //如果正在rdb dump,为了避免磁盘压力,将aof重写计划状态置为1,后期再进行rewrite;        server.aof_rewrite_scheduled = 1;        addReplyStatus(c,"Background append only file rewriting scheduled");    }    //如果当前没有aof rewrite和rdb dump在进行,则调用rewriteAppendOnlyFileBackground开始aof rewrite。    else if (rewriteAppendOnlyFileBackground() == REDIS_OK) {        addReplyStatus(c,"Background append only file rewriting started");    } else {        //出现异常返回错误。        addReply(c,shared.err);    }}

超出配置阈值

如果Redis实例开启了AOF配置,同时配置了auto-aof-rewrite-percentage以及auto-aof-rewrite-min-size,如果超出了阈值会触发AOF重写。

  //没有rdb子进程、没有aof重写子进程、aof文件设定了阈值以及aof文件大小绝对值超过阈值  if (server.rdb_child_pid == -1 &&         server.aof_child_pid == -1 &&         server.aof_rewrite_perc &&         server.aof_current_size > server.aof_rewrite_min_size)     {        long long base = server.aof_rewrite_base_size ?                        server.aof_rewrite_base_size : 1;        long long growth = (server.aof_current_size*100/base) - 100;        //超过阈值则进行重写        if (growth >= server.aof_rewrite_perc) {            serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);            rewriteAppendOnlyFileBackground();        }     }

aof_rewrite_scheduled被设置为待调度状态

在bgrewriteaofCommand函数中,如果当前正在执行RDB dump操作,那么对应的aof待调度aof_rewrite_scheduled状态就会被置为1,当前RDB dump完成之后,会继续执行AOF重写操作。

AOF重写过程是怎样的?

通过上文的描述,我们知道了Redis触发AOF重写的时机,那么当触发重写之后的具体业务是怎样的呢?我们一起看下AOF重写的大致流程:

(1)Redis主进程首先检查是不是存在rdb dump进程或者aof重写进程正在运行,如果不存在Redis主进程fork子进程进行aof文件重写;

(2)fork出来的子进程和原来的Redis主进程拥有同样的内存数据,子进程遍历此时的内存数据同时将内存数据写入到临时的AOF文件中;

(3)主进程此时仍然可以接收客户端请求,同时将新的缓存操作写入aof_buf以及aof_rewrite_buf中,根据对应的同步策略,将buf中的数据分别写入旧AOF文件以及临时AOF文件中;

(4)重写完成之后,临时AOF文件将替换原有的老的AOF文件,从而完成整个AOF重写。

图片

AOF模式优点

  • AOF的持久化策略更加丰富些,可以根据实际业务需要进行配置,因此相对来说在数据可靠性方面要更加有优势一点。



  • AOF文件内容比较好理解,更加方便理解业务缓存数据。



AOF模式缺点

  • 通常情况下,同样的缓存数据,AOF文件比RDB文件大小要大一些。

  • 在文件恢复场景下,AOF要比DRB恢复数据慢一些。




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK