谢文辉:高可用系统架构实践丨声网开发者创业讲堂 Vol.02

本文内容源自「声网开发者创业讲堂 Vol.02」的演讲分享,分享讲师为《分布式应用系统架构设计与实践》图书作者、阿里巴巴高级技术专家谢文辉。大家可以点击(Agora开发者创业讲堂丨第2期:创业初期如何进行技术选型与架构设计?| 内含视频回放&资料),观看视频回放以及下载讲师 PPT。

在整个技术体系,特别是完善的技术能力体系中,有 3 点是系统和分布式系统都会非常看重的。

高性能,提升整个系统的并发响应;

高可用,提升整个系统的可使用能力;

可维护性,系统上线后进行方便、高效的运营维护。

这 3 点中,系统的可用性是第一要位。不管系统规模大小,特别是初创团队的系统很多都需要从 0 到 1 进行构建,可用性都是最为重要的指标。本期内容,我们将分享在高可用方面的一些架构实践。


01 高可用的理论基础


在正式开始前,我们先分享一个故事。

2015 年 5 月 27 日下午,支付宝出现了一个大规模的宕机事故。有用户反馈,支付宝有一些支付和登录功能不能用了。经过排查发现,问题的原因是杭州萧山区的光纤在维修时被挖断了。经过应急响应,5 月 27 日的晚上 7 点多,系统才被修复。

因为这个事件,阿里集团将每年的 5 月 27 日定义为「故障日」,以此来纪念和提醒所有的技术人,在技术相关体系中要关注线上系统的稳定性和可靠性。

2020 年天猫双 11,阿里出现了出现了机房断电事故。但这次事故其实是一场人为的演练,通过手动切断电源来检查整个系统的稳定性表现。在这次模拟演练中,系统的整体表现还是非常不错的。

这两个故事给我们提供了一个很重要的警示:系统的高可用应该成为系统设计里的第一原则。

系统的“高可用”应该怎样定义呢?我认为应该是在系统允许的时间范围内对外提供服务的能力。那么,为什么一定要强调“允许的时间范围”?试想一下,如果系统响应一个请求需要花费 5 分钟或者 10 分钟,那这种可用性的意义就已经不是很大了。在一个高并发服务的系统场景下,我们强调的系统阈值是用户体验允许的时间范围。

对系统故障的定义一般采用“ N 个 9” 方式来表示,例如 2 个 9 (99%)的系统可用性,代表全年就有 87 小时的系统故障,“5 个 9”(99.999%)代表全年的系统故障时间不会超过 6 分钟,“6 个 9”就已经非常小了,这对系统技术的设计要求是非常高的。

阿里集团内部对于故障处理提出了一个“1-5-10方法论”。不管是在什么情况下,1 分钟之内感知系统的问题并告警,5 分钟之内快速定位故障原因,10 分钟之内修复这些故障。从“1-5-10”的定义来说,其中还涉及了系统可维护性、系统监控和告警等方面的内容。

系统的分布式理论可以为高可用提供一些原则性的指导:

(1) CAP 理论

● 数据一致性(Consistency)

系统可用性(Availability)

系统的连通性,也称为分区的容忍性(Partition tolerance)。

这里是取三者的首字母,将其称为 CAP 理论。CAP 理论指出,这三者无法同时实现,只能取其二,在分布式多副本场景中,分布式系统架构下,由于网络连通性问题是不可避免的,也就是说网络抖动导致的数据分区是客观存在的,所以一般来说分布式多副本场景下 CAP 最终关注的是 AP 或者 CP 的选择问题。

我们把 CAP 理论的 3 个点拆开来看:

假设选择 C 和 A 并舍弃 P,说明这个系统没有所谓的分区,即不需要分区,这个系统基本上是单点或者说单机能力的一个系统。假设是一个分布式系统,P 一定要考虑它本身存在分区的特点。因此,再扩展一下,如果是分布式系统,那么就是多机或者多集群,甚至是多机房的情况。

再来看看满足 C、P 并舍弃 A 的情况,即舍弃系统的可用性,在常见的抢票、抢购等高并发的系统中,可能出现不能访问等故障。还有可能满足 A、P 并舍弃 C, 即舍弃数据一致性,这种情况下要求数据一致性不敏感。例如,一个账号的授权系统对于数据一致性的要求不是很高,就可以舍弃这一点。

(2) BASE 理论,它是 CAP 理论的延伸。BASE 理论也有 3 个要素:

● 基本可用 (Basically Available)

● 软状态 (Soft State)

● 最终一致性(Eventually Consistent)

BASE 理论关注数据的最终一致性。其中基本可用相对于高可用,也就是当出现系统故障的时候要保障仍然可用。比如以前可能 10 毫秒就能响应,现在需要 1 秒才能响应,但仍处于一个基本可用的状态。

软状态与原子性处理(一次处理全部成功或者全部失败)对应。软状态允许中间存在一些不一致的状态,这也是 BASE 理论中一个很重要的中间态。最终一致性相对的就是强一致性,通过技术的手段,能够让它以软状态达到最终的一致性。


02 系统分层高可用实现方案


接下来我想跟大家聊一聊系统分层调回的一些常见实现方案,其中包括业务逻辑层数据存储层以及机房层

3

上图是系统分层示意图。我们在进行系统设计时,都会先做一个接入层,比如 RPC 或者 Restful 之类。接入层还有一些其他的概念,如 Nginx 或者 Lvs,可以叫作接入层组件。但我们现在把这个接入层作为系统实现的接入层,中间还有一个服务层,把接入层和服务层统一叫作业务逻辑,实现这样的一块功能的层叫作业务逻辑层。不管是基于缓存还是基于数据库,最终都要落地到数据存储中。

除了这样分布在一个服务或者一个集群范围内的系统,我们再把视角放大一点,这里还有一个机房的概念,例如华东机房、华北机房、华南机房。不同的机房都会布这样的系统,实现机房之间容错或者故障切分。我们从这几个层来讨论业务逻辑层怎么做、数据存储层怎么做、机房的容灾或者流量的切换有哪些比较常见的做法。

业务逻辑层

对于业务逻辑层,这里有必要讲一讲“有状态” 和“无状态”。系统首先要实现高可用,就应该把系统做成无状态的,因为无状态能够更好地实现分布式系统的可用性扩展。

什么叫作有状态和无状态呢?

4

举个例子,假设用户的账号登录这样的一个系统,外部发送一个请求,有可能本次会请求到后端服务器的节点 1,也有可能会请求到节点 N。每一次请求的 session 的信息,很有可能就落在这样的一个单台服务器上。

如果每次请求或者每个人有不同的请求,会发现每台服务器的状态数据非常不一致。在节点 1 可能存了 10 个用户,节点 2 又存储其他的几个用户,导致数据非常不一致。这种服务器数据不一致的场景,我们称之为系统有状态。

那什么叫作无状态呢?例如刚才所说的账号登录了这个状态的数据,把会话数据剥离出来,系统处理例如分布式的 session 能力,其实就是中间的一套组件,直接将处理逻辑放在中间。那么,所有的数据存储放在一个状态数据存储里,例如可能会存储在缓存中,其实就把一个系统的有状态改为无状态,叫作无状态的处理。可以看到上图右边的无状态,对于整个系统的高可用的扩展,只需要中间这一层不断加服务器就可以。

有状态服务特点如下:

● 服务本身依赖或者需要存储状态化数据,如果出现故障只能通过其他备份的状态数据进行恢复。

● 一个请求只会被某个节点处理。

● 如果单节点出现故障需要进行状态数据迁移。

● 由于存在多节点的数据存储,所以需要考虑数据一致性的问题。

无状态服务特点如下:

● 服务不依赖自身状态,状态数据存储在公共化存储服务中。

● 任何一个请求都可以被任何一个节点处理。

● 服务之间实现高可用以及水平扩展无须额外操作。

有状态和无状态的转换有两种方式:

(1) 状态数据的相互同步,最开始能想到的就是状态数据由每台服务器来存储,1 到 N 台服务器中都有这个数据,每台服务器中的数据可能都完全不同,在服务器内存中的状态数据如何实现同步是一个比较难处理的问题。

(2) 统一存储,使用分布式的数据存储,把中间的状态数据全部存储在中间服务中,基于中间的服务直接访问到下面的存储服务层。显然,从上图的方案来看,第二种更为高效、简单、合理。

业务逻辑层实现服务的可用有两种方式 —— 服务降级和服务限流。服务降级是指当服务负载过高或者出现一些故障的时候,将一些非核心业务或者故障业务移除或者暂不处理的措施,为其他业务留出处理的能力。服务降级如果是 Java 体系,最常见的开源组件是 Hystrix。

服务降级有几种处理方式:

(1) 基于线程隔离的处理,例如登录验证密码和下单存储的功能。服务降级可以把这些模块细化,基于线程隔离的方式能够避免请求出现的流量过大导致线程过高的问题,从而保障业务的有效处理。

(2) 熔断,它能够计算系统的健康度。例如在过往的 1 分钟或几分钟,计算请求的失败数和所有请求的数量,形成对系统健康度的判断。健康度判断通过设置一个阈值(例如大于 10%、大于 50%等)来触发熔断机制。假设调用第三方的某一个服务,若超时率过高,熔断可以避免系统在下游出现问题后不断向上以滚雪球的方式影响上游业务,造成所有请求都无法处理。服务降级经常会使用这种方式来做数据库中数据层的降级,这是一种比较简单的方式。例如,在数据库的 DAO 层编写接口时,用降级和非降级两种方式实现。在 Service 层调用 DAO 时,使用配置中心手动触发数据库降级,从而实现数据库动态降级。

Spring 提供了一个比较好用的 Hot Swap 方式,它是一个代理。它可以把 hot swap target source 实现两个 Bean——数据库访问类和降级实现类。触发降级时,HostSwap可以自动获取一个降级的Bean。Hot Swap对上游的业务是透明的,它调用 DAO 层,至于 DAO 层里面的实现是降级层还是数据库的读写层是透明的,内部自动实现,这也是一种做法。

第三个网络层,我们以机房来举个例子。

5

上图中,机房 1 和机房 2 通过专线的方式来进行数据通信。专线容易出现可靠性问题,应该怎样解决呢?第一种方式是不通过专线进行通信,代价是功能上的一些损失。第二种方式是通过配置中心下发指令来启动降级,比如停止流量和数据之间的同步。如果网络质量达不到期望,但并未完全切断,那么可以通过令牌桶或者流量桶等方式对网络拥塞进行控制。

服务限流是服务降级的一种轻度表现形式。服务降级中最后说到的令牌桶是一种典型的服务限流方式。服务限流的目的与服务降级相同,是让尽量多的流量能够流入到后端可以处理的能力范围中。

服务限流一般有几种做法:计数器、漏桶模式、令牌桶模式。

● 计数器的实现非常简单,例如在多久的时间范围之内,在单位时间内做多少。

● 漏桶模式定义了一个漏桶,其容量是一定的,然后不断地往里面加可取的令牌。它的流量是一个固定的流速,也就是说它可以进行均衡的、精确的处理,例如可以使用漏桶模式,将每秒的流数控制为 10 QPS 或者 100 QPS。Nginx 中的 limit 模块组件基本都是用漏桶模式进行精准限流。

● 令牌桶模式区别于漏桶模式之处就是允许突发流量。例如,在“抢购”这种突发流量比较大的情况下,我们也要保护后台一些服务,此时可以使用令牌桶模式实现服务限流。

服务限流中还有一种做法 —— 流量整形,其原理如下图所示,左侧是业务服务器,右侧是日志服务器,业务服务器不断地产生很多日志。在流量和数据量很大的情况下,中间通过消息队列来实现削峰填谷,不断地传输数据,使后端形成比较良好的流量波形,方便后端的接纳和处理。

6

数据逻辑层

首先介绍双机架构的几种模式:

● 主备模式,一台是主数据库,另一台是备数据库。“备”代表备⽤,正常情况下⼀般不使用,只有在数据库出现故障时才启⽤备数据库。由于当主数据库出现问题时需要手动切换,因此对于线上处理的性能提升或者资源的使用,这种模式是有一些浪费的。主备模式的最大优势是简单,只需要接入一台主数据库。在 OA 系统等可用性或流量不是很大的业务场景中,主备模式可以简单地容纳手动处理的时延。

● 主从模式从主备模式衍生而来,将正常场景下的备用数据库提升为可用的从数据库。当主数据库和从数据库出现问题时,主从切换有几种做法,例如通过第三方探测主数据库是否处于正常连接状态,从数据库是否出现问题,然后进行切换。

● 双主模式。如下图所示,双主模式的实现较为复杂,两台数据库要双向进行数据同步。

7

双主数据由于需要双向同步,会面临业务数据一致性如何做、数据的循环复制等一系列问题。另外,主从模式会存在怎样探测其服务器的处理问题。处理不当很有可能会出现脑裂的情况,两台服务器相互探测不到对方,只关注自己是否可以处理。我们认为比起用服务器自己的方式去探测(例如心跳探测),用第三方独立的服务探测其实会更合理,通过第三方的独立服务进行探测,从而解决所谓的脑裂问题。

多副本也是一种提高系统可用性的重要模式。副本是分布式系统容错、提高可用性的基本手段。一般数据副本的基本策略是以机器为单位,每个副本存储在不同的机器上,副本间的数据保持完全一致。在分布式系统中,副本一般采用切片的方式来做,如下图所示。

8

上图展示了切片后的数据。例如,原来分片 0 ~ 分片 2 可以分在不同的节点中,每个节点存储和处理的数据量较小。1000 QPS 分到 3 个节点,每个节点便可以只处理 300 多个分片。多副本模式还可以快速恢复数据。假设分片 0 出现了一些问题,甚至节点 0 和节点 1 都出现了问题,恢复数据时不需要把全部数据恢复。

在分布式系统中,分片多副本模式是解决数据之间可用性的一个较好的方案。

多机房

多机房一般有几种做法。

(1) 跨机房写。通过 DNS 解析将不同区域的⽤户分发到指定机房,流量上没有主从区别,但是从读写角度来看,机房已分为主从机房。假设有机房 1、机房 2 两个机房,机房 1 跨机房写会把机房分成主从模式,机房 1 接收读、写,机房 2 只负责读,它的写需要直接请求到主机房的数据库来进行。

这种做法适合读多写少的场景,例如用户中心、资讯内容服务等。跨机房写的优势是实现比较简单,在数据库层实现读写分离,业务只需要配置读写数据库链接就可实现多机房部署。

跨机房写的缺点是时延高,并且依赖机房之间专线的质量可靠性。

9

(2) 消息队列写。和跨机房写的架构很相似,只是在从机房写入的时候不是跨机房直接写入,而是先写入消息队列,再依靠消息队列将数据传输到主机房,写入都在本地完成。这一做法避免了跨机房的网络抖动带来的写入故障问题,同时也提升了系统在从机房的写入性能。主机房仍然是在逻辑层对消息队列的数据进行接收,再写入主机房的主数据库。由于写入采取了消息队列实现,所以这种模式可以适用于更广泛的场景,例如读多写少、读写均衡,以及对于数据及时性不敏感的写多读少业务,如本地生活、数据备份存储等。

以本地生活为例,假设一个用户一直在广东本地生活,数据不会出现频繁切换的情况,因此允许使用消息队列来进行传输,延迟一天关系也不大,数据可以做到高可用。

再以华南和华北两个不同地区的机房为例,华南机房可以先接纳华南地区用户的一些数据,华北机房的数据除了存储在华北机房,还需要备份到华南机房,但华北机房使用的数据仍然来自自身。这种同步降低了业务在实时链路中跨机房时延。

10

(3) 消息队列双向写。这种模式已经有双活的形态了,每个机房都可以独立地进行读写,本地通过数据库主从进行同步,本地有新增修改数据时都通过消息队列进行同步修改。双向写的模式主要适合本地写比较多的业务场景。消息队列双向写与用户切分紧密结合能够取得比较好的效果。

11

(4) 用户切分。正常访问场景下按照用户切分进行本地访问,并且引入数据库双向同步组件来进行数据同步,当其中⼀个机房出现故障的时候,直接将流量切换到另⼀个机房,并设置服务流量切换标志,接入层发现这个标志则不强制用户到指定机房,访问本地机房的数据库。由于在正常场景下用户的数据已实现同步,所以切换到另一个机房后也可以访问该用户的数据,不过数据同步的效率会使数据不一致问题短暂出现。这种架构模式比较适合数据备份、用户授权以及本地生活服务等场景。

12

以上是多机房中 4 个常见的读写方案。接下来我们通过一个账号授权系统案例来帮助大家进一步理解。


03 系统实现案例分析:账号授权系统 / 常见问题


用微信/支付宝登录服务就是一个授权的过程,用户提交登录请求后,进行账密验证。系统也需要向用户申请授权,例如核心应用可以获取用户所有信息字段,一些生态应用可以申请获取用户昵称、头像等信息。授权过程中通过 token 加解密来认证和互通权限。这个过程包括用户的 token 生成、token 加密以及 token 同步。

13

基于多机房的场景,假设 token 同步过程出现了问题,应该如何做呢?从原则上来说,设计方案中需要有 token 同步功能,接下来就是缓存和数据库,缓存 token 加解密等信息,数据库对配置进行存储,这些共同构成了授权服务的框架。

授权服务相对来说是一个比较简单的服务。但多机房怎么实现呢?回到上述的多机房实现方案,一个比较好的做法就是将用户切分与消息同步结合。

整体方案是用异步的方式来进行数据同步,然后进行数据的主动拉取和自降级校验。一般情况下,对用户进行切分,机房 1 和机房 2 分别处理相应的用户请求,生成 token 进行校验。假设此时机房 1 出现了一些问题,就需要把流量切换到机房 2,此时在机房 1 中 token 也会解析写入,同步到机房 2。这个过程中存在时间差,可能在机房 1 出现问题后,流量切换到机房 2 时 token 还没有解析写入,此时应该怎么办呢?可以参考以下做法:

● 机房接⼊层出现故障:切换流量到另外一个机房,如果本地能够获取 token 则直接验证,如果没有则采取对内接⼊层提供的内部接口实时获取(机房间专线);

● 如果机房专线也出现问题:则采取降级自校验的模式,例如授权用户获取的 key 通过加密封装⼀些用户基本信息(uid/time/机房 tag/业务属性等),解密后自校验;

● Token 的多机房同步:除了业务直接写入消息队列方案,也可监听缓存的写⼊事件,并且解析日志再同步到对应机房。这里存在两个问题:(1) 多机房循环复制问题(可通过机房标识解决);(2) 监听消息性能有限,容易出现较大同步时延。


04 总结


这次分享我们主要从高可用的一些实际事件引入,介绍了高可用的一些理论基础,接着对系统高可用分层实现方案做了详细的说明和介绍,最后对一个账号授权系统做了高可用架构设计的分析以及注意的问题。


答疑环节


Q:高可用和高并发如何学习?

A:高可用和高并发是一门实践型的知识。在技术的开发或设计过程中,我觉得应该从两个点来看。

第一,它确实是一个实践型的知识,但是不能纯粹等待实践。

第二,它也是一个沉淀型的知识。我们不一定必须要等到系统它高并发大流量时才去考虑这些问题。因为大家的职业经验或者工作属性不同,你不一定能面临这些问题,可能在双 11 的时候,例如天猫的流量体系特别大,它就必须面临这些问题。但是在平时做业务的过程当中,我们可以留意这些方面,比如考虑系统性能的设计,使用什么样的缓存,在这个数据库设计中使用 memcache 还是 Redis,分库还是分表,等等。假设数据系统要升级,从以前的这个大宽表的一个数据库,然后你可能会要往这个高性能去思考,最起码 sql 要写得响应更快一些,或者说是执行更快一些。那你可能就会思考怎样去分数据库,是直接垂直分还是横向分,垂直分依据是什么。在所有的系统中,第一要留意自己做这些系统设计的选型组件的原因,多问自己为什么需要这样去做,它的原理是什么。

第三,在高可用中也是类似的。设计系统的高可用时,可能是双机或者数据多备份,是一个双机房的。考虑的过程就是设计过程,可以把这些思考不断地纳入。不管系统和模块有多小,我们都应该考虑,因为做模块设计的时候也可以将服务降级,埋点,同时也可以考虑做限流。汇集小流,形成一个大河,不断地积累经验,慢慢形成一个体系化思维方式。总结起来就是,第一需要时间,第二需要在平时的业务开发过程中不断思考和积累。

Q:当遇到一个难题时,是业务人员妥协,还是技术人员花更多的精力去攻克难题?

A:这个问题是大家都会遇到的。我们可以秉承一个原则——业务的妥协与不妥协。不仅仅是说跟产品经理聊下来,产品经理说可以妥协,其他人员也可以去思考是否需要妥协。

我们思考业务的诉求是什么,从用户体验的视角出发。例如刚才我们所说的假设,业务的体验依据用户的体量。如果一个用户的体量比较小,它对于响应时延 RT 或者说整个系统资源的冲击可能就比较小。在这种情况下,不一定非得把精力不断地投入在这个技术中,因为技术要服务于业务,如果业务本身有诉求,我们可以朝着这个方向进行优化。

还有一个原则是判断用户体验。当我们作为用户体验时,谈体验不应该仅仅是产品经理来定。我在工作过程中经常倡导这一点,就是如何思考业务,如何链接业务的价值和业务的体验。这点不仅仅是产品经理的事情,技术人员也需要思考,例如我们体验用户请求时候,如果查询一个列表,请求的响应已经到了 1 分钟或者 2 分钟,这已经完全是不可容忍的状态了。我们完全可以基于这一点进行优化。如果是 1 秒、10 毫秒或者 20 毫秒这样级别的优化,在用户体量不大的情况下,可以暂时将优先级放低。这个问题我们可以这样把握,依据用户体量和对业务的体验判断是否应该进行优化。

推荐阅读
相关专栏
开发者实践
186 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。