笔记之高可用架构

626 阅读15分钟

本文来自极客时间《架构实战案例解析》--王庆友

01 高可用架构:基本常识

1.1 系统有哪些故障点?

这里,我用红色部分,标识出了整个处理过程中可能出现的故障点,如下图所示:

image-20220408224204644

这些故障点可以归纳为三类:

  1. 资源不可用。包括网络和服务器出故障,网络出故障表明节点连接不上,服务器出故障表明该节点本身不能正常工作。
  2. 资源不够用。常规的流量进来,节点能正常工作,但在高并发的情况下,节点无法正常工作,对外表现为响应超时。
  3. 节点的功能有问题。比如接口的内部业务逻辑有问题。

1.2 高可用策略

高可用的总体解决思路如下。

image-20220408222440853

  • 避免发生

要想让系统能够稳定可用,我们首先要考虑如何避免问题的发生。比如说,我们可以通过事先增加机器来解决硬件资源不足的问题。

  • 转移故障

如果问题真的发生了,我们就要考虑怎么转移故障(Failover)。比如说,我们可以通过冗余部署,当一个节点发生故障时,用其它正常的节点来代替问题节点。

  • 降低影响

如果故障无法以正面的方式解决,我们就要努力降低故障带来的影响。比如说流量太大,我们可以通过限流,来保证部分用户可以正常使用,或者通过业务降级的手段,关闭一些次要功能,保证核心功能仍旧可用。

  • 快速恢复

我们要尽快找到问题的原因,然后修复故障节点,使系统恢复到正常状态。

1.3 高可用架构原则

我们在做架构设计时,就可以从正面保障和减少损失两个角度来考虑具体的应对手段。

image-20220408222531081

第一个设计原则是冗余无单点。在接入层中,我们可以实现负载均衡的双节点部署,这样在一个节点出现问题时,另一个节点可以快速接管,继续提供服务。

第二个设计原则是水平扩展。我们需要通过增加机器数量,水平扩展这些节点的处理能力。

对于无状态的计算节点,比如应用层和服务层来说,水平扩展相对容易,我们直接增加机器就可以了;而对于有状态的节点,比如数据库,我们可以通过水平分库做水平扩展,不过这个需要应用一起配合,做比较大的改造。

第三个原则是柔性事务。在实践中,我们更多地使用 BASE 理论来指导系统设计。

我们平时对单个数据库事务的 ACID,因为这里不存在 P,所以 C 和 A 都能得到很好地保证,这是一种刚性事务。但在复杂的分布式场景下,基于 BASE 理论,我们通常只能实现部分的 C(软状态和最终一致)和部分的 A(基本可用),这是一种柔性事务。

柔性事务具体的实现方式有很多,比如说,通过异步消息在节点间同步数据。

第四个原则是系统可降级。当系统问题无法在短时间内解决时,我们就要考虑尽快止损,为故障支付尽可能小的代价。具体的解决手段主要有以下这几种。

  • 限流:让部分用户流量进入系统处理,其它流量直接抛弃。
  • 降级:系统抛弃部分不重要的功能,比如不发送短信通知,以此确保核心功能不受影响。
  • 熔断:我们不去调用出问题的服务,让系统绕开故障点,切断通路,避免系统资源大量被占用。 比如,用户下单时,如果积分服务出现问题,我们就先不送积分,后续再补偿。
  • 功能禁用:针对具体的功能,我们设置好功能开关。比如商品搜索,在系统繁忙时,我们可以选择不进行复杂的深度搜索。

最后一个设计原则,是系统可监控。

1.4 高可用手段

  • 客户端 -> 接入层

客户端到服务端通常是远程访问,所以我们首先要解决网络的可用性问题。

针对网络的高可用,我们可以拉多条线路,比如在企业私有的 IDC 机房和公有云之间,同时拉移动和电信的线路,让其中一条线路作为备份,当主线路有问题时就切换到备份线路上。

在接入层,也有很多成熟的 HA 方案,比如说,你可以选择 Nginx、HAProxy、LVS 等负载均衡软件,它们都能很好地支持双节点 + Keepalived 部署。这样当一个节点出了问题,另一个节点就可以自动顶上去,而且两个节点对外是共享一个虚拟 IP,所以节点的切换对外部是透明的。

这里,我们通过冗余和自动切换避免了单点的故障。

  • 接入层 -> Web 应用

Web 应用通常是无状态的,,我们可以部署多个实例,很方便地通过水平扩展的方式,提升系统的处理能力。

接入层的负载均衡设备,可以通过各种算法进行多个 Web 实例的路由,并且对它们进行健康检测,如果某个实例有问题,请求可以转发到另一个实例进行处理,从而实现故障的自动转移。

通常情况下,我们还可以在接入层做限流,比如,在 Nginx 中设置每秒多少个并发的限制,超过这个并发数,Nginx 就直接返回错误。

这里,我们同时支持了 Web 节点的水平扩展、自动故障转移以及系统的可降级(限流)。

  • Web 应用 -> 内部服务

服务通常也是无状态的,我们也可以通过部署多个实例进行水平扩展,支持服务实例的发现和负载均衡,比如在 Spring Cloud 中,我们就可以通过 Eureka 进行服务的自动注册和路由。

应用通常会访问多个服务,我们在这里可以做服务的隔离和熔断,避免服务之间相互影响。比如在 Spring Cloud 的 Hystrix 组件中,我们可以为不同服务配置不同的线程池,实现资源隔离,避免因为一个服务响应慢,而占用所有的线程资源;如果某个服务调用失败,我们可以对它进行熔断操作,避免无谓的超时等待,影响调用方的整体性能。

在应用和服务的内部,针对具体的功能,我们还可以做一些功能开关(这个在 eBay 有大量的落地)。

这里,我们同时支持了服务节点的水平扩展、自动故障转移以及系统的可降级(熔断和业务开关)。

  • 访问基础资源

常见的资源包括关系数据库、缓存和消息系统。

关系数据库属于有状态服务,它的水平扩展没有那么容易,但还是有很多手段能够保障数据库的可用性和处理能力。

首先,我们可以做数据库的主从部署,数据库有成熟的 MHA 方案,支持主库故障时,能够自动实现主从切换,应用可以通过 VIP 访问数据库,因此这个切换过程对应用也是透明的。

另外,我们也可以通过物理的水平分库方式,对数据进行分片,这样就有多个主库支持写入。

02 高可用架构案例:架构改造

2.1 项目背景介绍

这是一个小程序点餐平台。这个项目服务于一家大型的餐饮公司,公司在全国有大量的门店。他们准备搞一个长期的大型线上促销活动,促销的力度很大:用户可以在小程序上先领取优惠券,然后凭券再支付 1 元,就可以购买价值数十元的套餐。

结合以往的经验,以及这次的促销力度,我们预计在高峰时,前端小程序请求将会达到每秒 10 万 QPS,并且预计首日的订单数量会超过 500 万。在这种高并发的情况下,我们为了保证用户的体验,系统整体的可用性要达到 99.99%。

这个点餐平台的具体架构如下。

image-20220410223800721

系统主要的调用过程如下。

  1. 小程序前端通过 Nginx 网关,访问小程序服务端;
  2. 小程序服务端会调用一系列的基础服务,完成相应的请求处理;
  3. 订单服务接收到新订单后,先在本地数据库落地订单,然后通过 MQ 同步订单给 OMS 履单中心;
  4. 门店的收银系统通过 HTTP 远程访问云端的 OMS 履单中心,拉取新订单,并返回取餐码给 OMS,OMS 再调用小程序订单服务同步取餐码;
  5. 小程序前端刷新页面,访问服务端获得取餐码,然后用户可以根据取餐码到门店取餐或等待外卖。

2.2 高可用系统改造措施

这次活动的促销力度很大,高峰期流量将达到平时的数十倍,这就要求系统能够在高并发的场景下,保证高可用性。

在下面的系统架构图中,我标出了具体的改造点,主要有 10 处。

image-20220410223918889

  1. 前端接入改造

这里的前端有两个,C 端的小程序和 B 端的门店收银系统。前端部分主要是对三个点进行改造,包括小程序端的 CDN 优化、Nginx 负载均衡,以及收银端的通信线路备份。

  1. 应用和服务的水平扩展

针对小程序服务端的部署,我们把实例数从十几台提升到了 100 台,水平扩展它的处理能力。

  1. 订单水平分库

下单高峰期,订单主库的写访问频率很高,一个订单会对应 6~7 次的写操作。改造后,我们有 4 个主库来负责订单数据写入。数据库的配置,也从原来的 8 核 16G 提升到了 16 核 32G。

这里的订单水平分库是通过订单 ID 取模进行分库。

  1. 异步化处理

订单中心和后台 OMS 之间我们通过消息系统对它们进行了解耦。

还有在小程序服务端,在用户支付完成或者后台生成取餐码后,我们会以微信消息的方式通知用户,这个在代码中,也是通过异步方式实现的。

  1. 主动通知+轮询

改造后,我们落地了消息推送中心,收银系统通过 Socket 方式,和推送中心保持长连接。

为了避免因消息推送中心出问题,导致收银系统拿不到新订单,收银系统还保持对 OMS 系统的轮询,但频率降低到了 1 分钟一次。

  1. 缓存的使用

当收银系统向 OMS 拉取新订单时,OMS 不是到数据库里查询新订单,而是把新订单先保存在 Redis 队列里,OMS 通过直接查询 Redis,把新订单列表返回给收银系统。

每天凌晨,我们通过定时任务,模仿前端小程序,遍历访问每个商品数据,实现对缓存的预刷新,进一步保证缓存数据的一致性,也避免了缓存数据的同时失效,导致缓存雪崩。

  1. 一体化监控

我们通过采集节点的状态数据,实时监测每个节点的健康程度,并且用红黄绿三种颜色,表示每个节点的健康状况。这里所有的节点都在一个页面里显示,包括 Web 应用、Redis、MQ 和数据库,页面也会体现节点之间的上下游关系。

2.3 系统改造小结

我们是如何知道要配置多少个节点,有没有达到预定的效果呢?

对于这个问题,我们的做法是,按照 10 万 QPS 和 99.99% 的可用指标要求,通过大量的压测来确定的。

03 高可用架构:系统监控

很多时候,我们可以从监控的角度来倒推系统的可用性设计。

3.1 监控的分类

系统的组成包括接入层、应用系统、中间件、基础设施这几个部分,那我们的监控也是针对这些部分来实施的。一般来说,监控可以分为 5 个层次,如下图所示:

image-20220411223602506

  1. 用户体验监控:包括页面能否打开、关键接口的响应时间等等,用户体验监控一般结合前端的埋点来实现。
  2. 业务监控:比如说订单数、交易金额等等。
  3. 应用监控:比如接口在一段时间内的调用次数、响应时间、出错次数等等。(Cat、SkyWalking)
  4. 中间件监控:比如数据库、缓存、Tomcat 等等,这些组件对应的是系统的 PaaS 层。这些中间件往往带有配套的监控系统,比如,RabbitMQ 就有自带的监控后台。
  5. 基础平台监控:指的是对系统底层资源进行监控,如操作系统、硬件设备等等,这个层次的监控对应的是系统的 IaaS 层。(Zabbix)

3.2 监控的痛点

image-20220411224032751

  1. 发现问题慢:业务监控的曲线一般 1 分钟更新一次,有时候因为正常的业务抖动,Monitor 还需要把这种情况排除掉。因此,他会倾向于多观察几分钟,这样就导致问题的确认有很大的滞后性。
  2. 定位问题慢:系统节点多,大量的人需要介入排查,而且由于节点依赖复杂,需要反复沟通才能把信息串起来,因此很多时候,这种排查方式是串行或者说无序的。
  3. 解决问题慢:当定位到问题,对系统进行调整后,验证问题是否已经得到解决,也不是一件很直观的事情,需要各个研发到相应的监控系统里去进行观察,通过滞后的业务曲线观察业务是否恢复。

3.3 解决思路

我们能不能把系统所有的监控信息自动关联起来,并且以一种直观的方式展示,让所有人一看就明白是哪里出了问题,以及出问题的原因是什么呢?

从这个思路出发,对系统的监控,我们需要做到两点:

  1. 系统能够自动地判断每个节点是否正常,并直观地给出结果,不需要经过专业人员的分析。
  2. 系统能够自动把各个节点的监控信息有机地串起来,从整体的角度对系统进行监控,不需要很多人反复地进行沟通。

在交通图上,通过红黄绿三种状态,实时地反映了每条道路的拥堵情况。

image-20220411224204268

这里有几个关键词:实时、直观、整体。

  • 首先要实时,我们需要第一时间知道系统当前是否有问题
  • 然后要直观,节点是否有问题,我们需要很直观地就能判断出来,就像交通图上的红黄绿颜色标识一样。
  • 最后是整体,我们需要针对系统做整体监控,就像交通图一样,它是针对周边整体的道路情况进行展示,我们也需要把系统的各个节点放在一起,清晰地给出节点依赖关系。

所以,对照道路交通监控的思路,我们可以采取这样的监控方式:

  • 首先,系统中的每个节点对应交通图的一条道路;
  • 然后,节点的健康状况对应道路的拥堵情况,节点同样也有红黄绿三种不同的颜色,来展示该节点是否正常;
  • 最后,节点之间的调用关系对应道路的方位关系。

3.4 架构方案和效果

根据前面的思路,我们设计了监控系统的整体架构,如下图所示:

image-20220411224632444

每个被监控的节点,均有对应的 Agent 负责采集健康数据。Agent 每隔 3s 采集节点数据,然后上报数据给 Monitor Service。Monitor Service 负责确定节点当前的状态并保存到数据库,这样就完成了节点健康状态的检测。

最后,前端 Dashboard 每隔 3s,拉取所有节点的状态,以红黄绿三种颜色在同一页面展示,同时还会显示具体的出错信息。