RDB (redis database snapshot redis数据库快照)
RDB持久化是把当前进程数据生成快照保存到硬盘的过程, 触发RDB持久化过程分为手动触发
和自动触发
。
save 命令:阻塞当前redis服务器,所以线上环境不建议使用
19317:M 22 Nov 16:24:30.418 * DB saved on disk
bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。 阻塞只发生在fork阶段, 一般时间很短
3970:C 22 Nov 16:37:48.704 * RDB: 6 MB of memory used by copy-on-write
19317:M 22 Nov 16:37:48.715 * Background saving terminated with success
两个命令的不同之处,save用redis主进程进行同步转储,而bgsave则由子进程在后台进行转储
bgsave是主流的触发RDB持久化方式, 下图了解它的运作流程
1) 执行bgsave命令, Redis父进程判断当前是否存在正在执行的子进程, 如RDB/AOF子进程, 如果存在bgsave命令直接返回。
2) 父进程执行fork操作创建子进程, fork操作过程中父进程会阻塞, 通过info stats命令查看latest_fork_usec
选项, 可以获取最近一个fork操作的耗时, 单位为微秒
。
3) 父进程fork完成后, bgsave命令返回“Background saving started”信息并不再阻塞父进程, 可以继续响应其他命令。
ps命令会看到redis-rdb-bgsave子进程
4) 子进程创建RDB文件, 根据父进程内存生成临时快照文件, 完成后对原有文件进行原子替换。 执行lastsave命令可以获取最后一次生成RDB的时间, 对应info统计的rdb_last_save_time选项。
使用写时复制(copy-on-write)机制来实现快照持久化
5) 进程发送信号给父进程表示完成, 父进程更新统计信息, 具体见info Persistence下的rdb_*相关选项。
19317:M 22 Nov 16:37:48.669 * Background saving started by pid 3970
3970:C 22 Nov 16:37:48.703 * DB saved on disk
3970:C 22 Nov 16:37:48.704 * RDB: 6 MB of memory used by copy-on-write
19317:M 22 Nov 16:37:48.715 * Background saving terminated with success
192.168.49.171:6392> bgsave
Background saving started --父进程fork完成
192.168.49.171:6392> info persistence
# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1574411868 --执行lastsave命令可以获取最后一次生成RDB的时间,转成日期为(2019-11-22 16:37:48)
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
aof_enabled:1
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:0
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_current_size:17661740
aof_base_size:3046839
aof_pending_rewrite:0
aof_buffer_length:0
aof_rewrite_buffer_length:0
aof_pending_bio_fsync:0
aof_delayed_fsync:0
192.168.49.171:6392> info stats
# Stats
total_connections_received:37505
total_commands_processed:4151046
instantaneous_ops_per_sec:1
total_net_input_bytes:187212173
total_net_output_bytes:2059852448
instantaneous_input_kbps:0.07
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:2
sync_partial_ok:0
sync_partial_err:0
expired_keys:1
evicted_keys:0
keyspace_hits:597427
keyspace_misses:2815
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:5267 --最近一个fork操作的耗时5267毫秒
migrate_cached_sockets:0
192.168.49.171:6392>
RDB文件保存在dir配置指定的目录下, 文件名通过
dbfilename
配置指定。
可以通过执行config set dir{newDir}
和config setdbfilename{newFileName}
运行期动态执行, 当下次运行时RDB文件会保存到新目录
当遇到坏盘或磁盘写满等情况时, 可以通过config set dir{newDir}在线修改文件路径到可用的磁盘路径, 之后执行bgsave进行磁盘切换, 同样适用于AOF持久化文件。
如果Redis加载损坏的RDB文件时拒绝启动, 并打印如下日志:# Short read or OOM loading DB. Unrecoverable error, aborting now.这时可以使用Redis提供的redis-check-dump工具检测RDB文件并获取对应的错误报告.
①、save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave(这个命令下面会介绍,手动触发RDB持久化的命令)
默认如下配置:
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存
当然如果你只是用Redis的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。可以直接一个空字符串来实现停用:save ""
②、stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了
③、rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
虽然压缩RDB会消耗CPU, 但可大幅降低文件的体积, 方便保存到硬盘或通过网络发送给从节点, `因此线上建议开启`。
④、rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
⑤、dbfilename :设置快照的文件名,默认是 dump.rdb
⑥、dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。默认是和当前配置文件保存在同一目录。
也就是说通过在配置文件中配置的 save 方式,当实际操作满足该配置形式时就会进行 RDB 持久化,将当前的内存快照保存在 dir 配置的目录中,文件名由配置的 dbfilename 决定。
192.168.49.171:6392> config get save
1) "save"
2) ""
192.168.49.171:6392> flushall
OK
192.168.49.171:6392> keys *
(empty list or set)
192.168.49.171:6392> config get save
1) "save"
2) ""
192.168.49.171:6392> config set save "900 1 300 10 60 100000"
OK
192.168.49.171:6392> config get save
1) "save"
2) "900 1 300 10 60 100000"
shell >cat redis_import_key_10w.py
# coding: utf-8
import redis
client = redis.StrictRedis(host='192.168.49.171',port=6392)
for i in range(11000): --导入11000个key
client.pfadd("ids","user%d" % i)
total = client.pfcount("ids")
# if total != i 1:
# print total,i 1
# break
print 11000,client.pfcount("ids")
您在 /var/spool/mail/root 中有邮件
[root@yace-slave1 python]#
19317:M 22 Nov 18:04:33.058 * 10 changes in 300 seconds. Saving... --触发了300 秒内如果至少有 10 个 key 的值变化则保存这一条
19317:M 22 Nov 18:04:33.059 * Background saving started by pid 12362
12362:C 22 Nov 18:04:33.082 * DB saved on disk
12362:C 22 Nov 18:04:33.083 * RDB: 8 MB of memory used by copy-on-write
19317:M 22 Nov 18:04:33.159 * Background saving terminated with success
git clone https://github.com/sripathikrishnan/redis-rdb-tools
cd redis-rdb-tools
sudo python setup.py install
[root@node01 redis-rdb-tools]# rdb --command json /opt/cachecloud/data/dump-6392.rdb
[{
"a":"1",
"b":"2",
"c":"3"}]
[root@node01 redis-rdb-tools]#
AOF持久化: 以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令
达到恢复数据的目的 , AOF的主要作用是解决了数据持久化的实时性, 目前已经是Redis持久化的主流方式 。
开启AOF功能需要设置配置: appendonly yes, 默认不开启。 AOF文件名通过appendfilename配置设置, 默认文件名是appendonly.aof。 保存路径同RDB持久化方式一致, 通过dir配置指定。
配置参数
appendonly no
appendfilename "appendonly.aof"
AOF的工作流程操作: 命令写入(append) 、 文件同步(sync) 、 文件重写(rewrite) 、 重启加载(load)
AOF命令写入内容是采用文本协议格式。
3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
采用理由:
AOF命令追加到aof_buf中?
Redis提供了多种AOF缓冲区同步文件策略, 由参数appendfsync控制,不同值的含义。
系统调用write和fsync说明:
write操作会触发延迟写(delayed write) 机制。
Linux在内核提供页缓冲区用来提高硬盘IO性能。 write操作在写入系统缓冲区后直接返回。
同步硬盘操作依赖于系统调度机制, 例如: 缓冲区页空间写满或达到特定时间周期。 同步文件之前, 如果此时系统故障宕机, 缓冲区内数据将丢失。
fsync针对单个文件操作(比如AOF文件),做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回, 保证了数据持久化。
配置为always时, 每次写入都要同步AOF文件, 在一般的SATA硬盘上, Redis只能支持大约几百TPS写入, 显然跟Redis高性能特性背道而驰,不建议配置。
配置参数
#appendfsync always
appendfsync everysec
# appendfsync no
随着命令不断写入AOF, 文件会越来越大, 为了解决这个问题, Redis引入AOF重写机制压缩文件体积。 AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
AOF重写数据压缩的原理思想:
redis中有某些键超时或者已过期,键值多次更新保留新值,多条写命令和并成一条(lpush list a,lpush list b);好处数据文件变小,重启加快别redis重载。
AOF重写过程可以手动触发和自动触发:
配置参数
auto-aof-rewrite-min-size 64MB --AOF文件重写最小的文件大小
auto-aof-rewrite-percentage 100 --当前AOF文件比上次重写的增长比例大小
no-appendfsync-on-rewrite no --在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间,
no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。
设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。`Linux的默认fsync策略是30秒`。`可能丢失30秒数据`。默认值为no。
自动触发时机=aof_current_size>auto-aof-rewrite-minsize&&( aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewritepercentage
其中aof_current_size
和aof_base_size
可以在info Persistence
统计信息中查看。
192.168.49.171:6392> info persistence
# Persistence
aof_current_size:18234821
aof_base_size:3046839
流程说明:
ERR Background append only file rewriting already in progress
如果当前进程正在执行bgsave操作, 重写命令延迟到bgsave完成之后再执行, 返回如下响应:
Background append only file rewriting scheduled
redis 3.2版本
AOF和RDB文件都可以用于服务器重启时的数据恢复
流程说明:
- DB loaded from append only file: 5.841 seconds
- DB loaded from disk: 5.586 seconds
加载损坏的AOF文件时会拒绝启动, 并打印如下日志:
Bad file format reading the append only file: make a backup of your AOF file,then use
edis-check-aof --fix
对于错误格式的AOF文件, 先进行备份, 然后采用redis-check-aof–fix命令进行修复, 修复后使用diff-u对比数据的差异, 找出丢失的数据, 有些可以人工修改补全。
AOF文件可能存在结尾不完整的情况, 比如机器突然掉电导致AOF尾部文件命令写入不全。
Redis为我们提供了aof-load-truncated配置来兼容这种情况, 默认开启。 加载AOF时, 当遇到此问题时会忽略并继续启动, 同时打印
如下警告日志:
# !!! Warning: short read while loading the AOF file !!!
# !!! Truncating the AOF at offset 397856725 !!!
# AOF loaded anyway because aof-load-truncated is enabled
aof-load-truncated yes
redis4.0x之后混合持久化
开启了混合持久化,aof在重写时,不再是单纯将内存数据转换为RESP命令写入aof文件,而是将重写这一刻之前的内存做rdb快照处理,
并且将rdb快照内容和增量的aof修改内存数据的命令存在一起,都写入新的aof文件,新的aof文件一开始不叫appendonly.aof,
等到重写完成后,新的aof文件才会进行改名,原子的覆盖原有的aof文件,完成新旧两个aof文件的替换。
于是在redis重启的时候,可以先加载rdb文件,然后再重放增量的aof日志就可以完全替代之前的aof全量文件重放,因此重启效率大幅得到提高。
[原理](https://yq.aliyun.com/articles/193034)
aof-use-rdb-preamble yes --4.0版本的混合持久化默认关闭的,通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,默认是禁用的,可通过config set修改。
混合持久化过程
127.0.0.1:6379> info server
# Server
redis_version:5.0.6
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:24cefa6406f92a1f
redis_mode:standalone
os:Linux 3.10.0-862.el7.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:8.3.0
process_id:1
run_id:d9c2b6c262f5dec88de6a869aed1c481111ba582
tcp_port:6379
uptime_in_seconds:264540
uptime_in_days:3
hz:10
configured_hz:10
lru_clock:14384904
executable:/data/redis-server
config_file:
127.0.0.1:6379> config get aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "yes"
127.0.0.1:6379> debug populate 10000
OK
127.0.0.1:6379> save
OK
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379>
# head -10 appendonly.aof
REDIS0009dis-ver5.0.6edis-bitsctime]ed-mem -- 此时的aof文件已经和只开启AOF持久化文件不一样了,上半部分是RDB持久化的数据,下半部分是AOF格式数据。
value:4158key:949 value:94key:9963
value:9963key:585 value:585key:244 value:24key:1707
value:170key:6249
value:624key:8052
value:805key:1440
value:144key:4474
value:4474key:941 value:94key:6500
RDB
AOF
混合持久化
Redis持久化功能一直是影响Redis性能的高发地, 本节我们结合常见的持久化问题进行分析定位和优化。
当Redis做RDB或AOF重写时, 一个必不可少的操作就是执行fork操作创建子进程, 对于大多数操作系统来说fork是个重量级错误。 虽然fork创建的
子进程不需要拷贝父进程的物理内存空间, 但是会复制父进程的空间内存页表
。 例如对于10GB的Redis进程, 需要复制大约20MB的内存页表, 因此fork
操作耗时跟进程总内存量息息相关, 如果使用虚拟化技术, 特别是Xen虚拟机, fork操作会更耗时。
对于高流量的Redis实例OPS可达5万以上, 如果fork操作耗时在秒级别将拖慢Redis几万条命令执行, 对线上应用
延迟影响非常明显
。
正常情况下fork耗时应该是每GB消耗20毫秒左右
。 可以在info stats统计中查latest_fork_usec指标获取最近一次fork操作耗时,单位微秒
。
物理机或者高效支持fork操作的虚拟化技术
, 避免使用Xen。每个Redis实例内存控制在10GB以内
。适度放宽AOF自动触发时机
, 避免不必要的全量复制。子进程负责AOF或者RDB文件的重写,它的运行过程主要涉及CPU、内存、硬盘(IO)三部分的消耗。
子进程负责把进程内的数据分批写入文件,这个过程属于CPU密集操作, 通常子进程对
单核CPU利用率接近90%
.
CPU消耗优化。
Redis是CPU密集型服务,
不要做绑定单核CPU操作
。由于子进程非常消耗CPU,会和父进程产生单核资源竞争。
不要和其他CPU密集型服务部署在一起, 造成CPU过度竞争。如果部署多个Redis实例,尽量保证同一时刻只有一个子进程执行重写工作。
子进程通过fork操作产生,占用内存大小等同于父进程,理论上需要两倍的内存来完成持久化操作,但Linux有写时复制机制(copy-on-write)。
父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的页创建副本,而子进程在fork操作过程中共享整个父进程内存快照。
* Background saving started by pid 7692
* DB saved on disk
* RDB: 5 MB of memory used by copy-on-write
* Background saving terminated with success
如果重写过程中存在内存修改操作, 父进程
负责创建所修改内存页的副本
, 从日志中可以看出这部分内存消耗了5MB, 可以等价认为RDB重写消耗了5MB的内存
。
AOF重写时, Redis日志输出容如下:
* Background append only file rewriting started by pid 8937
* AOF rewrite child asks to stop sending diffs.
* Parent agreed to stop sending diffs. Finalizing AOF...
* Concatenating 0.00 MB of AOF diff received from parent.
* SYNC append only file rewrite performed
* AOF rewrite: 53 MB of memory used by copy-on-write
* Background AOF rewrite terminated with success
* Residual parent diff successfully flushed to the rewritten AOF (1.49 MB)
* Background AOF rewrite finished successfully
父进程维护页副本消耗同RDB重写过程类似, 不同之处在于AOF重写需要AOF重写缓冲区
, 因此根据以上日志可以预估内存消耗为:
53MB 1.49MB, 也就是AOF重写时子进程消耗的内存量
。
编写shell脚本根据Redis日志可快速定位子进程重写期间内存过度消耗情况。
内存消耗优化
Linux kernel在2.6.38内核增加了Transparent Huge Pages( THP),支持huge page( 2MB) 的页分配, 默认开启。
当开启时可以降低fork创建子进程的速度, 但执行fork之后, 如果开启THP, 复制页单位从原来4KB变为2MB, 会大幅增加重写期间父进程内存消耗。
建议设置“sudo echo never>/sys/kernel/mm/transparent_hugepage/enabled”关闭THP。
子进程主要职责是把AOF或者RDB文件写入硬盘持久化。 势必造成硬盘写入压力。
根据Redis重写AOF/RDB的数据量, 结合系统工具如sar、 iostat、 iotop等, 可分析出重写期间硬盘负载情况。
AOF重写时会消耗大量硬盘IO
,可以开启配置no-appendfsync-onrewrite,默认关闭。 表示在AOF重写期间不做fsync操作。注意:
配置no-appendfsync-on-rewrite=yes
时, 在极端情况下可能丢失整个AOF重写期间的数据, 需要根据数据安全性决定是否配置。
当开启AOF持久化时, 常用的同步硬盘的策略是everysec, 用于平衡性能和数据安全性。
对于这种方式, Redis使用另一条线程每秒执行fsync同步硬盘。 当系统硬盘资源繁忙时, 会造成Redis主线程阻塞。
阻塞流程分析:
如果距上次同步成功时间在2秒内, 主线程直接返回。
如果距上次同步成功时间超过2秒, 主线程将会阻塞, 直到同步操作完成。
通过对AOF阻塞流程可以发现两个问题:
AOF阻塞问题定位:
Asynchronous AOF fsync is taking too long (disk is busy). Writing the AOF bufferwithout waiting for fsync to complete, this may slow down Redis
Redis单线程架构导致无法充分利用CPU多核特性, 通常的做法是在一台机器上部署多个Redis实例。 当多个实例开启AOF重写后, 彼此之间会产生对CPU和IO的竞争。
对于单机多Redis部署, 如果同一时刻运行多个子进程, 对当前系统影响将非常明显, 因此需要采用一种措施, 把子进程工作进行隔离。
Redis在info Persistence中为我们提供了监控子进程运行状况的度量指标
可以通过外部程序轮询控制AOF重写操作的执行,整个过程: