什么是高可用?
高可用的定义
维基百科中对于高可用的定义如下:
高可用性(英语:high availability,缩写为 HA),IT术语,指系统无中断地执行其功能的能力,代表系统的可用性程度。是进行系统设计时的准则之一。高可用性系统与构成该系统的各个组件相比可以更长时间运行。[1]
高可用性通常通过提高系统的容错能力来实现。
高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的。
高可用的度量
其度量方式,是根据系统损害、无法使用的时间,以及由无法运作恢复到可运作状况的时间,与系统总运作时间的比较。
业界常见的标准
可用性 | 年故障时间 |
99.9999% | 32秒 |
99.999% | 5分15秒 |
99.99% | 52分34秒 |
99.9% | 8小时46分 |
99% | 3天15小时36分 |
在线系统和执行关键任务的系统通常要求其可用性要达到5个9标准(99.999%)。百度搜索首页应该是在大家心中可用性最高的网站了,因为每次测试网络的时候大家都会通过ping www.baidu.com
命令来判断网络是否连通。
什么场景会导致系统不可用
导致系统不可用的原因很多,这里只列举几个常见的场景,并提供对应的解决方案。
代码质量问题
可能的原因
代码bug导致内存溢出,数据库连接被长期占用,多线程环境下锁的设计不合理等。
解决方案
可以通过提高代码质量,提高测试质量等手段解决
突发流量
可能的原因
例如活动促销、恶意网络攻击等
解决方案
漏桶算法、令牌桶算法、分流、降级等
几种实现高可用的方案(以redis为例)
即使解决了上述所有问题,也很难说自己的系统是高可用的,因为高可用不只关注服务的可用时间,还关注服务多久能够在出现故障之后恢复。理想状态下就是当某个服务发生故障后,通过某种机制能够自动发现故障并自动完成故障的转移,接下来将以redis为例,介绍一下redis在服务高可用方面所做的努力。
数据的持久化
redis的数据持久化策略从单机的角度来看就是高可用的一种体现,因为redis在运行时所有的数据都保存在内存中,如果实例突然宕机,那么内存中的数据就会丢失,redis解决这个问题的方案就是定期的将数据持久化到磁盘中,这样即使实例宕机,数据也不会丢失(至少不会全部丢失)。
redis的持久化策略有RDB和AOF两个策略,下面分别介绍这两种持久化策略。
AOF
redis可以通过将所有执行的增删改命令保存起来,当某个redis实例从故障中恢复过来后,redis会创建一个伪客户端,然后加载磁盘中的AOF文件,并逐一执行AOF文件中的命令即可恢复数据。
AOF的流程
当服务端执行一次写命令后,该命令会被保存在一个缓冲区中,然后redis会根据用户配置的写入策略来决定何时将命令持久化到硬盘中,其示意图如下:
AOF的持久化策略
策略 | 描述 |
always | 表示服务端每执行一个命令都会将其持久化到磁盘中 |
everysec | 表示每秒执行一次将缓冲区中的命令持久化到磁盘中的操作 |
no | 不会主动将缓冲区中的命令持久化到磁盘中,持久化到磁盘的时机由操作系统决定 |
如果业务对数据安全性要求高,那么建议采用 always 策略。
如果业务对数据安全性要求相对而言不是很高,那么建议采用 everysec 策略,即使出现宕机,也最多只会丢失1s内的数据。
RDB
redis还可以通过对当前数据库拍快照的方式将内存数据库中的数据保存到磁盘中,即使当前redis进程退出,当该进程重启的时候,会加载磁盘中的RDB,从而恢复内存中的数据。与AOF文件中保存命令不同,RDB文件中保存的是压缩后的键值对信息。
生成RDB文件的方式
- 手动方式,客户端可以通过SAVE、BGSAVE
- SAVE: 服务端进程会被阻塞,在RDB文件没有创建完成之前,redis服务进程将会一直被阻塞,在阻塞期间,redis实例无法处理客户端发来的任何命令
- BGSAVE:该命令会fork一个主进程的子进程出来,该子进程来完成创建RDB文件,在此期间主进程能够一直对外提供服务。
- 自动方式,指的是redis实例会根据配置文件中声明的策略,通过定时的执行BGSAVE命令,来完成数据的持久化工作。
常见的配置策略(默认)如下:
save 900 1
save 300 10
save 60 10000
该策略的含义为:
如果服务器在900秒内,对数据库进行了至少1次修改
如果服务器在300秒内,对数据库进行了至少10次修改
如果服务器在60秒内,对数据库进行了至少10000次修改
请注意,上面三个策略是或的关系,即任何一个条件满足都会触发一次BGSAVE
总而言之,RDB文件中存储的是数据的键值对,存储的是真实的数据,而AOF文件中存储的是服务端执行的命令。
关于RDB和AOF两种策略的选择
redis是支持这两种策略同时生效的,也就是说你可以同时开启RDB和AOF这两种持久化机制,来确保数据安全。但是天下没有免费的午餐,两种机制都打开的话是会消耗服务器的性能的,所以你需要在此权衡。如果你做的是金融类项目,建议你还是将两种持久化策略都打开,并且AOF的策略最好设置为everysec
,因为数据丢失在金融业务中是绝不允许的。
主从复制
redis可以通过主从复制机制来实现数据的多机热备,是除数据持久化以外的另外一种数据冗余方式。主从复制机制也是后续要介绍的其他高可用方案的基础,所以主从复制机制是redis众多高可用方案的基石。
因为历史原因,redis存在两种复制的实现,分别是2.8版本之前的复制机制(我们称之为旧版复制)和2.8版本之后的复制机制(我们称之为新版复制机制),接下来我们将会分别介绍两个版本的实现方案。
redis的旧版同步
如何使用同步
可以使用slaveof host port
命令实现主从复制, 此时的主从服务器的关系如下
同步的过程分为全量同步和增量同步两个过程
全量同步
当从服务器接收到客户端发来的slaveof命令后,就意味着从服务器需要从主服务器那里同步数据,因为从服务器是一个刚刚启动的实例,所以此时从服务器需要和主服务器完成一次全量同步,全量同步的步骤如下:
- 从服务器向主服务器发送
SYNC
命令 - 主服务器接收到从服务器的
SYNC
命令后会执行BGSAVE
命令,为当前数据库生成一个RDB快照文件。并且主服务器会创建一个缓冲区用来保存执行BGSAVE
命令之后客户端发来的命令 - 当主服务器的RDB文件生成完成后,主服务器会将生成的RDB文件发送给从服务器
- 从服务器收到RDB文件后会将其载入到数据库中,此时从服务器的数据与主服务器执行
BGSAVE
命令那一刻的数据是一致的 - 主服务器会将缓冲区中的命令发送给从服务器
- 从服务器执行主服务器发来的命令,这是主从服务器的数据是一致性
增量同步
当完成上述步骤后,主、从服务器的数据在当前是一致的,但是这种一致是暂时的。因为如果此时某个客户端向主服务器发送一条新的写命令,当主服务器执行完客户端发来的命令后,主从服务器的数据又会不一致。增量同步的目的就是为了能让主、从服务器的数据从这种暂时的不一致转为一致。增量同步的实现很简单,就是将主服务器发来的命令在从服务器上执行一遍即可。
这个同步的过程可以用下图表示:
该方案的缺陷
如果当前从服务器是刚刚启动,并且此前没有复制过任何一个主服务器,那么该方案能很好的完成任务。但是考虑这个场景,如果当前从服务器在增量同步阶段因为某种原因断开了和主服务器的连接,一段时间后又重新和主服务器建立了连接。也许主、从服务器断开的时间只有几秒钟,他们之间的差异就是几个命令而已(这取决于从服务器的断线时间和主服务器在这段时间的业务访问量),但是旧版复制功能却只能通过全量同步流程使主、从服务器的数据重新一致。而且全量复制是非常耗费资源的,因为生成并发送RDB文件的操作都会大量的消耗主服务器的IO、CPU和存储资源,所以这个版本的方案无法很好的应对从服务器断线重连的这个场景。
redis的新版复制
旧版复制功能无法处理断线重连场景的根本原因是,当从服务器断线重连以后,主服务器无法得知当前自己的数据和从服务器的数据有多少差异,如果这个差异能计算出来,那么主服务器只需要将从服务器断线这段时间内自己所执行的命令发送给从服务器,从服务器执行完这些命令后,主从的数据就可以回到一致的状态。
针对旧版本同步功能的缺陷,新版的复制功能通过加入部分重同步功能很好的解决了主服务器处理断线重连场景的低效问题。新版复制功能和旧版复制功能相比,其主要新增的内容如下:
- 主服务器和从服务器都会维护一个复制偏移量的字段
当主服务器向从服务器发送一个命令后,主服务器会将自己维护的复制偏移量字段+1。
当从服务器接收并执行主服务器发来的命令后,从服务器也会将自己维护的复制偏移量字段+1。
这样便可以通过比较偏移量的值来确定当前主从服务器中数据不一致的差异。 - 主服务器新增复制积压缓冲区
复制积压缓冲区是一个定长的FIFO队列,默认的大小为1MB。主服务器不会存储所有数据的偏移量信息(这是需要额外的内存来维护的),它只会存储近一段时间内同步的数据和对应的偏移量信息,这是一种用空间换时间的优化。 - 所有服务器都新增机器的运行时ID字段
如果一个主服务器有多个从服务器,其中任意一个从服务器断线重连后,主服务器需要知道当前的从服务器是哪一台机器,拿到从服务器的运行时ID后主服务器会走如下流程来实现主从服务器的数据一致:
- 如果该从服务器从未和当前主服务器进行过复制,则直接走全量同步
- 如果该从服务器和当前主服务器曾经进行过复制,那么主服务器就会去判断当前从服务器发来的数据偏移量在复制积压缓冲区中是否能找到
- 如果能找到,则将差异的命令发送给从服务器执行
- 如果不能找到,则直接走全量同步
新版同步方案的执行流程如下:
redis保证的是数据的最终一致性,主服务器和从服务器的数据复制是异步的,这也意味当客户端的命令在主服务器执行后就会立刻返回,并不会等到主从服务器数据同步完成后再返回。所以redis并不满足CAP理论中关于一致性的要求。即使在主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以 Redis 满足CAP理论中的可用性。
问:如果客户端命令在主服务器执行完成后,主服务器向从服务器同步数据的时候,命令丢失了,会不会造成主从数据的不一致?
答:这种不一致是暂时的,因为正常情况下,每次主从数据同步完成后,从服务器都会向主服务器返回一个ACK信息,该ACK信息中包含着当前从服务器的数据偏移量信息,主服务器会保存该偏移量。如果主服务器发现该从服务器的偏移量与自己的偏移量不一致,会从复制积压缓冲区中取出该从服务器缺少的数据,发送给从服务器,从而使得主从服务器数据重新一致。
引入第三者监控服务
引入第三者监控服务实现高可用的主要流程为,第三者监控软件实时监控主、从服务器的健康,当发现主服务器挂掉之后,立即将从服务器升级为主服务器继续对外提供服务。根据其实现原理,主要分为redis+keepalived 和redis哨兵两种方案。
redis + keepalived的方案
keepalived简介
Keepalived的作用是检测服务器的状态,如果有一台web服务器宕机,或工作出现故障,Keepalived将检测到,并将有故障的服务器从系统中剔除,同时使用其他服务器代替该服务器的工作,当服务器工作正常后Keepalived自动将服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的服务器。
keepalived实现高可用的优缺点
优点
- 架构简单
- 运维简单
- 部署成本低,最低只需要两台机器(相较于哨兵的方案)
- 基于虚拟IP,当发生主备切换时,客户端是无感知的
缺点
- 至少需要两台服务器,其中一台为master始终提供服务,另外一台作为backup始终处于空闲状态(继承自主从复制)
keepalived实现高可用的设计思路
当 Master 与 Slave 均正常工作时, Master负责服务,Slave负责Standby。
当 Master 挂掉,Slave 正常时, Slave接管服务,同时关闭主从复制功能。
当 Master 恢复正常,则从Slave同步数据,同步数据之后关闭主从复制功能,恢复Master身份,于此同时Slave等待Master同步数据完成之后,恢复Slave身份。
然后依次循环。
keepalived+redis的架构图
个人觉得在业务量不大的时候,keepalived+redis的方案是一个很好的方案,它既能实现我们对高可用的需求,又能很好的减少部署成本和运维成本。国内某银行的贷前系统采用的就是这套高可用方案,该银行通过其线上系统,一年放出了将近100亿的贷款,从来没出过岔子。
redis的哨兵机制
哨兵机制是redis提供的一种高可用的解决方案,该方案能够实现监控多个redis主节点及其从节点的需求。
下面通过一组示意图来讲解哨兵机制的工作流程
1、当前服务中存在三个主服务器,并且每个主服务器都有一个从服务器,有一个哨兵集群监控着这六个服务器的健康。
2、在某一时刻主服务器1因为某种原因宕机,与此同时,从服务器1停止了与主服务器1的复制,其示意图如下所示(服务宕机用虚线表示)
3、主服务器1宕机的事实被哨兵集群中的某个哨兵节点察觉(哨兵每秒钟都会向所有被其监控的节点发送PING
命令),该节点将服务器1的状态标记为主观下线状态,并向其他哨兵节点询问该服务器是否下线
4、此时,如果哨兵集群中的半数以上节点都返回主服务器1已下线,则发现服务器1下线的哨兵会将服务器1的状态标记为客观下线状态
5、将主服务器1标记为客观下线后,通过一系列的操作,节点1(红色)当选为哨兵集群的leader,redis哨兵的选举方案是对Raft算法选举的实现,不是这里的重点,所以不展开介绍
6、哨兵集群完成选举后是,哨兵的leader需要对主服务器1进行故障转移
- 如果主服务器1只有一个从服务器的话,该从服务器将会被升级为主服务器。
- 如果主服务器1有多个从服务器,哨兵会在多个从服务器中挑选一个合适的节点,并将其升级为主服务器,其挑选从服务器的策略如下:
- 首先剔除已经下线或断开连接的从服务器。
- 剔除5秒内没有回复哨兵消息的从服务器(哨兵节点会每隔10秒向其监控的每隔服务器发送INFO命令)。
- 比较剩余服务器中的复制偏移量,选偏移量最大的服务器当主服务器。
7、哨兵集群会一致监控着原主服务器1的状态,一旦发现原来的主服务器从故障中恢复,则会立即向其发送SLAVEOF
命令,让其成为从服务器
哨兵机制的优缺点
基于主从复制模式实现,所以优缺点都继承自主从复制模式,但是哨兵机制却至少需要多出三台机器来实现哨兵节点的高可用,所以其对机器资源的需求较大。
关于keepalived+redis和哨兵方案的选择
- 从运维的难易程度上讲
keepalived+redis的方案适合redis实例数量较少的场景,例如上面例子中提到的一主一从的架构。因为检查实例是否健康以及实例发生故障后执行故障转移的脚本都是需要自己维护的,所以当实例变得很多的时候,这种方案就显得力不从心。
而哨兵方案中的哨兵集群更像是一种基础设施,他可以监控一个或多个redis主从集群是否健康,当被哨兵监控的某台服务发生故障后,哨兵能够自动发现故障并自动完成故障转移。但是哨兵的缺点就是哨兵至少需要三台机器做集群,如果业务系统所需要的redis实例个数不多的话,哨兵的方案就显得有点昂贵。 - 集群伸缩性
keepalived+redis的方案在伸缩性上的表现非常的差,如果需要在当前的主从复制集群中新增一个redis,那么只能修改keepalive的
配置文件和脚本,并重启服务。
而在哨兵方案中,哨兵实例会定期的向被其监控的redis实例发送INFO
命令,根据命令返回的信息来动态发现集群中实例个数的变化。
水平切分(分片)
分片(partitioning)就是将数据拆分到多个 Redis 实例的过程,这样每个实例将只包含所有键的子集。
主从模式的缺点,即使最简单的一主一从的架构,也要求主服务器和从服务器的配置是一样的,至少不能差太远。你应该听说过著名的木桶理论,其核心内容为:**一只木桶盛水的多少,并不取决于桶壁上最高的那块木块,而恰恰取决于桶壁上最短的那块。**木桶理论能成立的前提是我的桶是水平放在地上的,假如我把桶斜着放呢?那我这个桶能装多少水就取决于桶壁上最高的那块木块了。我个人理解分片方案就是上述把桶斜着放这种思想的一种体现,因为我无法保证我的服务器配置都一样好,总有些服务器的性能好一点,有些服务器的性能差一点。理想状态是性能好的机器就多处理一点任务,而性能差的机器就少处理一点任务。分片方案的诞生就是为了实现这个目的。
常见的分片方案为哈希分片,即对于命令中的每一个key都执行一次hash运算并以集群实例个数取模,这样计算得到的结果就是redis的实例在集群中的编号。但是这种简单的哈希分片会导致集群的伸缩性比较差,当集群中实例的数量从3个增加到5个的时候,所有的数据都必须要重新计算其所属的实力编号。为了解决这个问题,有人提出了更高级的 一致性HASH
常见的分片方案有客户端分片和服务端分片
客户端分片
所谓客户端分片,就是由客户端自己计算数据的key应该在哪个机器上存储和查找,这样做的好处就是降低了服务端集群的复杂度,但是缺点也是显而易见的,客户端分片对集群的伸缩性的支持也是比较差的,我们主要介绍redis的服务器端分片。
服务端分片
redis提供的集群方案就是通过服务端分片来实现的,而redis服务器端分片的核心是槽指派,下面我们来分析一下redis的集群方案。
槽指派
redis将整个数据库分成16K(16384)个槽,每一个数据库key都属于这16K个槽的其中一个,你可以通过CLUSTER ADDSLOTS <slot> [slot ...]
命令完成槽指派。假设目前集群三个节点的端口号为7000,7001,7002,通过槽指派命令可以实现让7000节点负责编号第05461的槽位,7001节点负责编号第546210922的槽位,7002节点负责编号第10923~16384的槽位。
记录槽指派信息
集群中的每一个redis实例都会维护一个长度为16384/8=2048个自己的二进制位数组,数组的索引代表对应的槽位,而对应索引的值如果为1则表示数据由当前实例负责处理,0则表示数据不由当前实例负责处理。示意图如下:
在redis集群启动的时候,集群中的每个实例都会向其他实例发送自己的槽指派信息,每个实例都会将收到的槽指派信息保存起来,用于后续的请求转发。
下面我们以一个有三个实例的redis集群为例介绍一下redis集群的工作流程
下图表示一个有三个redis实例的集群
1、通过槽指派命令后,每个节点都被分配用于处理对应槽位的数据,并且通过槽指派信息在集群中的传播,集群中的每个节点都知道数据库中16K个槽都分别被指派给了哪些节点。
2、假设此时客户端向集群中的redis1实例发来一个命令 SET name foo
3、redis1实例计算当前key所对应的槽位,假设name的槽位为编号第10000号,redis1实例查询自己维护的槽指派信息后发现当前key不由自己处理,于是便向客户端返回一个MOVED错误,该错误类似HTTP协议中的重定向,MOVED错误中包含了重定向后的IP和端口号,以及对应的槽位信息。下图中以实例名称代替ip和port等信息。
4、客户端得到MOVED错误后知道自己应该去请求redis2实例,于是便重定向到redis2实例完成命令的执行。
集群的故障检测和故障转移
集群中的每个节点都会定期的向其他节点发送PING
命令来检测其他节点是否在线,与哨兵的机制类似,某个节点1发现另外一个节点2下线时,会将节点2的状态标记为疑似下线(PFAIL),然后将该消息发送给集群中的其他节点,如果集群中的半数以上节点都将节点2标记为疑似下线的话,则节点2将会被标记为已下线(FAIL)状态,接下来的故障转移流程与哨兵机制的故障转移流程类似,都是通过RAFT算法来实现的leader的选举,然后再将某个从节点升级为主节点,继续对外提供服务。