大家好,我是小林。上周有读者被问到:Redis的大Key对持久化有什么影响?Redis的持久化方式有两种:AOF日志和RDB快照。那么接下来,对这两种持久化方式进行详细分析分析。大键对AOF日志的影响先说一下AOF日志回写磁盘的三种策略。Redis提供了三种将AOF日志写回硬盘的策略。它们是:Always,这个词的意思是“总是”,所以它的意思是每次执行完写操作命令后,都会将AOF日志数据同步回写到硬盘;everysec,这个词的意思是“每一秒”,所以它的意思是每次写操作命令执行完后,该命令会先被写入。进入AOF文件的内核缓冲区,然后每秒将缓冲区的内容写回硬盘;不是,意思是回写硬盘的时机不受Redis控制,回写的时机是由操作系统控制的,即每次执行写操作命令后,先将命令写到AOF文件的内核缓冲区,然后由操作系统决定何时将缓冲区内容写回硬盘。这三种策略只是控制何时调用fsync()函数。当应用程序向文件写入数据时,内核通常首先将数据复制到内核缓冲区中,然后将其入队,然后由内核决定何时写入硬盘。如果希望应用程序在向文件写入数据后立即将数据同步到硬盘,可以调用fsync()函数,让内核直接将内核缓冲区中的数据写入硬盘,并等待硬盘写操作完成,函数返回。Always策略是每次AOF文件数据写入时执行fsync()函数;Everysec策略将创建一个异步任务来执行fsync()函数;No策略是永远不执行fsync()函数;说说这三个在持久化大Key的时候这个策略会做什么?当使用Always策略时,主线程执行命令后会将数据写入AOF日志文件,然后调用fsync()函数直接将内核缓冲区中的数据写入硬盘,等到硬盘磁盘写操作完成,函数返回。使用Always策略时,如果写入的是大Key,主线程在执行fsync()函数时会阻塞很长时间,因为当写入的数据量很大时,数据会同步到硬盘。这个过程很耗时。使用Everysec策略时,由于fsync()函数是异步执行的,所以持久化大key(数据同步到磁盘)的过程不会影响主线程。当使用No策略时,由于fsync()函数从不执行,所以持久化大Keys的过程不会影响主线程。大键对AOF重写和RDB的影响当AOF日志中写入很多大键时,AOF日志文件的大小会很大,很快就会触发AOF重写机制。AOF重写机制和RDB快照(bgsave命令)的进程会分别通过fork()函数创建一个子进程来处理任务。在创建子进程的过程中,操作系统会将父进程的“页表”复制给子进程。这个页表记录的是虚拟地址和物理地址的映射关系,不需要复制物理内存。也就是说,两者的虚拟空间不同,但对应的物理空间是相同的。这样子进程共享父进程的物理内存数据,可以节省物理内存资源,并且页表对应的页表项的属性会将物理内存的权限标记为只读。随着Redis的大key越来越多,Redis会占用大量的内存,对应的页表也会越来越大。通过fork()函数创建子进程时,虽然不会复制父进程的物理内存,但内核会将父进程的页表复制给子进程。如果页表很大,复制过程会非常耗时,然后执行fork函数时就会发生阻塞。而且,fork函数是由Redis主线程调用的。如果fork函数被阻塞,则意味着Redis主线程将被阻塞。由于Redis执行命令是在主线程中处理的,当Redis主线程阻塞时,无法处理客户端发送的后续命令。我们可以执行info命令获取latest_fork_usec指标,表示Redis的最新fork操作需要时间。#最新的fork操作耗时latest_fork_usec:315如果fork耗时较长,比如超过1秒,需要做优化调整:单实例内存占用控制在10GB以下,这样fork函数可以快速返回。如果Redis只是作为纯缓存使用,并不关心Redis的数据安全,可以考虑关闭AOF和AOF重写,这样fork函数就不会被调用。在master-slave架构中,应该适当增加repl-backlog-size,避免master节点因为repl_backlog_buffer不够大而频繁使用全量同步方式。全量同步时,会创建RDB文件,即会调用fork函数。什么时候会发生物理内存的复制?当父进程或子进程向共享内存发起写操作时,CPU会触发缺页中断。这个缺页中断是由于权限被侵犯引起的,然后操作系统会在“缺页异常处理函数”中进行物理内存处理。复制,并重新设置其内存映射关系,设置父子进程的内存读写权限为可读可写,最后写入内存。这个过程叫做“**写时复制(CopyOnWrite)**”。Copy-on-write,顾名思义,只有当写操作发生时,操作系统才会复制物理内存。这是为了防止fork创建子进程时,由于物理内存数据拷贝时间长,导致父进程长时间阻塞。如果子进程创建后父进程修改了共享内存中的大Key,那么内核会copy-on-write,复制物理内存。由于大Key占用的物理内存比较大,那么复制物理内存的过程也比较耗时,所以父进程(主线程)会被阻塞。因此,有两个阶段会导致父进程阻塞:在创建子进程的途中,由于需要复制父进程的页表等数据结构,阻塞时间与大小有关页表,页表越大,阻塞时间越长;子进程创建后,如果子进程或父进程修改了共享数据,就会发生copy-on-write,这期间会复制物理内存。内存越大,自然阻塞的时间就会越长;这里额外提一下,如果Linux启用大内存页,会影响Redis的性能。Linux内核从2.6.38开始支持内存大页机制,支持2MB大小的内存页分配,而常规内存页分配以4KB的粒度进行。如果使用大内存页,即使客户端请求只修改100B的数据,Redis发生copy-on-write后也需要复制2MB的大页。反之,如果是常规的内存页机制,只会复制4KB。对比两者可以看出,每次写命令造成的复制内存页单元放大了512倍,这会拖慢写操作的执行时间,最终导致Redis性能变慢。那么该怎么办?很简单,关闭内存大页面(默认是关闭的)。禁用方法如下:echonever>/sys/kernel/mm/transparent_hugepage/enabled总结当AOF回写策略配置Always策略时,如果写入的是大Key,执行时主线程阻塞fsync()函数会耗费很长时间,因为当写入的数据量很大时,同步数据到硬盘的过程是非常耗时的。AOF重写机制和RDB快照(bgsave命令)的进程会分别通过fork()函数创建一个子进程来处理任务。会有两个阶段会导致父进程(主线程)阻塞:在创建子进程的途中,由于复制父进程的页表等数据结构,阻塞时间与大小有关页表的。页表越大,阻塞的时间也越长;子进程创建后,如果父进程修改了共享数据中的大Key,就会发生copy-on-write,这期间会复制物理内存。由于大Key占用的物理内存会很大,那么在复制物理内存的过程中会很耗时,所以父进程可能会被阻塞。大键除了影响持久化外,还有以下作用。客户端超时并阻塞。由于Redis在单线程进程中执行命令,操作大key需要时间,Redis会被阻塞。从客户的角度来看,长时间没有任何反应。造成网络拥塞。每次获取大密钥,网络流量都比较大。如果一个key的大小是1MB,每秒的访问次数是1000,那么每秒就会产生1000MB的流量,对于普通千兆网卡的服务器来说是灾难性的。的。阻止工作线程。如果用del删除一个大的key,工作线程就会被阻塞,以至于后面的命令没法处理。内存分布不均。当集群模型存在均匀槽分片时,会出现数据和查询倾斜。一些key大的Redis节点占用内存大,QPS会比较大。如何避免大Key?最好在设计阶段将大键拆分成小键。或者,定期检查Redis中是否有大键。如果大key可以删除,就不要用DEL命令删除,因为这个命令的删除过程会阻塞主线程。而是使用unlink命令(Redis4.0+)来删除bigkey,因为这个命令的删除过程是异步的,不会阻塞主线程。
