高并发系统设计:系统的“三高”目标

高并发系统设计的三大目标:高性能、高可用、可扩展

  • 高并发,是指运用设计手段让系统能够处理更多的用户并发请求,也就是承担更大的流量。
    • 它是一切架构设计的背景和前提,脱离了它去谈性能和可用性是没有意义的。
    • 因为在每秒一次请求和每秒一万次请求,两种不同的场景下,分别做到毫秒级响应时间和五个九(99.999%)的可用性,无论是设计难度还是方案的复杂度,都不是一个级别的。
    • 当同时访问系统的用户不断的增加时,要消耗的计算资源也不断增加,这意味着系统需要更多的CPU和内存去处理用户的计算请求,需要更多的网络带宽去传输用户的数据,需要更多的硬盘空间去存储用户的数据。而当消耗的资源超过了服务器资源极限的时候,服务器就会崩溃,整个系统将无法正常使用
  • 性能和可用性,是我们实现高并发系统设计必须考虑的因素
    • 性能反映了系统的使用体验。同样承担每秒一万次请求的两个系统,一个响应时间是毫秒级,一个响应时间在秒级别,它们带给用户的体验肯定是不同的。
    • 可用性则表示系统可以正常服务的时间。还是两个承担每秒一万次的系统,一个可以做到全年不停机、无故障,一个隔三差五宕机维护,如果你是用户,你会选择使用哪一个系统呢?答案不言而喻。
  • 可扩展性它同样是高并发系统设计需要考虑的因素。为什么呢?举个例子。
    • 流量分为平时流量和峰值流量两种,峰值流量可能会是平时流量的几倍甚至几十倍,在应对峰值流量的时候,我们通常需要在架构和方案上做更多准备。而易于扩展的系统能够在短时间内快速完成扩容,更加平稳的承担峰值流量

如何达到高性能?

要想提升系统性能,我们就要先找出性能问题在哪里。

要想找出性能问题在哪里,我们就要先知道用什么东西来衡量一个系统的性能

性能的度量指标(性能指标)

通常我们都从两个层面定义性能场景的需求指标:

  • 业务指标
  • 技术指标

这两个层面需要有映射关系,技术指标不能脱离业务指标。一旦脱离,你会发现你能回答“一个系统在多少响应时间之下能支持TPS”这样的问题,但是回答不了“业务状态是什么”的问题

举例来说,如果一个系统要支持1000万人在线,可能你能测试出来的结果是系统能支持1万TPS,可是如果问你,1000万人在线会不会有问题?这估计就很难回答了

可以根据如下示意图理解业务指标和性能指标之间的关系:

在这里插入图片描述
这个示意显然不够详细,但也能说明关系了。所有的技术指标都是在有业务场景的前提下制定的,而技术指标和业务指标之间也要有详细的换算过程

系统设计与实现

业务指标

在设计系统之初,我们要先搞清楚系统的需求。

需求分为:

  • 功能需求
  • 非功能需求

功能需求是指需要实现些什么功能:比如用户注册、搜索视频等

非功能需求一般指的是:

  • 预计用户量
  • 日活用户量

我们通过非功能需求来找出我们需要满足的系统需要满足些什么技术指标

技术指标

系统性能分析

系统的性能我们主要关注三个地方

响应时间

也叫做访问延时,是指客户发出请求到被成功服务的时间,常用接口的响应时间来表示

一般来说,度量性能的指标是系统接口的响应时间,响应时间越短,性能越好,一般一个接口的响应时间是在毫秒级。

在系统中,我们可以把响应时间自下而上细分为以下几种:

  • 数据库响应时间:数据库操作所消耗的时间,往往是整个请求链中最耗时的
  • 服务端响应时间:服务端包括 Nginx 分发的请求所消耗的时间以及服务端程序执行所消耗的时间。
  • 网络响应时间:这是网络传输时,网络硬件需要对传输的请求进行解析等操作所消耗的时间。
  • 客户端响应时间:对于普通的 Web、App 客户端来说,消耗时间是可以忽略不计的,但如果你的客户端嵌入了大量的逻辑处理,消耗的时间就有可能变长,从而成为系统的瓶颈。

单次的响应时间是没有意义的,你需要知道一段时间的性能情况是什么样的。所以,我们需要收集这段时间的响应时间数据,然后依据一些统计方法计算出特征值,这些特征值就能够代表这段时间的性能情况。我们常见的特征值有以下几类

(1)平均值

  • 顾名思义,平均值是把这段时间所有请求的响应时间数据相加,再除以总请求数。平均值可以在一定程度上反应这段时间的性能,但它敏感度比较差,如果这段时间有少量慢请求时,在平均值上并不能如实的反应。
  • 举个例子,假设我们在 30s 内有 10000 次请求,每次请求的响应时间都是 1ms,那么这段时间响应时间平均值也是 1ms。这时,当其中 100 次请求的响应时间变成了 100ms,那么整体的响应时间是 (100 * 100 + 9900 * 1) / 10000 = 1.99ms。你看,虽然从平均值上来看仅仅增加了不到 1ms,但是实际情况是有 1% 的请求(100/10000)的响应时间已经增加了 100 倍。所以,平均值对于度量性能来说只能作为一个参考

(2)最大值

  • 就是这段时间内所有请求响应时间最长的值,但它的问题又在于过于敏感了。
  • 还拿上面的例子来说,如果 10000 次请求中只有一次请求的响应时间达到 100ms,那么这段时间请求的响应耗时的最大值就是 100ms,性能损耗为原先的百分之一,这种说法明显是不准确的。

(3)分位值

  • 分位值有很多种,比如 90 分位、95 分位、75 分位。
  • 以 90 分位为例,我们把这段时间请求的响应时间从小到大排序,假如一共有 100 个请求,那么排在第 90 位的响应时间就是90 分位值。
  • 分位值除了偶发极慢请求对于数据的影响,能够很好的反应这段时间的性能情况,分位值越大,对于慢请求的影响就越敏感

在这里插入图片描述
分位值是最适合作为时间段内,响应时间统计值来使用的,在实际工作中也应用最多。除此之外,平均值也可以作为一个参考值来使用。

问题:响应时间究竟控制在多长时间比较合适呢?这个不能一概而论。

  • 从用户使用体验的角度来看,200ms 是第一个分界点:接口的响应时间在 200ms 之内,用户是感觉不到延迟的,就像是瞬时发生的一样。
  • 而 1s 是另外一个分界点:接口的响应时间在 1s 之内时,虽然用户可以感受到一些延迟,但却是可以接受的
  • 超过 1s 之后用户就会有明显等待的感觉,等待时间越长,用户的使用体验就越差。

所以,健康系统的 99 分位值的响应时间通常需要控制在 200ms 之内,而不超过 1s 的请求占比要在 99.99% 以上。

吞吐量

在测试中,我们往往会比较注重系统接口的 TPS(每秒事务处理量),因为 TPS 体现了接口的性能,TPS 越大,性能越好。

  • 吞吐量,是指在一次性能测试过程中网络上传输的数据量的总和。对于交互式应用来说,吞吐量指标反映的是服务器承受的压力(它可以表示系统最多能够同时在线用户数
    • 在容量规划的测试中,吞吐量是一个重点关注的指标,因为它能够说明系统级别的负载能力
    • 在性能调优过程中,吞吐量指标也有重要的价值。如一个大型工厂,他们的生产效率与生产速度很快,一天生产 10W 吨的货物,结果工厂的运输能力不行,就两辆小型三轮车一天拉 2 吨的货物,比喻有些夸张,但我想说明的是这个运输能力是整个系统的瓶颈。
      • 80% 系统的性能瓶颈都是由吞吐量制约
      • 并发用户和吞吐量瓶颈之间存在一定的关联
      • 通过不断增加并发用户数和吞吐量观察系统的性能瓶颈。然后,从网络、数据库、应用服务器和代码本身 4 个环节确定系统的性能瓶颈。
    • 吞吐量与上面的服务延迟相辅相成:
      • 一个注重时间,也就是服务延迟;
      • 一个注重空间,也就是系统容量。
      • 一个系统的外部性能主要受到这两个条件的约束,缺一不可。比如,一个在线聊天服务系统,可以提供每秒钟一百万的吞吐率,但是客户的访问延迟是 5分钟以上,那么这个“一百万的吞吐率”没啥意义。反之,访问延迟很短,但是吞吐率很低,同样没有意义。
    • 吞吐量和响应时间的关系
      • 倒数关系,比如:响应时间 1s 时,吞吐量是每秒 1 次,响应时间缩短到 10ms,那么吞吐量就上升到每秒 100 次。
      • 所以,一般我们度量性能时都会同时兼顾吞吐量和响应时间,比如我们设立性能优化的目标时通常会这样表述:在每秒 1 万次的请求量下,响应时间 99 分位值在 10ms 以下。

补充:

  • 吞吐率是指单位时间(比如每秒)可以成功处理的请求数或者任务数量,单位是 “req/s”,表示系统的并发处理能力
    • 一般然而,我们更加关心的是服务器并发处理能力的上限,也就是单位时间内服务器能够处理的最大请求数,即最大吞吐率。
    • 所以我们普遍使用 “压力测试” 的方法,通过模拟足够多数目的并发用户,分别持续发送一定的 HTTP 请求,并统计测试持续的总时间,计算出基于这种 “压力” 下的吞吐率,即为一个平均计算值
      • 在 Web 服务器的实际工作中,其处理的 HTTP 请求通常包括对很多不同资源的请求,也就是请求不同的 URL, 比如这些请求有的是获取图片,有的是获取动态内容,显然服务器处理这些请求所花费的时间各不相同,而这些请求的不同时间组成比例又是不确定的。这就是实际情况下的吞吐率。
      • 所以,我们 对于同一个特定有代表性的请求进行压力测试,然后对多个请求的吞吐率按照比例计算加权平均值。

那应该怎么衡量系统的并发量呢?我们先来有并发相关的指标,主要有如下这些

目标用户数

目标用户数是所有可能访问我们系统的潜在用户的总和,比如微信的目标用户是所有中国人,那么微信的目标用户数就是 13 亿。目标用户数可以反映潜在的市场规模。

系统用户数

并不是所有的目标用户都会来访问我们的系统,只有那些真正访问过我们系统的用户才被称作系统用户。越是成功的系统,系统用户数和目标用户数越接近。

活跃用户数

同样地,访问过我们系统的用户可能只是偶尔过来访问一下,甚至只访问一次就永不再来。所以我们还需要关注用户的活跃度,也就是经常来访问的用户规模有多大。如果以一个月为单位,那么一个月内只要来访问过一次,就会被统计为活跃用户,这个数目被称为月活用户数。同样地,一天内访问过的总用户数被称为日活用户数。

在线用户数

当活跃用户登录我们的系统的时候,就成为在线用户了。在线用户数就是正在使用我们系统的用户总数

并发用户数

但在线用户也并不总是在点击 App,请求我们的系统服务,他可能搜索得到一个页面,然后就在自己的手机端浏览。只有发起请求,在服务器正在处理这个请求的用户才是并发用户。事实上,高并发架构主要关注的就是用户发起请求,服务器处理请求时需要消耗的计算资源。所以并发用户数是架构设计时主要关注的指标

关系

一般我们都是通过市场规模估计一个目标用户数,然后再根据产品特点,竞品数据,逐步估算其他的用户数指标

有了上面的这些用户数指标,我们就可以进一步挂算架构设计需要考虑的其他一些技术指标,比如每天需要新增的文件存储空间,存储总系统用户需要的数据库规模,总用户带宽,每秒处理的请求数等

资源使用率

一个系统和服务总是需要软硬件容量来支撑的,那么资源的使用率就很重要了。因为它直接决定了系统和服务的运营成本

这一指标虽然是面向系统容量的,但其实也和客户直接相关。如果资源使用率低,比如CPU使用率低,在系统容量固定的情况下,吞吐率也会比较低,或者访问延迟会比较高,因为资源没有被充分利用

指标之间是如何相互影响的

这三个指标的变化是会相互影响的。

  • 对一个系统而言,如果吞吐率很低,服务延迟往往会非常稳定。当吞吐率增高时,访问延迟一般会快速增加。
    在这里插入图片描述
  • 上图中,在吞吐率低于每秒 600 时,访问延迟小于 5 毫秒,这个延迟相对稳定。然后随着吞吐率变大,访问延迟飞速攀升
  • 一般而言,根据系统的延迟可接受大小,我们需要控制负载流量,以免访问延迟过大而影响客户体验
  • 我们在测量访问延迟的时候,不仅要计算均值,还需要注意延迟的分布情况,比如,有百分之几的在服务允许的范围,有百分之几的略微超出了,有百分之几的完全不可接受。多数情况下,平均延迟达标了,但是其中可能有很大比例(比如 20%)远远超出了我们可接受的范围。
  • 所以,我们在规定延迟标准的时候,除了均值,还需要定义百分位数的可接受值。比如,平均延迟 10 毫秒,P90 小于 30 毫秒,P99 小于 50 毫秒等等。

关于吞吐率,现实中的系统往往会有一个峰值极限

  • 超过这个峰值极限,系统就会超载,除了服务延迟超标,还会造成一系列的性能问题(比如系统挂掉)
  • 这个峰值极限往往需要经过仔细的性能测试,并且结合访问延迟标准来确定。
  • 有了这个峰值极限值后,系统的设计和运维就需要确保系统的负载不要超过这个值。

除了影响运营成本和系统容量,资源使用率的标准也需要考虑其他几个重要因素:

  • 一个因素是意外事件的缓冲(buffer)和灾难恢复(Disacter Recovery,DR)
  • 一个系统,随时可能会有意外发生(比如流量波动)或者部分网络故障,这就需要整个系统资源保留一定的缓存,来应付这些意外和从发生的灾难中恢复。
  • 比如CPU的使用率,虽然理论上可以达100%,但考虑这些因素,实际的使用率指标往往低于100%
指标之间的关系

性能优化的两个核心指标:

  • 吞吐高并发
  • 延迟(响应时间)响应快

这两个指标是从应用负载的视觉来考察性能,直接影响了产品终端的用户体验。 跟她们对应的是从系统资源的视觉出发的指标,比如:

  • 资源使用率
  • 饱和度
  • 。。。

随着应用负载的增加,系统资源的使用也会升高,甚至达到极限。

系统常见的资源有:

  • CPU
  • 内存
  • 网络

如何找出性能瓶颈

性能问题的本质,就是系统资源已经达到了瓶颈,但请求的处理却还不够块,无法支撑更多的情况

  • 所有的性能问题,虽然表现形式各异,归根结底是某种资源制约,不够用了。(性能问题归根结底是某个资源不够
  • 这里的资源指的是一个计算机系统,程序和互联网会用到的每一种资源,比如CPU、网络等。
  • 换句话说,客户的请求在处理时在某个地方“卡住了”。这个卡住的地方就叫“瓶颈”(或者叫卡点,Choke point)。

性能分析,其实就是找出应用或者系统的瓶颈,并设法去避免或者缓解它们,从而更高效的利用系统资源处理更多的请求。这包含了一系列的步骤:

  • 选择指标评估应用程序和系统的性能
  • 为应用程序和系统设置性能目标
  • 进行性能基准测试
  • 性能分析定位瓶颈
  • 优化监控和告警

下图展示了一个系统常见的十大瓶颈,基本上覆盖了所有可能出现性能问题的地方。
在这里插入图片描述
这十大瓶颈可以大致分为四类

  • 软件系统:包括操作系统、应用程序、各种类库、文件系统
  • CPU和内存:包括CPU性能、QPI(QuickPath Interconnect,处理器之间的快速通道互联)和缓存内存
  • 存储和外部IO:包括处理器的IO的接口性能、各种存储系统等
  • 网络:包括服务器到机柜交换机的网络、数据中心的网络、CDN 和互联网。

总体来说,性能分析的目的,就是提供高性能,低延时,高效率的服务。

要实现这一目的,就需要找到系统和服务的性能瓶颈,然后尽可能的消除瓶颈,或者降低瓶颈带来的影响。系统和服务有性能瓶颈就说明这个地方的资源不够用了。所谓的最大性能瓶颈,就是说这个地方的资源短缺程度最大,相对而言,其他地方的资源有富余。

如何找到最大的性能瓶颈。

这就需要进行性能测试和性能分析了。性能分析时需要知道三个层次的知识:

  • 第一个层次是可能的性能瓶颈。知道了瓶颈才有目标去分析
  • 第二个层次是每个瓶颈有哪些资源可能短缺。比如内存就有很多不同的资源,不仅仅是简单的内存大小。除了内存使用量,还有内存带宽和内存访问延迟
  • 第三个层次是对每个瓶颈的每种资源要了解它和其他模块是如何交互的,对整个系统性能是如何影响的,它的正常值和极限值是多少,如何分析测量等

找到性能最大瓶颈后,具体的优化方式就是什么资源不够就加什么资源,同时尽量降低资源消耗,这样就可以做到在资源总量一定的情况下,有能力支撑更高的吞吐率和实现更低的延迟。

依据数据和剖析来分析

做性能分析时,必须采用科学的方法,尽量依据数据,来引导我们的分析和验证我们的推论,而不是完全凭空猜测。

这一点可以说是性能分析和优化的第一原则。当我们还疑性能有问题的时候,应该通过合理的测试、日志分析,并做合理的剖析(profilling),来分析出哪里有问题,从而有的放矢,而不是凭感觉。

比如,如果是CPU相关的性能问题,按照80/20定律,系统绝大多数的时间应该都耗费在少量的代码片段里面。如果找到这些需要优化的代码呢?唯一可靠的方法就是profile。现代流行的各种编程语言,比如java,都有相关的profile工具。所以,会使用这些profile工具是性能分析和优化的必要条件。

高并发下如何进行性能优化

比如,现在有一个系统,这个系统中处理核心只有一个,执行的任务的响应时间都在10ms,它的吞吐量是每秒100次。那么我们如何来优化性能从而提升系统的并发能力呢?主要有两种思路:一种是提高系统的处理核心数,另一种是减少单次任务的响应时间。

提高系统的处理核心数

这个思路是优化性能最简单的途径。

  • 拿上一个例子来说,你可以把系统的处理核心数增加为两个,并且增加一个进程,让这两个进程跑在不同的核心上。这样从理论上,你系统的吞吐量可以增加一倍。
  • 当然了,在这种情况下,吞吐量和响应时间就不是倒数关系了,而是:吞吐量 = 并发进程数 / 响应时间。

那是不是无限制地增加处理核心数就能无限制地提升性能,从而提升系统处理高并发的能力呢?并不是。

  • 随着并发进出数的增加,并行的任务对于系统资源的争抢也会愈发严重。在某一个临界点上继续增加并发进程数,反而会造成系统性能下降,这就是性能测试中的拐点模型。
    在这里插入图片描述
  • 从上图中可以发现:
    • 并发用户数处于轻压力区时,响应时间平稳,吞吐量和并发用户数是线性相关。
    • 当并发用户数处于重压力区时,系统资源利用率到达极限,吞吐量开始有下降的趋势,相依时间也会略有上升。
    • 这个时候,再对系统增加压力,系统就进入“拐点区”,处于超负荷状态,吞吐量下降,响应时间大幅度上升
  • 所以我们在评估系统性能时通常需要做压力测试,目的就是找到系统的“拐点”,从而知道系统的承载压力,也便于找到系统的瓶颈,持续优化系统性能

减少单次任务的响应时间

想要减少任务的响应时间,首先要看你的系统是CPU密集型还是IO密集型的,因为不同类型的系统性能优化方式是不一样的。

  • CPU密集型系统中,需要处理大量的CPU运算,那么选用更高效的算法或者减少运算次数就是这类系统的优化手段
    • 比如,如果系统的主要任务是计算Hash值,那么这时选择更高性能的Hash算法就可以大大提升系统的性能
    • 发现这类问题的主要方式,是通过一些profile工具来找到消耗CPU时间最多的方法或者模块,比如 Linux 的 perf、eBPF
  • IO密集型,指的是系统的大部分操作都是在等待IO完成,这里IO指的是磁盘IO和网络IO
    • 大部分系统都是IO密集型,比如数据库系统、缓存系统、web系统。
    • 这类系统的性能瓶颈可能出现在系统的颞部,也可能是依赖的其他系统,而发现这类性能瓶颈的手段主要有两类
      • 第一类是采用工具。Linux 的工具集很丰富,完全可以满足你的优化需要,比如网络协议栈、网卡、磁盘、文件系统、内存,等等。这些工具的用法很多,你可以在排查问题的过程中逐渐积累。除此之外呢,一些开发语言还有针对语言特性的分析工具,比如说 Java 语言就有其专属的内存分析工具。
      • 第二中手段是可以通过监控来发现性能问题。在监听中我们可以对任务的每一个步骤做分时的统计,从而找到任务的哪一步消耗了更多的时间
    • 找到了系统的瓶颈点,我们要如何进行优化呢?优化方案会随着问题的不同而不同。比如
      • 如果是数据库访问慢,那么就要看是不是有锁表的情况、是不是有全表扫描、索引加得是否合适、是否有 JOIN 操作、需不需要加缓存,等等;
      • 如果是网络的问题,就要看网络的参数是否有优化的空间,抓包来看是否有大量的超时重传,网卡是否有大量丢包等。

性能分析的常用工具

在这里插入图片描述

性能优化从哪些方面做起

在这里插入图片描述

性能优化的原则

  • 首先,性能优化一定不能盲目,一定是问题导向的。脱离了问题,盲目地提早优化会增加系统的复杂度,浪费开发人员的时间,也因为某些优化可能会对业务上有些折中的考虑,所以也会损伤业务。
  • 其次,性能优化也遵循“八二原则”,即你可以用 20% 的精力解决 80% 的性能问题。所以我们在优化过程中一定要抓住主要矛盾,优先优化主要的性能瓶颈点。
  • 再次,性能优化也要有数据支撑。在优化过程中,你要时刻了解你的优化让响应时间减少了多少,提升了多少的吞吐量。
  • 最后,性能优化的过程是持续的。高并发的系统通常是业务逻辑相对复杂的系统,那么在这类系统中出现的性能问题通常也会有多方面的原因。因此,我们在做性能优化的时候要明确目标,比方说,支撑每秒 1 万次请求的吞吐量下响应时间在 10ms,那么我们就需要持续不断地寻找性能瓶颈,制定优化方案,直到达到目标为止。

小结

对于一个系统,可以这样去优化:

  • 数据优先,你做一个新的系统在上线之前一定要把性能监控系统做好;
  • 掌握一些性能优化工具和方法,这就需要在工作中不断的积累;
  • 计算机基础知识很重要,比如说网络知识、操作系统知识等等,掌握了基础知识才能让你在优化过程中抓住性能问题的关键,也能在性能优化过程中游刃有余。

系统怎样做到高可用?

高可用性设置的是系统具备较高的无故障运行的能力。

  • 通常来讲,一个高并发大流量的系统,系统出现故障比系统性能低更损伤用户的使用体验。
  • 想象一下,一个日活用户过百万的系统,一分钟的故障可能会影响到上千的用户。而且随着系统日活的增加,一分钟的故障时间影响到的用户数也随之增加,系统对于可用性的要求也会更高。

可用性的度量

可用性是一个抽象的概念,你需要知道要如何来度量它,与之相关的概念是:MTBF 和MTTR。

  • MTBF(Mean Time Between Failure)是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。
  • MTTR(Mean Time To Repair)表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。

可用性与 MTBF 和 MTTR 的值息息相关,我们可以用下面的公式表示它们之间的关系:
A v a i l a b i l i t y = M T B F / ( M T B F + M T T R ) Availability = MTBF / (MTBF + MTTR) Availability=MTBF/(MTBF+MTTR)

这个公式计算出的结果是一个比例,而这个比例代表着系统的可用性。一般来说,我们会使用几个九来描述系统的可用性。
在这里插入图片描述

  • 其实通过这张图你可以发现,一个九和两个九的可用性是很容易达到的,基本可以人工实现
  • 三个九之后,系统的年故障时间从 3 天锐减到 8 小时。
  • 到了四个九之后,年故障时间缩减到 1 小时之内。这个级别之下,可能就需要建立完善的运维值班体系、故障处理流程和业务变更流程。你可能还需要在系统设计上有更多的考虑。比如,在开发中你要考虑,如果发生故障,是否不用人工介入就能自动恢复。当然了,在工具建设方面,你也需要多加完善,以便快速排查故障原因,让系统快速恢复。
  • 到达五个九之后,故障就不能靠人力恢复了。想象一下,从故障发生到你接收报警,再到你打开电脑登录服务器处理问题,时间可能早就过了十分钟了。所以这个级别的可用性考察的是系统的容灾和自动恢复的能力,让机器来处理故障,才会让可用性指标提升一个档次。

一般来说,我们的核心业务系统的可用性,需要达到四个九,非核心系统的可用性最多容忍到三个九。

高可用系统设计的思路

一个成熟系统的可用性需要从系统设计和系统运维两方面来做保障,两者共用作用,缺一不可

系统设计

“Design for failure“是我们做高可用系统设计时的第一原则。在承担百万QPS的高并发系统中,集群中机器的数量成百上千,单机的故障是常态,几乎每一天都有发生故障的可能

  • 因此,我们在做系统设计时,要把发生故障作为一个重要的考虑点,预先考虑如何自动化的发现故障,发生故障之后要如何解决。
  • 另外,我们还需要掌握一些具体的优化方法。比如故障转移、超时控制、降级和限流
故障转移

一般来说,发生故障转移的节点可能有两种情况:

  • 在完全对等的节点之间做故障转移
    • 在这种系统中所有节点都承担读写流量,并且节点中不保存状态,每个节点都可以作为另一个节点的镜像
    • 这种情况下,如果访问某一个节点失败,那么简单的随机访问另一个节点就好了
  • 在不对等的节点之间,即系统中存在主节点也存在备节点
    • 这种方式比较复杂。比如我们有一个主节点,有多台备用节点,这些备用节点可以是是热备(同样在线提供服务的备用节点),也可以是冷备(只作为备份使用),那么我们需要在代码中控制如何检测主备机器是否故障,以及如何做主备切换
    • 使用最广泛的故障检测机制是“心跳”。你可以在客户端上定期的项主节点发送心跳包,也可以从备份节点上定期发送心跳包。当一段时间内未收到心跳包,就可以认为主节点已经发生故障,可以触发选主的操作
    • 选主的结果需要在多个备份节点上达成一致,所以会使用某一种分布式一致性算法
超时控制

除了故障转移以外,对于系统间调用超时的控制也是高可用系统设计的一个重要考虑方面。

  • 复杂的高并发系统通常会由很多的系统模块组成,同时也会依赖很多的组件和服务。比如缓存组件、队列服务等。它们之间的调用最怕的是延迟而不是失败,因为失败通常是瞬间的,可以通过重试的方法解决。而一旦调用某一个模块或者服务发生比较大的延迟,调用方就会阻塞在这次系统调用上,它已经占用的资源得不到释放。当存在大量这种阻塞请求时,调用方就会因为用尽资源而挂掉。

  • 在系统开发的初期,超时控制通常不被重视,或者是没有方式来确定正确的超时时间。

既然要做超时控制,那么我们怎么来确定超时时间呢?这是一个比较困难的问题。

  • 超时时间短了,会造成大量的超时错误,对用户体验产生影响;
  • 超时时间长了,又起不到作用

建议:

  • 通过收集系统之间的调用日志,统计比如说 99% 的响应时间是怎样的,然后依据这个时间来指定超时时间
  • 如果没有调用的日志,那么只能按照经验值来指定超时时间。

不过,无论使用哪种方式,超时时间都不是一成不变的,需要在后面的系统维护过程中不断地修改

超时控制实际上就是不让请求一直保持,而是在经过一定时间之后让请求失败,释放资源给接下来的请求使用。

  • 这对于用户来说是有损的,但是却是必要的,因为它牺牲了少量的请求却保证了整体系统的可用性。
  • 而我们还有另外两种有损的方案能保证系统的高可用,它们就是降级和限流。
降级

降级是为了保证核心服务的稳定而牺牲非核心服务的做法

  • 比方说我们发一条微博会先经过反垃圾服务检测,检测内容是否是广告,通过后才会完成诸如写数据库等逻辑。
  • 反垃圾的检测是一个相对比较重的操作,因为涉及到非常多的策略匹配,在日常流量下虽然会比较耗时却还能正常响应。但是当并发较高的情况下,它就有可能成为瓶颈,而且它也不是发布微博的主体流程,所以我们可以暂时关闭反垃圾服务检测,这样就可以保证主体的流程更加稳定
限流

限流:通过对并发的请求进行限速来保护系统

  • 比如对于web应用,限制单机只能处理每秒1000次的请求,超过的部分直接返回错误给客户端
  • 虽然这种做法损害了用户的使用体验,但是它是在极端并发下的无奈之举,是短暂的行为,因此是可以接受的。

系统运维

可以从灰度发布、故障演练两个方面来考虑如何提升系统的可用性。

灰度发布

在业务平稳运行的过程中,系统是很少发生故障的,90%的故障是发生在上线变更阶段的。比如说,你上了一个新的功能,由于设计方案的问题,数据库的慢请求数翻了一倍,导致系统请求被拖慢而产生故障。

如果没有变更,数据库怎么会无缘无故地产生那么多的慢请求呢?因此,为了提升系统的可用性,重视变更管理尤为重要。而除了提供必要回滚方案,以便在出现问题时快速回滚恢复之外,另一个主要的手段就是灰度发布

  • 灰度发布指的是系统的变更不是一次性的推到线上的,而是按照一定的比例逐步推进的
  • 一般情况下,灰度发布是以机器维度进行的。比方说,我们先在 10% 的机器上进行变更,同时观察 Dashboard 上的系统性能指标以及错误日志。如果运行了一段时间之后系统指标比较平稳并且没有出现大量的错误日志,那么再推动全量变更。
故障演练

灰度发布是在系统正常运行的情况下,保证系统高可用的运维手段,那么我们如何知道发生故障时系统的表现呢?这里就要依靠另外一个手段:故障演练。

  • 故障演练指的是对系统进行一些破坏性的手段,观察在出现局部故障时,整体的系统表现是怎样的,从而发现系统中存在的,潜在的可用性问题。
  • 一个复杂的高并发系统依赖了太多的组件,比方说磁盘,数据库,网卡等,这些组件随时随地都可能会发生故障,而一旦它们发生故障,会不会如蝴蝶效应一般造成整体服务不可用呢?我们并不知道,因此,故障演练尤为重要。

“Chaos Monkey”工具就是故障演练绝佳的工具。它通过在线上系统上随机地关闭线上节点来模拟故障,让工程师可以了解,在出现此类故障时会有什么样的影响。

小结

从上面可以看到从开发和运维角度上来看,提升可用性的方法是不同的:

  • 开发注重的是如何处理故障,关键词是冗余和取舍。冗余指的是有备用节点,集群来顶替出故障的服务,比如文中提到的故障转移,还有多活架构等等;取舍指的是丢卒保车,保障主体服务的安全。
  • 从运维角度来看则更偏保守,注重的是如何避免故障的发生,比如更关注变更管理以及如何做故障的演练。

两者结合起来才能组成一套完善的高可用体系。

还需要注意的是,提高系统的可用性有时候是以牺牲用户体验或者是牺牲系统性能为前提的,也需要大量人力来建设相应的系统,完善机制。所以我们要把握一个度,不该做过度的优化。就像在文中提到的,核心系统四个九的可用性已经可以满足需求,就没有必要一味地追求五个九甚至六个九的可用性。

另外,一般的系统或者组件都是追求极致的性能的,那么有没有不追求性能,只追求极致的可用性的呢?答案是有的。比如配置下发的系统,它只需要在其它系统启动时提供一份配置即可,所以秒级返回也可,十秒钟也 OK,无非就是增加了其它系统的启动速度而已。但是,它对可用性的要求是极高的,甚至会到六个九,原因是配置可以获取的慢,但是不能获取不到。

可见,可用性和性能有时候是需要做取舍的,但如何取舍就要视不同的系统而定,不能一概而论了。

如何让系统易于扩展

从架构设计在来说,高扩展性是一个设计的指标,它表示可以通过增加机器的方式来线性提高系统的处理能力,从而承担更高的流量和并发。

问题:“为什么不在架构之初就考虑好使用多少台机器,支持现有的并发呢”?

  • 原因是峰值流量不可控
  • 一般来说,基于成本考虑,在业务平稳期,我们会预留30%到50%的冗余以应对运营活动或者推广带来的峰值流量,但是当有一个突发事件发生时,流量可能会瞬间提升到2~3倍甚至更高。

那我们要如何应对突发的流量呢?架构的改造已经来不及了,最快的方法就是堆机器。不过我们需要保证,扩容了三倍的机器之后,相应的我们的系统也能支撑三倍的流量。有的人可能会产生疑问:“这不是显而易见的吗?很简单啊。”真的是这样吗?我们来看看做这件事儿难在哪儿。

为什么提升扩展性会很复杂

我们知道,在单机系统中可以通过增加处理核心的方式,来增加系统的并行处理能力,但是这个方式并不总是生效。因为当并行的任务数量较多时,系统会因为争抢资源而达到性能上的拐点,系统处理能力不升反降。

而对于由多台机器组成的集群系统也是如此。集群系统中,不同的系统分层上可能存在一些“瓶颈点”,这些瓶颈点制约着系统的横线扩展能力。举个例子

  • 比方说,你系统的流量是每秒 1000 次请求,对数据库的请求量也是每秒 1000 次。如果流量增加 10 倍,虽然系统可以通过扩容正常服务,数据库却成了瓶颈
  • 再比方说,单机网络带宽是 50Mbps,那么如果扩容到 30 台机器,前端负载均衡的带宽就超过了千兆带宽的限制,也就会成为瓶颈。

那么,我们的系统中哪些服务会称为制约系统扩展的重要因素呢?

  • 其实,无状态的服务和组件更易于扩展,而像MySQL这种存储服务是有状态的,就比较难以扩展。因为向存储集群中增加或者减少机器时,会涉及到大量的数据迁移,而一般传统的关系型数据库不支持。这就是为什么提升系统扩展性会很复杂的主要原因
  • 我们需要站在整体整体架构的角度,而不仅仅是业务服务器角度来考虑系统的扩展性 。所以说,数据库、缓存、依赖的第三方、负载均衡、交换机
    宽等等都是系统扩展时需要考虑的因素。我们要知道系统并发到了某一个量级之后,哪一个因素会成为我们的瓶颈点,从而针对性地进行扩展

高可扩展性的设计思路

拆分是提升系统扩展性最重要的一个思路,它会把庞杂的系统拆分成独立的,有单一职责的模块。相对于大系统来说,考虑一个一个小模块的扩展性当然会简单一些。将复杂的问题简单化,这就是我们的思路。

但对于不同类型的模块,我们在拆分上遵循的原则是不一样的。我给你举一个简单的例子,假如你要设计一个社区,那么社区会有几个模块呢?可能有 5 个模块。

  • 用户:负责维护社区用户信息,注册,登陆等;
  • 关系:用户之间关注、好友、拉黑等关系的维护;
  • 内容:社区发的内容,就像朋友圈或者微博的内容;
  • 评论、赞:用户可能会有的两种常规互动操作;
  • 搜索:用户的搜索,内容的搜索。

而部署方式遵照最简单的三层部署架构,负载均衡负责请求的分发,应用服务器负责业务逻辑的处理,数据库负责数据的存储落地。这时,所有模块的业务代码都混合在一起了,数据也都存储在一个库里。
在这里插入图片描述

存储层的扩展性

无论是存储的数据量,还是并发访问量,不同的业务模块之间的量级相差很大,比如说成熟社区中,关系的数据量是远远大于用户数据量的,但是用户数据量的访问量却远比关系数据要打。所以如果存储储目前的瓶颈点是容量,那么我们只需要针对关系模块的数据做拆分就好了,而不需要拆分用户模块的数据。所以存储拆分首先考虑的维度是业务维度

拆分之后,这个简单的社区系统就有了用户库、内容库、评论库、点赞库和关系库。这么做还能隔离故障,某一个库“挂了”不会影响到其它的数据库。
在这里插入图片描述
按照业务数据拆分,在一定程度上提升了系统的扩展性,但是系统运行时间长了之后,单一的业务数据库在容量和并发请求量上仍然会超过单机的限制。这时,我们就需要针对数据库做二次拆分

这次拆分是按照数据特性做水平的拆分,比如说我们可以给用户库增加两个节点,然后按照某些算法将用户的数据拆分到这三个库里面。

水平拆分之后,我们就可以让数据库突破单机的限制了。但是这里要注意,我们不能随意的增加节点,因为一旦增加节点就需要手动的迁移数据,成本还是很高的。所以基于长远的考虑,我们最好一次性增加足够的节点以避免频繁的扩容。

当数据库按照业务和数据维度拆分之后,我们尽量不要使用事务

  • 因为当一个事务中同时更新不同的数据库时,需要使用二阶段来提交,来协调所有数据库要么全部更新成功,要么全部更新失败
  • 这些协调的成本会随着资源的扩展不断升高,最终达到无法承受的程度

业务层的扩展性

我们一般会从三个维度考虑业务层的拆分方案,它们分别是:业务纬度,重要性纬度和请求来源纬度。

  • 首先,我们要把单独的业务拆分成单独的业务池。比方说上面的社区系统中,我们可以按照业务的维度拆分成用户池、内容池、关系池、评论池、点赞池和搜索池。
  • 每个业务依赖独自的数据库资源,不会依赖其他业务的数据库资源。这样当某一个业务的接口成为瓶颈时,我们只需要扩展业务的池子,以及确认上下游的依赖方就可以了,这样就大大减少了扩容的复杂度。

在这里插入图片描述
除此之外,我们还可以根据业务接口的重要程度,把业务分为核心池和非核心池。

  • 打个比方,就关系池而言,关注、取消关注接口相对重要一些,可以放在核心池里面;
  • 拉黑和取消拉黑的操作就相对不那么重要,可以放在非核心池里面。

这样,我们可以优先保证核心池的性能,当整体流量上升时优先扩容核心池,降级部分非核心池的接口,从而保证整体系统的稳定性
在这里插入图片描述

最后,你还可以根据接入客户端类型的不同做业务池的拆分。比如说,服务于客户端接口的业务可以定义为外网池,服务于小程序或者 HTML5 页面的业务可以定义为 H5 池,服务于内部其它部门的业务可以定义为内网池,等等。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值