今天我们来聊一下互联网三高(高并发、高性能、高可用)中的高可用,看完本文相信能解开你关于高可用设计的大部分困惑
前言
高可用(High availability,即 HA)的主要目的是为了保障「业务的连续性」,即在用户眼里,业务永远是正常(或者说基本正常)对外提供服务的。高可用主要是针对架构而言,那么要做好高可用,就要首先设计好架构,第一步我们一般会采用分层的思想将一个庞大的 IT 系统拆分成为应用层,中间件,数据存储层等独立的层,每一层再拆分成为更细粒度的组件,第二步就是让每个组件对外提供服务,毕竟每个组件都不是孤立存在的,都需要互相协作,对外提供服务才有意义。
要保证架构的高可用,就要保证架构中所有组件以及其对外暴露服务都要做高可用设计,任何一个组件或其服务没做高可用,都意味着系统存在风险。
那么这么多组件该怎么做高可用设计呢,其实任何组件要做高可用,都离不开「冗余」和「自动故障转移」,众所周知单点是高可用的大敌,所以组件一般是以集群(至少两台机器)的形式存在的,这样只要某台机器出现问题,集群中的其他机器就可以随时顶替,这就是「冗余」。简单计算一下,假设一台机器的可用性为 90%,则两台机器组成的集群可用性为 1-0.1*0.1 = 99%,所以显然冗余的机器越多,可用性越高。
但光有冗余还不够,如果机器出现问题,需要人工切换的话也是费时费力,而且容易出错,所以我们还需要借助第三方工具(即仲裁者)的力量来实现「自动」的故障转移,以达到实现近实时的故障转移的目的,近实时的故障转移才是高可用的主要意义
怎样的系统可以称之为高可用呢,业界一般用几个九来衡量系统的可用性,如下
可用性级别 | 系统可用性% | 宕机时间/年 | 宕机时间/月 | 宕机时间/周 | 宕机时间/天 |
---|---|---|---|---|---|
不可用 | 90% | 36.5 天 | 73 小时 | 16.8 小时 | 144 分钟 |
基本可用 | 99% | 87.6 小时 | 7.3 小时 | 1.68 小时 | 14.4 分钟 |
较高可用 | 99.9% | 8.76 小时 | 43.8 分钟 | 10.1 分钟 | 1.44 分钟 |
高可用 | 99.99% | 52.56 分钟 | 4.38 分钟 | 1.01 秒 | 8.64 秒 |
极高可用 | 99.999% | 5.26 分钟 | 26.28 秒 | 6.06 秒 | 0.86 秒 |
一般实现两个 9 很简单,毕竟每天宕机 14 分钟已经严重影响业务了,这样的公司迟早歇菜,大厂一般要求 4 个 9,其他要求严苛的业务要达到五个九以上,比如如果因为一个电脑的故障导致所有列车停驶,那么就会有数以万计的人正常生活受到阻碍,这种情况就要求五个九以上
接下来我们就来一起看看架构中的各个组件如何借助「冗余」和「自动故障转移」来实现高可用。
互联网架构剖析
目前多数互联网都会采用微服务架构,常见架构如下:
可以看到架构主要分以下几层
- 接入层:主要由 F5 硬件或 LVS 软件来承载所有的流量入口
- 反向代理层:Nginx,主要负责根据 url 来分发流量,限流等
- 网关:主要负责流控,风控,协议转换等
- 站点层:主要负责调用会员,促销等基本服务来装配 json 等数据并返回给客户端
- 基础 service:其实与站点层都属于微服务,是平级关系,只不过基础 service 属于基础设施,能被上层的各个业务层 server 调用而已
- 存储层:也就是 DB,如 MySQL,Oracle 等,一般由基础 service 调用返回给站点层
- 中间件:ZK,ES,Redis,MQ 等,主要起到加速访问数据等功能,在下文中我们会简单介绍下各个组件的作用
如前所述,要实现整体架构的高可用,必须要实现每一层组件的高可用,接下来我们就来分别看一下每一层的组件都是如何实现高可用的
接入层&反向代理层
这两层的高可用都和 keepalived 有关,所以我们结合起来一起看
对外,两个 LVS 以主备的形式对外提供服务,注意只有 master 在工作(即此时的 VIP 在 master 上生效),另外一个 backup 在 master 宕机之后会接管 master 的工作,那么 backup 怎么知道 master 是否正常呢,答案是通过 keepalived,在主备机器上都装上 keepalived 软件,启动后就会通过心跳检测彼此的健康状况,一旦检测到 master 宕机,keepalived 会检测到,从而 backup 自动转成 master,对外提供服务,此时 VIP 地址(即图中的 115.204.94.139)即在 backup 上生效,也就是我们常说的「IP漂移」,通过这样的方式即解决了 LVS 的高可用。
keepalived 的心跳检测主要通过发送 ICMP 报文,或者利用 TCP 的端口连接和扫描检测来检测的,同样的,它也可以用来检测 Nginx 暴露的端口,如果某些 Nginx 不正常 keepalived 也能检测到并将其从 LVS 能转发的服务列表中剔出。
借用 keepalived 这个第三方工具,同时实现了 LVS 和 Nginx 的高可用,同时在出现故障时也可以将宕机情况发送到对应开发人员的邮箱以让他们及时收到通知处理,确实很方便,keepalived 应用广泛,下文我们也能看到它也可以用在 MySQL 上来实现 MySQL 的高可用。
微服务
接下来我们再来看一下「网关」,「站点层」,「基础服务层」,这三者一般就是我们所说的微服务架构组件,当然这些微服务组件还需要通过一些 RPC 框架如 Dubbo 来支撑,所以微服务要实现高可用,就意味着 dubbo 这些 RPC 框架也要提供支撑微服务高可用的能力,我们就以 dubbo 为例来看下它是如何实现高可用的
我们先来简单地看下 dubbo 的基本架构
思路也很简单,首先是 Provider(服务提供者)向 Registry(注册中心,如 ZK 或 Nacos 等)注册服务,然后 Consumer(服务消费者)向注册中心订阅和拉取 Provider 服务列表,获取服务列表后,Consumer 就可以根据其负载均衡策略选择其中一个 Provider 来向其发出请求,当其中某个 Provider 不可用(下线或者因为 GC 阻塞等)时,会被注册中心及时监听(通过心跳机制)到,也会及时推送给 Consumer,这样 Consumer 就能将其从可用的 Provider 列表中剔除,也就实现了故障的自动转移,不难看出,注册中心就起到了类似 keepalived 的作用
中间件
我们再来看下这些中间件如 ZK,Redis 等是如何实现高可用的呢
ZK
上一节微服务中我们提到了注册中心,那我们就以 ZK(ZooKeeper)为例来看看它的高可用是如何实现的,先来看下它的整体架构图如下
Zookeeper 中的主要角色如下
- Leader: 即领导者,在集群中只有一个 Leader,主要承担了以下的功能
- 事务请求的唯一调度和处理者,保证集群事物处理的顺序性,所有 Follower 的写请求都会转给 Leader 执行,用来保证事务的一致性
- 集群内部各服务器的调度者:处理好事务请求后,会将数据广播同步到各个 Follower,统计 Follower 写入成功的数量,超过半数 Follower 写入成功,Leader 就会认为写请求提交成功,通知所有的 Follower commit 这个这与操作,保证事后哪怕是集群崩溃恢复或者重启,这个写操作也不会丢失。
- Follower:
- 处理客户端非事务请求、转发事务请求给 leader 服务器
- 参与事物请求 Proposal 的投票(需要半数以上服务器通过才能通知 leader commit 数据; Leader 发起的提案,要求 Follower 投票)
- 参与 Leader 选举的投票
画外音:Zookeeper 3.0 之后新增了一种 Observer 的角色,不过与此处讨论的 ZK 高可用关系不是很大,为了简化问题,所以省略
可以看到由于只有一个 Leader,很显然,此 Leader 存在单点隐患,那么 ZK 是怎么解决此问题的呢,首先 Follower 与 Leader 会用心跳机制保持连接,如果 Leader 出现问题了(宕机或者因为 FullGC 等原因无法响应),Follower 就无法感知到 Leader 的心跳,就会认为 Leader 出问题了,于是它们就会发起投票选举,最终在多个 Follower 中选出一个 Leader 来(这里主要用到了 Zookeeper Atomic Broadcast,即 ZAB 协议,它是为 ZK 专门设计的一种支持崩溃恢复的一致性协议),选举的细节不是本文重点,就不在此详述了。
除了 ZAB 协议,业界上常用的还有 Paxos,Raft 等协议算法,也可以用在 Leader 选举上,也就是是在分布式架构中,这些协议算法承担了“第三者”也就是仲裁者的作用,以承担故障的自动转移
Redis
Redis 的高可用需要根据它的部署模式来看看,主要分为「主从模式」和「Cluster 分片模式」两种
主从模式
先来看一下主从模式,架构如下
主从模式即一主多从(一个或者多个从节点),其中主节点主要负责读和写,然后会将数据同步到多个从节点上,Client 也可以对多个从节点发起读请求,这样可以减轻主节点的压力,但和 ZK 一样,由于只有一个主节点,存在单点隐患,所以必须引入第三方仲裁者的机制来判定主节点是否宕机以及在判定主节点宕机后快速选出某个从节点来充当主节点的角色,这个第三方仲裁者在 Redis 中我们一般称其为「哨兵」(sentinel),当然哨兵进程本身也有可能挂掉,所以为了安全起见,需要部署多个哨兵(即哨兵集群)
这些哨兵通过 gossip(流言) 协议来接收关于主服务器是否下线的信息,并在判定主节点宕机后使用 Raft 协议来选举出新的主节点
Cluster 分片集群
主从模式看似完美,但存在以下几个问题
- 主节点写的压力难以降低:因为只有一个主节点能接收写请求,如果在高并发的情况下,写请求如果很高的话可能会把主节点的网卡打满,造成主节点对外无法服务
- 主节点的存储能力受到单机存储容量的限制:因为不管是主节点还是从节点,存储的都是全量缓存数据,那么随着业务量的增长,缓存数据很可能直线上升,直到达到存储瓶颈
- 同步风暴:因为数据都是从 master 同步到 slave 的,如果有多个从节点的话,master 节点的压力会很大
为了解决主从模式的以上问题,分片集群应运而生,所谓分片集群即将数据分片,每一个分片数据由相应的主节点负责读写,这样的话就有多个主节点来分担写的压力,并且每个节点只存储部分数据,也就解决了单机存储瓶颈的问题,但需要注意的是每个主节点都存在单点问题,所以需要针对每个主节点做高可用,整体架构如下
原理也很简单,在 Proxy 收到 client 执行的 redis 的读写命令后,首先会对 key 进行计算得出一个值,如果这个值落在相应 master 负责的数值范围(一般将每个数字称为槽,Redis 一共有 16384 个槽)之内,那就把这条 redis 命令发给对应的 master 去执行,可以看到每个 master 节点只负责处理一部分的 redis 数据,同时为了避免每个 master 的单点问题,也为其配备了多个从节点以组成集群,当主节点宕机时,集群会通过 Raft 算法来从从节点中选举出一个主节点
ES
再来看一下 ES 是如何实现高可用的,在 ES 中,数据是以分片(Shard)的形式存在的,如下图所示,一个节点中索引数据共分为三个分片存储
但只有一个节点的话,显然存在和 Redis 的主从架构一样的单点问题,这个节点挂了,ES 也就挂了,所以显然需要创建多个节点
一旦创建了多个节点,分片的优势就体现出来了,可以将分片数据分布式存储到其它节点上,极大提升了数据的水平扩展能力,同时每个节点都能承担读写请求,采用负载均衡的形式避免了单点的读写压力
ES 的写机制与 Redis 和 MySQL 的主从架构有些差别(后两者的写都是直接向 master 节点发起写请求,而 ES 则不是),所以这里稍微解释一下 ES 的工作原理
首先说下节点的工作机制,节点(Node)分为主节点(Master Node)和从结点(Slave Node),主节点的主要职责是负责集群层面的相关操作,管理集群变更,如创建或删除索引,跟踪哪些节点是集群的一部分,并决定哪些分片分配给相关的节点,主节点也只有一个,一般通过类 Bully 算法来选举出来,如果主节点不可用了,则其他从节点也可以通过此算法来选举以实现集群的高可用,任何节点都可以接收读写请求以达到负载均衡的目的
再说一下分片的工作原理,分片分为主分片(Primary Shard,即图中 P0,P1,P2)和副本分片(Replica Shard,即图中 R0,R1,R2),主分片负责数据的写操作,所以虽然任何节点可以接收读写请求,但如果此节点接收的是写请求并且没有写数据所在的主分片话,此节点会将写请求调度到主分片所在的节点上,写入主分片后,主分片再把数据复制到其他节点的副本分片上,以有两个副本的集群为例,写操作如下
MQ
ES 中利用数据分片来提升高可用和水平扩展能力的思想也应用在其他组件的架构设计上,我们以 MQ 中的 kafka 为例再来看下数据分片的应用
如上是 Kafka 集群,可以看到每个 Topic 的 Partition 都分布式存储在其它消息服务器上,这样一旦某个 Partition 不可用,可以从其他 follower 选举出 leader 继续服务,不过与 ES 中的数据分片不同的是,follower Partition 属于冷备,也就是说在正常情况下不会对外服务,只有在 leader 挂掉之后从多个 follower 中选出leader 后才能对外提供服务
存储层
接下来我们再来看一下最后一层,存储层(DB),这里我们以 MySQL 为例来简单地讨论一下其高可用设计,其实大家如果看完了以上的高可用设计,会发现 MySQL 的高可用也不过如此,思想都是类似的,与 Redis 类似,它也分主从和分片(即我们常说的分库分表)两种架构
主从的话与 LVS 类似,一般使用 keepalived 的形式来实现高可用,如下所示
如果 master 宕机了 ,Keepalived 也会及时发现,于是从库会升级主库,并且 VIP 也会“漂移”到原从库上生效,所以说大家在工程配置的 MySQL 地址一般是 VIP 以保证高可用
数据量大了之后就要分库分表了,于是就有了多主,就像 Redis 的分片集群一样,需要针对每个主配备多个从,如下
之前有读者问分库分表之后为啥还要做主从,现在我想大家应该都明白了,不是为了解决读写性能问题,主要是为了实现高可用
总结
看完了架构层面的高可用设计,相信大家对高可用的核心思想「冗余」和「自动故障转移」会有更深刻的体会,观察以上架构中的组件你会发现冗余的主要原因是因为只有一主,为什么不能有多主呢,也不是不可以,但这样在分布式系统下要保证数据的一致性是非常困难的,尤其是节点多了的话,数据之间的同步更是一大难题,所以多数组件采用一主的形式,然后再在主和多从之间同步,多数组件之所以选择一主本质上是技术上的 tradeoff
那么做好每个组件的高可用之后是否整个架构就真的可用了呢,非也,这只能说迈出了第一步,在生产上还有很多突发情况会让我们的系统面临挑战,比如
- 瞬时流量问题:比如我们可能会面临秒杀带来的瞬时流量激增导致系统的承载能力被压垮,这种情况可能影响日常交易等核心链路,所以需要做到系统之间的隔离,如单独为秒杀部署一套独立的集群
- 安全问题:比如 DDOS 攻击,爬虫频繁请求甚至删库跑路等导致系统拒绝服务
- 代码问题:比如代码 bug 引起内存泄露导致 FullGC 导致系统无法响应等
- 部署问题:在发布过程中如果贸然中止当前正在运行的服务也是不行的,需要做到优雅停机,平滑发布
- 第三方问题:比如我们之前的服务依赖第三方系统,第三方可能出问题导致影响我们的核心业务
- 不可抗力:如机房断电,所以需要做好容灾,异地多活,之前我司业务就由于机房故障导致服务四小时不可用,损失惨重
所以除了做好架构的高可用之外,我们还需要在做好系统隔离,限流,熔断,风控,降级,对关键操作限制操作人权限等措施以保证系统的可用。
这里特别提一下降级,这是为了保证系统可用性采取的常用的措施,简单举几个例子
- 我们之前对接过一个第三方资金方由于自身原因借款功能出了问题导致无法借款,这种情况为了避免引起用户恐慌,于是我们在用户申请第三方借款的时候返回了一个类似「为了提升你的额度,资金方正在系统升级」这样的文案,避免了客诉
- 在流媒体领域,当用户观看直播出现严重卡顿时,很多企业的第一选择不是查 log 排查问题,而是为用户自动降码率。因为比起画质降低,卡的看不了显然会让用户更痛苦
- 双十一零点高峰期,我们把用户的注册登录等非核心功能给停掉了,以保证下单等核心流程的顺利
另外我们最好能做到事前防御,在系统出问题前把它扼杀在摇篮里,所以我们需要做单元测试,做全链路压测等来发现问题,还需要针对 CPU,线程数等做好监控,当其达到我们设定的域值时就触发告警以让我们及时发现修复问题(我司之前就碰到过一个类似的生产事故复盘大家可以看一下),此外在做好单元测试的前提下,依然有可能因为代码的潜在 bug 引起线上问题,所以我们需要在关键时间(比如双十一期间)封网(也就是不让发布代码)
此外我们还需要在出事后能快速定位问题,快速回滚,这就需要记录每一次的发布时间,发布人等,这里的发布不仅包括工程的发布,还包括配置中心等的发布
画外音:上图是我司的发布记录,可以看到有代码变更,回滚等,这样如果发现有问题的话可以一键回滚
最后我们以一张图来总结一下高可用的常见手段