Redis分布式解决方案

Published on 2017 - 06 - 03

复制

对于有扩展平台以适应更高负载经验的工程师和管理员来说,复制(replication)是不可或缺的。复制可以让其他服务器拥有一个不断地更新的数据副本,从而使得拥有数据副本的服务器可以用于处理客户端发送的读请求。关系数据库通常会使用一个主服务器(master)向多个从服务器(slave)发送更新,并使用从服务器来处理所有读请求。Redis也采用了同样的方法来实现自己的复制特性,并将其用作扩展性能的一种手段。本节将对Redis的复制配置选项进行讨论,并说明Redis在进行复制时的各个步骤。

尽管Redis的性能非常优秀,但它也会遇上没办法快速地处理请求的情况,特别是在对集合和有序集合进行操作的时候,涉及的元素可能会有上万个甚至上百万个,在这种情况下,执行操作所花费的时间可能需要以秒来进行计算,而不是毫秒或者微秒。但即使一个命令只需要花费10毫秒就能完成,单个Redis实例(instance)1秒也只能处理100个命令。

SUNIONSTORE命令的性能作为对Redis性能的一个参考,在主频为2.4 GHz的英特尔酷睿2处理器上,对两个分别包含10 000个元素的集合执行SUNIONSTORE命令并产生一个包含20 000个元素的结果集合,需要花费Redis七八毫秒的时间。

在需要扩展读请求的时候,或者在需要写入临时数据的时候,用户可以通过设置额外的Redis从服务器来保存数据集的副本。在接收到主服务器发送的数据初始副本(initial copy of the data)之后,客户端每次向主服务器进行写入时,从服务器都会实时地得到更新。在部署好主从服务器之后,客户端就可以向任意一个从服务器发送读请求了,而不必再像之前一样,总是把每个读请求都发送给主服务器(客户端通常会随机地选择使用哪个从服务器,从而将负载平均分配到各个从服务器上)。

接下来的一节将介绍配置Redis主从服务器的方法,并说明Redis在整个复制过程中所做的各项操作。

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

当从服务器连接主服务器的时候,主服务器会执行BGSAVE操作。因此为了正确地使用复制特性,用户需要保证主服务器已经正确地设置了dir选项和dbfilename选项,并且这两个选项所指示的路径和文件对于Redis进程来说都是可写的(writable)。

尽管有多个不同的选项可以控制从服务器自身的行为,但开启从服务器所必须的选项只有slaveof一个。如果用户在启动Redis服务器的时候,指定了一个包含slaveof host port选项的配置文件,那么Redis服务器将根据该选项给定的IP地址和端口号来连接主服务器。对于一个正在运行的Redis服务器,用户可以通过发送SLAVEOF no one命令来让服务器终止复制操作,不再接受主服务器的数据更新;也可以通过发送SLAVEOF host port命令来让服务器开始复制一个新的主服务器。

开启Redis的主从复制特性并不需要进行太多的配置,但了解Redis服务器是如何变成主服务器或者从服务器的,对于我们来说将是非常有用的和有趣的过程。

Redis复制的启动过程

本文前面曾经说过,从服务器在连接一个主服务器的时候,主服务器会创建一个快照文件并将其发送至从服务器,但这只是主从复制执行过程的其中一步。表2完整地列出了当从服务器连接主服务器时,主从服务器执行的所有操作。

步  骤 主服务器操作 从服务器操作
1 (等待命令进入) 连接(或者重连接)主服务器,发送SYNC命令
2 开始执行BGSAVE,并使用缓冲区记录BGSAVE之后执行的所有写命令 根据配置选项来决定是继续使用现有的数据(如果有的话)来处理客户端的命令请求,还是向发送请求的客户端返回错误
3 BGSAVE执行完毕,向从服务器发送快照文件,并在发送期间继续使用缓冲区记录被执行的写命令 丢弃所有旧数据(如果有的话),开始载入主服务器发来的快照文件
4 快照文件发送完毕,开始向从服务器发送存储在缓冲区里面的写命令 完成对快照文件的解释操作,像往常一样开始接受命令请求
5 缓冲区存储的写命令发送完毕;从现在开始,每执行一个写命令,就向从服务器发送相同的写命令 执行主服务器发来的所有存储在缓冲区里面的写命令;并从现在开始,接收并执行主服务器传来的每个写命令

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

设置从服务器的步骤非常简单,用户既可以通过配置选项SLAVEOF host port来将一个Redis服务器设置为从服务器,又可以通过向运行中的Redis服务器发送SLAVEOF命令来将其设置为从服务器。如果用户使用的是SLAVEOF配置选项,那么Redis在启动时首先会载入当前可用的任何快照文件或者AOF文件,然后连接主服务器并执行表2所示的复制过程。如果用户使用的是SLAVEOF命令,那么Redis会立即尝试连接主服务器,并在连接成功之后,开始表2所示的复制过程。

从服务器在进行同步时,会清空自己的所有数据因为有些用户在第一次使用从服务器时会忘记这件事,所以这里要特别提醒一下:从服务器在与主服务器进行初始连接时,数据库中原有的所有数据都将丢失,并被替换成主服务器发来的数据。

警告:Redis不支持主主复制(master-master replication)
因为Redis允许用户在服务器启动之后使用SLAVEOF命令来设置从服务器选项(slaving options),所以可能会有读者误以为可以通过将两个Redis实例互相设置为对方的主服务器来实现多主复制(multi-master replication)(甚至可能会在一个循环里面将多个实例互相设置为主服务器)。遗憾的是,这种做法是行不通的:被互相设置为主服务器的两个Redis实例只会持续地占用大量处理器资源并且连续不断地尝试与对方进行通信,根据客户端连接的服务器的不同,客户端的请求可能会得到不一致的数据,或者完全得不到数据。

当多个从服务器尝试连接同一个主服务器的时候,就会出现表3所示的两种情况中的其中一种。

当有新的从服务器连接主服务器时 主服务器的操作
表2的步骤3尚未执行 所有从服务器都会接收到相同的快照文件和相同的缓冲区写命令
表2的步骤3正在执行或者已经执行完毕 当主服务器与较早进行连接的从服务器执行完复制所需的5个步骤之后,主服务器会与新连接的从服务器执行一次新的步骤1至步骤5

在大部分情况下,Redis都会尽可能地减少复制所需的工作,然而,如果从服务器连接主服务器的时间并不凑巧,那么主服务器就需要多做一些额外的工作。另一方面,当多个从服务器同时连接主服务器的时候,同步多个从服务器所占用的带宽可能会使得其他命令请求难以传递给主服务器,与主服务器位于同一网络中的其他硬件的网速可能也会因此而降低。

主从链

有些用户发现,创建多个从服务器可能会造成网络不可用——当复制需要通过互联网进行或者需要在不同数据中心之间进行时,尤为如此。因为Redis的主服务器和从服务器并没有特别不同的地方,所以从服务器也可以拥有自己的从服务器,并由此形成主从链(master/slave chaining)。

从服务器对从服务器进行复制在操作上和从服务器对主服务器进行复制的唯一区别在于,如果从服务器X拥有从服务器Y,那么当从服务器X在执行表2中的步骤4时,它将断开与从服务器Y的连接,导致从服务器Y需要重新连接并重新同步(resync)。

当读请求的重要性明显高于写请求的重要性,并且读请求的数量远远超出一台Redis服务器可以处理的范围时,用户就需要添加新的从服务器来处理读请求。随着负载不断上升,主服务器可能会无法快速地更新所有从服务器,或者因为重新连接和重新同步从服务器而导致系统超载。为了缓解这个问题,用户可以创建一个由Redis主从节点(master/slave node)组成的中间层来分担主服务器的复制工作,如图1所示。

[图1 一个Redis主从复制树(master/slave replica tree)示例,树的中层有3个帮助开展复制工作的服务器,底层有9个从服务器]

尽管主从服务器之间并不一定要像图1那样组成一个树状结构,但记住并理解这种树状结构对于Redis复制来说是可行的(possible)并且是合理的(reasonable)将有助于读者理解之后的内容。AOF持久化的同步选项可以控制数据丢失的时间长度:通过将每个写命令同步到硬盘里面,用户几乎可以不损失任何数据(除非系统崩溃或者硬盘驱动器损坏),但这种做法会对服务器的性能造成影响;另一方面,如果用户将同步的频率设置为每秒一次,那么服务器的性能将回到正常水平,但故障可能会造成1秒的数据丢失。通过同时使用复制和AOF持久化,我们可以将数据持久化到多台机器上面。

为了将数据保存到多台机器上面,用户首先需要为主服务器设置多个从服务器,然后对每个从服务器设置appendonly yes选项和appendfsync everysec选项(如果有需要的话,也可以对主服务器进行相同的设置),这样的话,用户就可以让多台服务器以每秒一次的频率将数据同步到硬盘上了。但这还只是第一步:因为用户还必须等待主服务器发送的写命令到达从服务器,并且在执行后续操作之前,检查数据是否已经被同步到了硬盘里面。

检验硬盘写入

为了验证主服务器是否已经将写数据发送至从服务器,用户需要在向主服务器写入真正的数据之后,再向主服务器写入一个唯一的虚构值(unique dummy value),然后通过检查虚构值是否存在于从服务器来判断写数据是否已经到达从服务器,这个操作很容易就可以实现。另一方面,判断数据是否已经被保存到硬盘里面则要困难得多。对于每秒同步一次AOF文件的Redis服务器来说,用户总是可以通过等待1秒来确保数据已经被保存到硬盘里面;但更节约时间的做法是,检查INFO命令的输出结果中aof_pending_bio_fsync属性的值是否为0,如果是的话,那么就表示服务器已经将已知的所有数据都保存到硬盘里面了。在向主服务器写入数据之后,用户可以将主服务器和从服务器的连接作为参数,调用代码清单3所示的函数来自动进行上述的检查操作。

def wait_for_sync(mconn, sconn):
    identifier = str(uuid.uuid4())
    mconn.zadd('sync:wait', identifier, time.time())        #A

    while not sconn.info()['master_link_status'] != 'up':   #B
        time.sleep(.001)

    while not sconn.zscore('sync:wait', identifier):        #C
        time.sleep(.001)

    deadline = time.time() + 1.01                           #D
    while time.time() < deadline:                           #D
        if sconn.info()['aof_pending_bio_fsync'] == 0:      #E
            break                                           #E
        time.sleep(.001)

    mconn.zrem('sync:wait', identifier)                     #F
    mconn.zremrangebyscore('sync:wait', 0, time.time()-900) #F
    #A Add the token to the master
#B Wait for the slave to sync (if necessary)
#C Wait for the slave to receive the data change
#D Wait up to 1 second
#E Check to see if the data is known to be on disk
#F Clean up our status and clean out older entries that may have been left there

INFO命令中的其他信息INFO命令提供了大量的与Redis服务器当前状态有关的信息,比如内存占用量、客户端连接数、每个数据库包含的键的数量、上一次创建快照文件之后执行的命令数量,等等。总的来说,INFO命令对于了解Redis服务器的综合状态非常有帮助,网上有很多资源都对INFO命令进行了详细的介绍。

为了确保操作可以正确执行,wait_for_sync()函数会首先确认从服务器已经连接上主服务器,然后检查自己添加到等待同步有序集合(sync wait ZSET)里面的值是否已经存在于从服务器。在发现值已经存在于从服务器之后,函数会检查从服务器写入缓冲区的状态,并在1秒之内,等待从服务器将缓冲区中的所有数据写入硬盘里面。虽然函数最多会花费1秒来等待同步完成,但实际上大部分同步都会在很短的时间完成。最后,在确认数据已经被保存到硬盘之后,函数会执行一些清理操作。

通过同时使用复制和AOF持久化,用户可以增强Redis对于系统崩溃的抵抗能力。

处理系统故障

用户必须做好相应的准备来应对Redis的系统故障。本文在系统故障这个主题上花费了大量的篇幅,这是因为如果我们决定要将Redis用作应用程序唯一的数据存储手段,那么就必须确保Redis不会丢失任何数据。跟提供了ACID1保证的传统关系数据库不同,在使用Redis为后端构建应用程序的时候,用户需要多做一些工作才能保证数据的一致性。Redis是一个软件,它运行在硬件之上,即使软件和硬件都设计得完美无瑕,也有可能会出现停电、发电机因为燃料耗尽而无法发电或者备用电池电量消尽等情况。这一节接下来将对Redis提供的一些工具进行介绍,说明如何使用这些工具来应对潜在的系统故障。下面先来看看在出现系统故障时,用户应该采取什么措施。

验证快照文件和AOF文件

无论是快照持久化还是AOF持久化,都提供了在遇到系统故障时进行数据恢复的工具。Redis提供了两个命令行程序redis-check-aof和redis-check-dump,它们可以在系统故障发生之后,检查AOF文件和快照文件的状态,并在有需要的情况下对文件进行修复。在不给定任何参数的情况下运行这两个程序,就可以看见它们的基本使用方法:

如果用户在运行redis-check-aof程序时给定了--fix参数,那么程序将对AOF文件进行修复。程序修复AOF文件的方法非常简单:它会扫描给定的AOF文件,寻找不正确或者不完整的命令,当发现第一个出错命令的时候,程序会删除出错的命令以及位于出错命令之后的所有命令,只保留那些位于出错命令之前的正确命令。在大多数情况下,被删除的都是AOF文件末尾的不完整的写命令。

遗憾的是,目前并没有办法可以修复出错的快照文件。尽管发现快照文件首个出现错误的地方是有可能的,但因为快照文件本身经过了压缩,而出现在快照文件中间的错误有可能会导致快照文件的剩余部分无法被读取。因此,用户最好为重要的快照文件保留多个备份,并在进行数据恢复时,通过计算快照文件的SHA1散列值和SHA256散列值来对内容进行验证。(当今的Linux平台和Unix平台都包含类似sha1sum和sha256sum这样的用于生成和验证散列值的命令行程序。)

校验和(checksum)与散列值(hash) 从2.6版本开始,Redis会在快照文件中包含快照文件自身的CRC64校验和。CRC校验和对于发现典型的网络传输错误和硬盘损坏非常有帮助,而SHA加密散列值则更擅长于发现文件中的任意错误(arbitrary error)。简单来说,用户可以翻转文件中任意数量的二进制位,然后通过翻转文件最后64个二进制位的一个子集(subset)来产生与原文件相同的CRC64校验和。而对于SHA1和SHA256,目前还没有任何已知的方法可以做到这一点。

在了解了如何验证持久化文件是否完好无损,并且在有需要时对其进行修复之后,我们接下来要考虑的就是如何更换出现故障的Redis服务器。

更换故障主服务器

在运行一组同时使用复制和持久化的Redis服务器时,用户迟早都会遇上某个或某些Redis服务器停止运行的情况。造成故障的原因可能是硬盘驱动器出错、内存出错或者电量耗尽,但无论服务器因为何种原因出现故障,用户最终都要对发生故障的服务器进行更换。现在让我们来看看,在拥有一个主服务器和一个从服务器的情况下,更换主服务器的具体步骤。

假设A、B两台机器都运行着Redis,其中机器A的Redis为主服务器,而机器B的Redis为从服务器。不巧的是,机器A刚刚因为某个暂时无法修复的故障而断开了网络连接,因此用户决定将同样安装了Redis的机器C用作新的主服务器。

更换服务器的计划非常简单:首先向机器B发送一个SAVE命令,让它创建一个新的快照文件,接着将这个快照文件发送给机器C,并在机器C上面启动Redis。最后,让机器B成为机器C的从服务器2。代码清单4展示了更换服务器时用到的各个命令。

user@vpn-master ~:$ ssh root@machine-b.vpn                          #A
Last login: Wed Mar 28 15:21:06 2012 from ...                       #A
root@machine-b ~:$ redis-cli                                        #B
redis 127.0.0.1:6379> SAVE                                          #C
OK                                                                  #C
redis 127.0.0.1:6379> QUIT                                          #C
root@machine-b ~:$ scp \\                                           #D
> /var/local/redis/dump.rdb machine-c.vpn:/var/local/redis/         #D
dump.rdb                      100%   525MB  8.1MB/s   01:05         #D
root@machine-b ~:$ ssh machine-c.vpn                                #E
Last login: Tue Mar 27 12:42:31 2012 from ...                       #E
root@machine-c ~:$ sudo /etc/init.d/redis-server start              #E
Starting Redis server...                                            #E
root@machine-c ~:$ exit
root@machine-b ~:$ redis-cli                                        #F
redis 127.0.0.1:6379> SLAVEOF machine-c.vpn 6379                    #F
OK                                                                  #F
#A Connect to machine B on our vpn network
#B Start up the command line redis client to do a few simple operations
#C Start a SAVE, and when it is done, QUIT so that we can continue
#D Copy the snapshot over to the new master, machine C
#E Connect to the new master and start Redis
#F Tell machine B's Redis that it should use C as the new master

代码清单4中列出的大部分命令,对于使用和维护Unix系统或者Linux系统的人来说应该都不会陌生。在这些命令当中,比较有趣的要数在机器B上运行的SAVE命令,以及将机器B设置为机器C的从服务器的SLAVEOF命令。

另一种创建新的主服务器的方法,就是将从服务器升级(turn)为主服务器,并为升级后的主服务器创建从服务器。以上列举的两种方法都可以让Redis回到之前的一个主服务器和一个从服务器的状态,而用户接下来要做的就是更新客户端的配置,让它们去读写正确的服务器。除此之外,如果用户需要重启Redis的话,那么可能还需要对服务器的持久化配置进行更新。

Redis SentinelRedis Sentinel可以监视指定的Redis主服务器及其属下的从服务器,并在主服务器下线时自动进行故障转移(failover)。

参考文档


  1. ACID是指原子性(atomicity)、一致性(consistency)、隔离性(isolation)和耐久性(durability),如果一个数据库想要实现可靠的数据事务,那么它就必须保证ACID性质。 

  2. 因为机器B原本就是一个从服务器,所以我们的客户端不能对它进行写入,并且在机器B执行快照操作之后,我们的客户端也不会与其他试图对机器B进行写入的客户端产生竞争条件。