• 【重学Reids 缓存】之Reids 缓存之RDB 持久化


    Reids 缓存之RDB 持久化


    一、Redis RDB是什么?

    快照,顾名思义可以理解为拍照一样,把整个内存数据映射到硬盘中,保存一份到硬盘,因此恢复数据起来比较快,把数据映射回去即可,不像AOF,一条条的执行操作命令。

    快照是默认的持久化方式。这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。可以通过配置设置自动做快照持久化的方式。

    产生快照的情况有以下几种:
    •执行bgsave命令(此时Redis会fork一个子进程,子进程负责生成硬盘文件,父进程负责继续接收命令)
    •或执行save命令(和bgsave命令不同,发送save命令后,系统创建快照完成之前系统不会再接收任何新的命令,换句话说save命令会阻塞后面的命令,而bgsave不会)
    •根据用户在配置文件中配置的快照触发时间执行
    •客户端发送shutdown,系统会先执行save命令阻塞客户端,然后关闭服务器
    •当有主从架构时,从服务器向主服务器发送sync命令来执行复制操作时,主服务器会执行bgsave操作

    二、初始化环境

    1.创建配置/数据/日志目录

    代码如下(示例):

    1.1 创建日志目录

    # 创建配置目录
    mkdir -p /usr/local/redis/conf
    # 创建数据目录
    mkdir -p /usr/local/redis/data
    # 创建日志目录
    mkdir -p /usr/local/redis/log
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.2 创建配置文件

    配置文件(示例):
    创建一份配置文件至 conf 目录。

    vim /usr/local/redis/conf/redis.conf
    
    • 1

    文件内容如下

    # 放行访问IP限制
    bind 0.0.0.0
    # 后台启动
    daemonize yes
    # 日志存储目录及日志文件名
    logfile "/usr/local/redis/log/redis.log"
    # rdb数据文件名
    dbfilename dump.rdb
    # rdb数据文件和aof数据文件的存储目录
    dir /usr/local/redis/data
    # 设置密码
    requirepass 123456
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    1.3 准备数据

    配置文件(示例):
    在 /usr/local/redis/bin 目录下创建 initdata.py,内容如下

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    
    class Token(object):
        def __init__(self, value):
            if isinstance(value, Token):
                value = value.value
            self.value = value
    
        def __repr__(self):
            return self.value
    
        def __str__(self):
            return self.value
    
    def b(x):
        return x
    
    
    SYM_STAR = b('*')
    SYM_DOLLAR = b('$')
    SYM_CRLF = b('\r\n')
    SYM_EMPTY = b('')
    
    
    class RedisProto(object):
        def __init__(self, encoding='utf-8', encoding_errors='strict'):
            self.encoding = encoding
            self.encoding_errors = encoding_errors
    
        def pack_command(self, *args):
            """将redis命令安装redis的协议编码,返回编码后的数组,如果命令很大,返回的是编码后chunk的数组"""
            output = []
            command = args[0]
            if ' ' in command:
                args = tuple([Token(s) for s in command.split(' ')]) + args[1:]
            else:
                args = (Token(command),) + args[1:]
    
            buff = SYM_EMPTY.join(
                (SYM_STAR, b(str(len(args))), SYM_CRLF))
    
            for arg in map(self.encode, args):
                """数据量特别大的时候,分成部分小的chunk"""
                if len(buff) > 6000 or len(arg) > 6000:
                    buff = SYM_EMPTY.join((buff, SYM_DOLLAR, b(str(len(arg))), SYM_CRLF))
                    output.append(buff)
                    output.append(arg)
                    buff = SYM_CRLF
                else:
                    buff = SYM_EMPTY.join((buff, SYM_DOLLAR, b(str(len(arg))), SYM_CRLF, arg, SYM_CRLF))
    
            output.append(buff)
            return output
    
        def encode(self, value):
            if isinstance(value, Token):
                return b(value.value)
            elif isinstance(value, bytes):
                return value
            elif isinstance(value, int):
                value = b(str(value))
            elif not isinstance(value, str):
                value = str(value)
            if isinstance(value, str):
                value = value.encode(self.encoding, self.encoding_errors)
            return value
    
    
    if __name__ == '__main__':
        for i in range(5000000):
            commands_args = [('SET', 'key_' + str(i), 'value_' + str(i))]
            commands = ''.join([RedisProto().pack_command(*args)[0] for args in commands_args])
            print commands
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    在 /usr/local/redis/bin 目录下执行以下命令加载数据(根据机器不同可能需要30s~60s):

    python initdata.py | ./redis-cli -a 123456 --pipe
    
    • 1

    1.4 开启RBD

    配置文件(示例):
    我们可以配置Redis在n秒内如果超过m个key被修改就自动做快照,下面是默认的快照保存配置(这3个选项都屏蔽,则RDB禁用):

    # 900秒内如果超过1个key改动,则发起快照保存
    save 900 1
    # 300秒内如果超过10个key改动,则发起快照保存
    save 300 10
    # 60秒内如果超过1W个key改动,则发起快照保存
    save 60 10000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    三.RDB工作原理

    Redis默认会将快照文件存储在Redis当前进程的工作目录中的dump.rdb文件中,可以通过配置dir和dbfilename两个参数分别指定快照文件的存储路径和文件名。
    •Redis使用fork函数复制一份当前进程(父进程)的副本(子进程);
    •父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件;
    •当子进程写入完所有数据后会用该临时文件替换旧的 RDB 文件,至此一次快照操作完成。

    在执行 fork 的时候操作系统(类 Unix 操作系统)会使用写时复制(copy-on-write)策略,即fork函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(如执行一个写命令),操作系统会将该片数据复制一份以保证子进程的数据不受影响,所以新的RDB文件存储的是执行fork一刻的内存数据。

    另外需要注意的是,当进行快照的过程中,如果写入操作较多,造成 fork 前后数据差异较大,是会使得内存使用量显著超过实际数据大小的,因为内存中不仅保存了当前的数据库数据,而且还保存着 fork 时刻的内存数据。进行内存用量估算时很容易忽略这一问题,造成内存用量超限。

    通过上述过程可以发现Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候 RDB 文件都是完整的。这使得我们可以通过定时备份 RDB 文件来实现 Redis 数据库备份。RDB 文件是经过压缩(可以配置rdbcompression 参数以禁用压缩节省CPU占用)的二进制格式,所以占用的空间会小于内存中的数据大小,更加利于传输。

    Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存。根据数据量大小与结构和服务器性能不同,这个时间也不同。通常将一个记录1000万个字符串类型键、大小为1 GB 的快照文件载入到内存中需要花费20~30秒。

    总结:

    a、内存资源风险:Redis fork子进程做RDB持久化,由于写的比例为80%,那么在持久化过程中,“写实复制”会重新分配整个实例80%的内存副本,大约需要重新分配1.6GB内存空间,这样整个系统的内存使用接近饱和,如果此时父进程又有大量新key写入,很快机器内存就会被吃光,如果机器开启了Swap机制,那么Redis会有一部分数据被换到磁盘上,当Redis访问这部分在磁盘上的数据时,性能会急剧下降,已经达不到高性能的标准(可以理解为武功被废)。如果机器没有开启Swap,会直接触发OOM,父子进程会面临被系统kill掉的风险。

    b、CPU资源风险:虽然子进程在做RDB持久化,但生成RDB快照过程会消耗大量的CPU资源,虽然Redis处理处理请求是单线程的,但Redis Server还有其他线程在后台工作,例如AOF每秒刷盘、异步关闭文件描述符这些操作。由于机器只有2核CPU,这也就意味着父进程占用了超过一半的CPU资源,此时子进程做RDB持久化,可能会产生CPU竞争,导致的结果就是父进程处理请求延迟增大,子进程生成RDB快照的时间也会变长,整个Redis Server性能下降。

    c、另外,可以再延伸一下,没有提到Redis进程是否绑定了CPU,如果绑定了CPU,那么子进程会继承父进程的CPU亲和性属性,子进程必然会与父进程争夺同一个CPU资源,整个Redis Server的性能必然会受到影响!所以如果Redis需要开启定时RDB和AOF重写,进程一定不要绑定CPU。

    四.RBD的优点

    •一旦采用RDB方式,那么你的整个Redis数据库将只包含一个紧凑压缩的二进制文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

    •对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

    •性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

    •相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

    五.RDB的缺点

    •如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。

    •由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

  • 相关阅读:
    Jmeter压力测试教程(上)
    Apache Tomcat 漏洞复现
    32FLASH闪存
    a16z公布AI产品流量排名,ChatGPT占据榜首
    安卓的ATV系统
    qml GroupBox用法介绍
    C/C++跨平台构建工具CMake入门
    都说计算机今年炸了,究竟炸到什么程度呢?
    基于leetcode的算法训练:Day4
    关于环保电缆,你了解多少
  • 原文地址:https://blog.csdn.net/qq_32048567/article/details/126671828