Redis数据安全与性能保障——redis读书笔记4

4. Redis数据安全与性能保障


4.1 Redis持久化数据

为了重用数据,或者防止系统故障而降数据备份到另外一个远程位置。有些数据是可能是经过长时间的计算,或者程序正在使用redis存储的数据进行计算,所以希望将这些数据存储起来,redis提供了两种不同的持久化方法将数据写入到硬盘里:

(1) 快照。将存在于某一时刻的所有数据都写入到硬盘里面。

相关的配置选项:

save 60 10000 #从最近一次创建快照开始算起,当60秒之内有10000次写入,就会自动触发BGSAVE命令,创建快照
stop-writes-on-bgsave-error no
rdbcompression yes
dbfilename dump.rdp # 快照被写入的指定文件

(2) 只追加文件。在执行写命令时,将被执行的写命令复制到硬盘里。

相关的配置选项:

appendonly no # 是否使用AOF持久化
appendfsync everysec # AOF文件同步频率,有几个选项
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

dir ./ # 共享选项,这个选项决定了快照文件和AOF文件的保存位置

这两种方法也可以同时使用。

4.1.1 快照持久化

用户可以将快照复制到其他服务器创建相同数据的服务器副本,也可以将其留在原地以便重启服务器使用。

在新的快照文件创建完毕之前,如果系统,redis或者硬件中任何一个崩溃,将丢失最近一次快照写入的所有数据。

创建快照的方法有以下几种:

(1)向Redis发送一个BGSAVE的命令来创建一个快照。

Redis将fork一个子进程,用来处理将快照写入到磁盘中,父进程则继续处理命令请求。

(2)向Redis发送一个SAVE命令来创建一个快照。在创建快照完毕之前不再响应其他命令。

SAVE命令并不常用,一般在没有足够的内存去执行BGSAVE命令,或者即使等待持久化操作执行完毕也无所谓的情况下,才会使用该命令。

(3)如果用户设置了save配置选项。
譬如 save 60 10000,即redis最近一次创建快照之后算起,当60秒之内有100000次写入这个条件被满足,redis就会自动触发BGSAVE命令.
如果用户设置了多个save配置选项,那么当任意一个save配置选项所设置的条件被满足时,redis就会触发一次BGSAVE命令。

(4)当Redis接收到SHUTDOW关闭服务器的请求时,或者接收到标准TERM信号时,会执行一个SAVE命令,阻塞所有客户端,不再执行客户端发送的任何命令,并在save执行完毕后关闭服务器。

(5)当一个redis服务器连接另一个redis服务器,并向对方发送SYNC命令来开始一次复制操作的时候,主服务器会先执行BGSAVE命令。

在新的快照文件创建完毕之前,如果系统,redis或者硬件中任何一个崩溃,将丢失最近一次生成快照之后更改的所有数据,因此,快照持久化只适用于即使丢失一部分数据也不会造成问题的应用程序。

根据几个使用快照持久化的场景,学习如何通过修改配置来获得自己想要的快照持久化:

  1. 个人开发。我们需要降低快照持久化带来的资源消耗,设置了save 900 1这一规则,距离上一次成功生成快照已经超过900秒,并且在此区间执行了至少一次写入操作,那么redis就会自动开启一次新的BGSAVE操作。
    如果需要在生产服务器中使用快照持久化并存储大量数据,需要把开发环境设置尽可能贴近生产环境,有助于判断快照是否生成得过于频繁(浪费资源)或者过于稀少(有可能丢失大量数据)。
  2. 对日志进行聚合计算。
    对日志文件进行聚合计算或者对页面浏览量进行分析时,我们要考虑,如果Redis因为崩溃而未能成功创建新的快照,我们能够承受丢失多久时间以内产生的新数据。如果丢失一个小时以内的是可以接受的,那么我们可以配置 save 3600 1。

接下来需要解决,如何恢复因为故障而被中断的日志处理操作。可以通过将日志处理进度记录到redis里面,程序在系统崩溃之后,根据进度记录继续执行之前未完成的处理工作。

  1. 大数据。
    如果Redis的内存占用量达到了数据十个GB,并且剩余的空闲内存并不多,或者redis运行在虚拟机上,那么执行BGSAVE可能会导致系统长时间的停顿,也可能引发系统大量得使用虚拟内存,从而使redis性能降低到无法使用的程度。

为了防止redis因为创建子进程而出现停顿,我们可以考虑关闭自动保存,转而通过手动发送BGSAVE或者save来进程持久化。
手动发送BGSAVE一样会引起停顿,唯一不同的是用户可以通过手动发送BGSAVE命令来控制手动发送BGSAVE来控制停顿时间。
另外,SAVE虽然会一直阻塞redis直至快照生成完毕,但因为它不需要创建子进程,不会出现像BGSAVE一样因为创建子进程而导致redis停顿,并因为没有子进程在争抢资源,创建快照的速度会比BGSAVE创建快照的速度来得更快。

对于大数据的redis持久化,如果程序只需要每天生成一次快照,可以写一个脚本,让她每天凌晨3点停止所有客户端对redis的访问,调动save命令并等待该命令执行完毕,之后备份刚刚生成的快照文件,并通知客户端继续执行操作。

4.1.2 AOF持久化

AOF持久化会将执行的写命令写入到AOF文件的末尾,以此来记录数据发生的变化。因为redis只要从头到尾重新执行一次AOF文件所包含的写命令,就可以恢复AOF文件所记录的数据集。

文件同步:在硬盘写入文件时,当调用file.write()方法对文件进行写入时,写入的内容首先会被存储到缓冲区,然后操作系统会在将来的某个时候将缓冲区存储的内容写入硬盘。
用户也可以调用file.flush()方法来请求操作系统尽快将缓冲区存储的数据写入硬盘里,但具体何时执行写入操作仍然由操作系统决定。
用户还可以命令操作系统将文件同步到硬盘,同步操作会一直阻塞直到指定的文件被写入到硬盘为止。当同步操作执行完毕后,即使系统出现故障也不会对被同步的文件造成任何影响。

AOF同步频率的几个选项:
(1)always
每个redis写命令都要同步写入硬盘,可以将发生系统崩溃时出现数据丢失减到最小,但需要对硬盘进行大量写入,redis处理命令的速度会受到硬盘性能的限制。

(2)everysec 每秒执行一次同步,显示地将多个写命令同步到硬盘。不会过多影响性能,也能保证即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。

(3) no
让操作系统来决定应该何时进行同步,几乎不会对redis性能带来影响,但系统崩溃将导致使用这种选项的redis服务器丢失不定数量的数据。另外如果用户硬盘写入速度不够快,那么当缓冲区被等待写入硬盘的数据填满时,redis写入操作将被阻塞,并导致redis处理命令请求的速度变慢。因为这个原因,一般来说并不推荐使用no选项。

4.1.3 重写压缩AOF文件

AOF持久化既然可以将丢失数据的时间窗口降低至1秒,又可以在极短时间内完成定期的持久化操作,我们有什么理由不使用AOF持久化?

(1)redis是不断将执行的写命令记录到AOF文件,随着他的不断运行,AOF的体积就会不断增长,极端情况下,AOF文件可能会用完硬盘的所有可用空间。

(2)redis重启需要将AOF文件记录的所有写命令来还原数据集,所以如果AOF文件体积非常大,那么还原操作执行的时间可能会非常长。

为了解决AOF文件体积不断增大的问题,用户可以向redis发送BGREWRITEAOF命令,通过移除AOF文件中的冗余命令来重写AOF文件,使AOF文件的体积变得尽可能地小。

BGRREWRITEAOF和BGSAVE创建快照的工作原理非常相似:redis会创建一个子进程,然后由子进程负责对AOF文件进行重写。所以在快照持久化因为创建子进程而导致性能问题和内存占用问题,在AOF持久化也同样存在。更糟糕的是,如果不加以控制,AOF文件的体积可能会比快照文件的体积大上好几倍,在进行AOF文件重写并删除旧AOF文件的时候,删除一个体积达到数十GB大的旧AOF文件可能会导致操作系统挂起数秒。

AOF可以通过设置auto-aof-rewrite-percentage选项和auto-aof-rewrite-min-size选项来自动执行BGREWRITEAOF。

例如:我们设置的配置选项如下:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
并且启动了AOF持久化,那么当AOF文件体积大于64mb,并且AOF文件的体积比上一次重写之后的体积大了至少一倍(100%)的时候,redis将执行BGREWRITEAOF命令。
如果AOF重写过于频繁,用户可以将auto-aof-rewrite-percentage设置为100以上,让redis在AOF文件的体积变得更大之后才执行重写操作,不过会让redis重启在还原数据集所需的时间变得更长。

除了对数据进行持久化,用户还必须对持久化文件进行备份,最好是备份到不同的地方或者不同的服务器上,尽量避免数据丢失事故的发生。

4.2 复制

复制可以让其他服务器拥有一个不断更新的数据副本,从而使得拥有数据副本的服务器可以用户客户端发送的读请求。

redis使用一个主服务器(master)向多个从服务器(slave)发送更新,并使用从服务器来处理所有读请求这种方式来实现复制特性,并将其用作扩展性能的一种手段。

4.2.1 对redis的复制相关选项进行配置

为了正确地使用复制的特性,用户需要保证主服务器已经正确地设置了dir选项和dbfilename选项,并且这两个选项所指示的路径和文件对于redis进程来说都是可写的。

尽管有多个不同的选项可以控制服务器自身的行为,但开启从服务器所必须的选项只有 slaveof 一个。
用户在启动Redis服务器的时候,指定一个包含slaveof host port选项的配置文件,那么redis将根据该配置给定的IP地址和端口号来连接主服务器。

对于一个正在运行的redis服务器,用户可以通过发送slaveof no one命令来让服务器终止复制操作,不再接受主服务器的数据更新;也可以通过发送slaveof host port命令来让服务器开始复制一个新的主服务器。

4.2.2 redis复制的启动过程

从服务器连接主服务器时的步骤:

(1)主服务器操作:等待命令进入
从服务器操作:连接主服务器,发送SYNC命令

(2)主服务器操作:开始执行BGSAVE,并使用缓存区记录BGSAVE之后执行的所有写命令。
从服务器操作:根据配置选项来决定是使用现有的数据来处理客户端的命令请求,还是向发送请求的客户端返回错误。

(3)主服务器操作:BGSAVE执行完毕,向从服务器发送快照文件,并在发送期间继续使用缓冲区记录被执行的写命令。
从服务器操作:丢弃旧数据,开始载入主服务器发来的快照文件。

(4)主服务器操作:快照文件发送完毕,开始向主服务器发送存储在缓冲区里面的写命令。
从服务器操作:完成快照文件的解释操作,像往常一样开始接受命令请求。

(5)主服务器操作:缓冲区存储的写命令发送完毕,从现在开始,每执行一个写命令,就向从服务器发送相同的写命令。
从服务器操作:执行主服务器发来的所有存储在缓冲区的写命令,并从现在开始,接收并执行主服务器传来的每个写命令。

Redis在复制进行期间会尽可能处理接收到的命令请求,但是如果主从服务器之间的网络带宽不足,或者主服务器没有足够的内存来创建子进程和创建写命令的缓冲区,那么redis处理命令请求的效率就会受到影响。因此,在实际中我们最好让主服务器只使用50% ~ 60%的内存,留下30% ~ 45%的内存用于执行BGSAVE命令和创建记录写命令的缓冲区。

设置从服务器的方法:

(1)可以通过配置slaveof host port来将一个redis服务器设置为从服务器。用这种方法首先redis在启动时先载入当前可用的任何快照文件或者AOF文件,然后连接主服务器并执行复制过程。

(2)通过向运行中的redis服务器发送slaveof命令来将其设置为从服务器。这时redis会立即尝试连接主服务器,并在连接成功之后,开始复制过程。

注意:从服务器在进行同步时,会清空自己的所有数据,从服务器在与主服务器进行初始连接时,数据库中原有的所有数据将丢失,并被替换成主服务器发来的数据。

redis不支持主主复制。redis是不允许通过两个redis实例互相设置为对方的主服务器来实现多主复制,这种做法会使redis实例持续占用大量处理器资源并且连续不断地尝试与对方进行通信,根据客户端连接的服务器不同,客户端的请求可能会得到不一致的数据,或者完全得不到数据。

4.2.3 主从链

当多个从服务器同时连接主服务器时,同步多个从服务器所占用的带宽可能会使其他命令请求难以传递给主服务器。与主服务器位于同一网络中的其他硬件的网速可能也会因此而降低。

从服务器也可以拥有自己的从服务器,从而形成主从链。

从服务器对从服务器进行复制在操作上与从服务器对主服务器进行操作的区别在于:如果从服务器X拥有从服务器Y,那么当从服务器X在执行复制步骤4,会断开与从服务器Y的连接,导致从服务器Y需要重新连接并重新同步。

4.2.4 检查硬盘写入

为了验证主服务器是否已经将写数据发送至从服务器,用户需要在写入数据之后,再向主服务器写入一个唯一的虚构值,然后通过检查虚构值是否存在于从服务器来判断写数据是否已经到达从服务器。
另外,我们还需检查数据是否已经保存到硬盘中。我们可以检查INFO命令的输出结果中aof-pending-bio-fsync属性的值是否为0,如果是的话,那么表示服务器已经将已知的所有数据保存到硬盘中。

INFO命令提供了大量的redis服务器当前状态有关的信息,比如内存占用量,客户端连接数,每个数据库包含的键的数量,上一次创建快照文件之后执行的命令数量等等。

4.3 处理系统故障

4.3.1 验证快照文件和AOF文件

redis提供了两个命令行程序redis-check-aof和redis-check-dump,他们可以在系统故障发生之后,检查AOF文件和快照文件的状态,并在有需要的情况下对文件进行修复。

$ redis-check-aof [--fix] <file.aof>
该命令会扫描给定的AOF文件,寻找不正确或者不完整的命令,当发现第一个出错命令的时候,程序会删除出错的命令以及位于出错命令之后的所有命令。
在大多数情况下,被删除的都是AOF文件末尾的不正确的写命令。

$ redis-check-dump <dump.rdb>
检查快照文件。但目前并没有办法可以修复出错的快照文件。尽管发现快照文件首个出错的地方是有可能,但因为快照文件本身经过压缩,而出现在快照文件中间的错误有可能会导致快照文件的剩余部分无法被读取。因此,用户最好为重要的快照文件保留多个备份。

4.3.2 更换故障主服务器

假设A、B两台机器都运行着Redis,其他机器A的redis为主服务器,而机器B的redis为从服务器,机器A由于某个暂时无法修复的故障而断开网络连接,用户决定将机器C用作新的主服务器,具体步骤如下:
向机器B发送一个SAVE命令,让它创建一个新的快照文件,接着将这个快照文件发送给机器C,并在机器C上启动redis,最后让机器B成为机器C的从服务器。

用到的代码示例如下:

# 连接机器B
$ ssh root@machine-b.vpn

# 启动命令行redis客户端来执行简单的操作
$ redis-cli
redis 127.0.0.1:6379 > SAVE
# 执行完,退出客户端
redis 127.0.0.1:6379 > QUIT

# 将快照文件发送到新的主服务器
$ scp /var/local/redis/dump.rdb machine-c.vpn:/var/local/redis/dump.rdb

# 连接机器C,并开启redis
$ ssh root@machine-c.vpn
$ sudo /etc/init.d/redis-server start
$ exit

# 连接机器b,告知机器b的redis,让它将机器c作为新的主服务器
$ ssh root@machine-b.vpn
$ redis-cli
$ SLAVEOF machine-c.vpn 6379
$ QUIT

4.4 Redis事务

Redis的事务以特殊命令MULTI为开始,之后跟着用户传入多个命令,最后以EXEC结束。但是由于这种简单的事务在EXEC命令被调用之前不会执行任何实际操作,所以用户将没办法根据读取到的数据决定。

除了要使用MULIT命令和EXEC命令外,还需要配合使用WATCH命令,有时候甚至还会用到UNWATCH和DISCARD命令。在用户使用WATCH命令对键进行监视之后,直到用户执行EXEC命令的这段时间,如果有其他客户端抢先对任何被监视的键进行替换、更新或删除等操作,那么当用户尝试执行EXEC命令的时候,事务将失败并返回一个错误,之后用户可以选择重试事务或者放弃事务。

通过以上的这些命令,程序可以再执行某些重要操作时,通过确保自己正在使用的数据并没有发生变化来避免数据出错。

UNWATCH命令可以在WATCH命令执行之后,MULTI命令执行之前对连接进行重置。

DISCARD命令也可以在MULTI命令执行之后,EXEC命令执行之前对连接进行重置。也就是说,用户在使用WATCH监视一个或多个键,接着使用MULTI开始一个新事务并将多个命令入队到事务队列之后,仍然可以通过发送DISCARD命令来取消WATCH命令并清空所有已入队命令。

Redis没有实现典型的加锁功能,在访问以写入为目的数据的时候(SQL中的select for update),关系数据库会对被访问的数据进行加锁,直到事务被提交或者被回滚为止,如果有其他客户端试图对被加锁的数据进行写入,那么该客户端将被阻塞,直到第一个事务执行完毕为止。这种加锁方式被称为悲观锁。

因为加锁有可能造成长时间的等待,所以redis为了尽可能减少客户端的等待时间,并不会在执行WATCH命令时对数据进行加锁。只会在数据已经被其他客户端抢先修改的情况下,通知执行了WATCH命令的客户端,这种做法称为乐观锁。这样客户端就不必去等待第一个取得锁的客户端,它们只需要在自己的事务执行失败时进行重试就可以了。

4.5 非事务型流水线

在需要执行大量命令的情况下,即使命令实际上不需要放在事务里面执行,但为了通过一次发送所有命令减少通信并降低延迟值,用户也可以将命令包裹在
MULTI和EXEC里面执行。只是MULTI和EXEC也会消耗资源,并且可能导致其他重要的命令被延迟执行。

实际上,我们可以在不使用MULTI和EXEC的情况下,获得流水线带来的所有好处。

譬如说,使用python执行MULTI和EXEC命令:

pipe = conn.pipeline()

如果用户在执行pipeline时传入True作为参数,或者不传入任何参数,那么客户端将使用MULTI和EXEC包裹起用户要执行的所有命令。
如果用户传入False为参数,那么客户端会像执行事务那样收集起用户要执行的所有命令,只是不再使用MULTI和EXEC包裹这些命令。所以如果用户需要向
Redis发送多条命令,并且对于这些命令来说,一个命令的执行结果并不会影响另一个命令的输入,并且这些命令也不需要以事务的方式来执行,我们可以通过这种方式来进一步提升Redis的整体性能。

4.6 关于性能方面的注意事项

要对redis的性能进行优化,用户首先要弄清各种类型的Redis命令到底能跑多快,而这一点可以通过调用redis附带的性能测试程序redis-benchmark来得知。

# 给定‘-q’选项让程序简化输出结果,给定‘-c 1’选项让程序只使用一个客户端来进行测试,如果不给定任何参数,将使用50个客户端来进行性能测试。
$ redis-benchmark -c 1 -q

PING_INLINE: 35460.99 requests per second
PING_BULK: 37453.18 requests per second
SET: 33222.59 requests per second
GET: 34843.21 requests per second
INCR: 35335.69 requests per second
LPUSH: 33333.33 requests per second
LPOP: 33333.33 requests per second
SADD: 32154.34 requests per second
SPOP: 35335.69 requests per second
LPUSH (needed to benchmark LRANGE): 29673.59 requests per second
LRANGE_100 (first 100 elements): 21786.49 requests per second
LRANGE_300 (first 300 elements): 10593.22 requests per second
LRANGE_500 (first 450 elements): 7880.22 requests per second
LRANGE_600 (first 600 elements): 6191.95 requests per second
MSET (10 keys): 29069.77 requests per second

下面列举一些可能引起性能问题的原因:

(1)性能或者错误:单个客户端的性能达到redis-benchmark的50% ~ 60%
可能的原因:只是不使用流水线时的预期性能
解决方法:无

(2)性能或者错误:单个客户端的性能达到redis-benchmark的25% ~ 30%
可能的原因:对于每个命令或者每组命令都创建了新的连接
解决方法:重用已有的redis连接

(3)性能或者错误:客户端返回错误:“Cannot assign requested address”(无法分配指定的地址)
可能的原因:对于每个命令或者每组命令都创建了新的连接
解决方法:重用已有的redis连接

大部分Redis的客户端都提供了某种级别的内置连接池。以python的redis客户端为例,对于每个redis服务器,用户只需要创建一个redis.Redis()对象,该对象就会按需创建连接,重用已有的连接并关闭超时的连接,并且python客户端的连接池还可以安全地应用于多线程环境和多进程环境。

相关推荐