【译文】原文地址
我们生存在一个充满大规模面向互联网系统的时代,其中谷歌、亚马逊、facebook等就是标杆。这些公司每秒处理大量请求,管理规模空前的数据存储。由于商业机密的原因,获得这些系统精确的流量数据并不容易。尽管如此,随着用户增长这些网站快速扩张的能力是它们持续成功背后的重要保证。
幸运的是,通过一家科技公司的年度使用报告,我们可以深入了解互联网处理需求和数据量的规模。你可以点击这里浏览自2019年以来它们令人难以置信的详细使用情况统计数据。这是了解大规模系统令人惊人的一幕。
对于绝大多数业务和政府系统,可扩展在开发和部署的早期阶段并不是主要的质量要求。增强可用性和实用性是我们开发周期的驱动力。只要在正常负载下性能足够,我们就会不断添加面向用户的特性来提高系统的业务价值。
尽管如此,系统发展到一定状态,性能和可伸缩性变得紧急,甚至成为生存问题,这种情况并不少见。 吸引人的特性和高实用性造就了成功,这也导致更多请求需要处理和更多数据需要管理。这经常会有一个拐点,原先的系统设计在低负载时是可行的,现在却成了技术债。外部触发事件往往会导致这些引爆点——看看2020年3月的媒体,就会看到许多关于政府失业和超市在线订购网站因冠状病毒大流行造成的大量请求而崩溃的报道。
当系统达到瓶颈点,架构师的责任是引导系统向快速响应、可扩展的系统发展。核心系统架构和模式将需要重新设计,以便能够处理不断增加的请求量。对很多架构师来说,这些并不是他们熟悉的领域,因为可扩展有时会使我们偏离广泛理解的软件架构原则。
以下6个原则是每个架构师都应该拿来帮助构建可扩展系统。这些通用原则可以作为架构师可靠地处理不断增长的请求负载和数据量的指导。
1、成本和可扩展性的关联
一个可扩展系统的核心原则就是可以通过简单地添加资源来应对增加的负载。对很多系统来说,简单而有效的方法就是部署多个无状态服务并使用负载均衡器来分发请求到多个实例(如下图所示)。
假设这些资源是分布在AWS云平台上,你的成本包括:
- 部署服务的虚拟机
- 负载均衡器,由请求和数据量决定
在这种模式下,随着系统请求量增加,你将需要更多的处理能力。这会导致更高的成本。你的负载均衡器成本也会和请求量和数据量成比例增加。因此成本和扩展是相伴的。你的扩展设计不可避免地会影响部署成本。 忘记这一点,你可能会发现你自己和许多著名的公司一样,在月底将收到大量出乎意料的部署账单!
在这种情况下,您的设计如何降低成本?主要有两种方式:
1、使用弹性负载均衡器,根据负载情况调整服务实例数量 。在低流量时会支付最少数量的资源。随着请求量增加,负载均衡器扩展新的服务实例相应的处理能力增加。
2、增加单个服务实例的处理能力。这通常是调整服务部署参数(例如线程数、连接数、堆大小等)。默认的设置很少能满足负载要求。准确的选择参数设置可以发现会有很大的性能提高以及容量的增加。你基本上是在用同样的资源做更多的工作——这是实现规模化的关键原则。
2、你的系统在某些地方存在瓶颈
扩展系统本质上意味着增加它的容量。在上面的例子中,我们通过在负载均衡的计算资源上部署更多的虚拟机来提高请求处理能力。然而,软件系统由多个相关的处理组件或微服务组成。不可避免的是,当某些微服务的容量增加时,会导致其他微服务的不堪重负。
在我们的负载均衡示例中,假设我们的服务实例都与共享数据库连接。随着部署的服务器数量增加,我们增加了数据库请求压力(看下图)。在某些场景下,数据库会达到饱和,会导致更大的延时。数据库会成为瓶颈-如果增加更多服务来处理当然没关系。
为了进一步扩展,解决方案是以某种方式增加数据库容量。您可以尝试优化查询,或添加更多cpu和或内存。可以复制和或切分您的数据库。有很多扩展方法。
系统中的任何共享资源都可能成为限制容量的瓶颈。当您在架构的某些部分中增加容量时,您将需要仔细查看下游容量,以确保不会因为激增的请求量导致系统奔溃。这可能会迅速导致级联故障(参见下一条规则),并导致整个系统崩溃。
数据库、消息队列,延时长的网络连接、线程和连接池和共享微服务都是主要的潜在瓶颈。可以很确信大流量情况下很快会暴露这些问题。关键是在瓶颈暴露时防止系统突然崩溃,并能够快速部署更多容量。
3、缓慢的服务比失败的服务更糟糕
在正常的操作下,系统应该能提供微服务和数据库间连接稳定、延时低。随着客户端增加,服务间请求延时增加。起初,这通常是一个缓慢但稳定的增长,可能不会严重影响整个系统操作,特别是在负载激增短暂的情况下。但是,如果请求负载继续超过容量(服务B),未完成的请求将开始堆积到请求微服务(服务A)中,由于下游处理缓慢,该微服务现在面临的接收到请求比已完成的请求更多。
在这种情况下,事情会很快恶化。当一个服务被击垮并由于抖动或资源耗尽而基本上停止时,请求服务将对客户端失去响应,客户端也将停止。这被称为级联失败-一个短板微服务造成请求处理链路超时导致系统失败。
这就是为什么缓慢微服务比不可用服务更糟糕的原因。如果您调用了一个失败的服务或一个由于暂时的网络问题而被分区的服务,您将立即收到一个异常,并可以明确决定做什么(例如后退和重试,报告错误)。缓慢超负荷的服务行为正常,只是延时更长。这暴露了所有相关服务潜在瓶颈,最终会出现可怕的问题。
像断路器和隔离这样的架构模式是针对级联故障的防护措施。如果服务的延迟超过了指定的值,断路器可以对请求负载进行截流。如果只有一个下游依赖项失败,隔离可以保护一个完整的微服务免于失败。使用这些技术来构建弹性和可扩展的架构。
4、数据层是最难扩展的
数据库实际上是每个系统的核心。其包含业务正确运行所需的核心数据。 核心业务数据包含客户信息、事务和帐户余额。这些数据必须正确、一致和高可用。
操作型数据的例子包括用户会话长度、每小时的访问者和页面访问量。这些数据通常有一个保质期,可以随着时间的推移进行聚合和总结。如果不是100%正确,也不会世界末日。因此,我们可以更容易地捕获和存储操作数据,例如将其写入日志文件或消息队列。然后消费者定期检索数据并将其写入数据存储。
随着系统请求处理层的扩展,共享事务数据库将承受更多的负载。随着查询负载的增长,这些会迅速成为瓶颈。首先可以进行查询优化,然后添加更多内存使数据库引擎能够缓存索引和表数据也是有用的措施。但最终,您的数据库引擎将会达到极限,需要进行更彻底的改造。
首先要注意的是,任何数据模式的更改都可能在数据层中带来麻烦。如果更改关系数据库中的模式,可能需要运行脚本重新加载数据以匹配新模式。在脚本运行期间(对于大型数据库来说可能是一段很长的时间),您的系统不可用于写操作。这可能导致客户不满。
NoSQL、无模式数据库减少了重新加载数据库的需要,但您仍然必须更改查询代码,以识别修改后的数据模式。如果您有业务数据集合,其中一些已使用修改后的模式,而另一些项保持原始格式,那么您可能还需要管理数据对象版本控制。
如果要进一步扩展,可能需要分布式数据库。也许带只读副本的主从模式就足够了。对于大多数数据库,这很容易实现,但是需要持续地监控。当主节点故障时,故障转移很少是实时的,有时需要手动干预。这些问题都依赖于数据库引擎。
如果你采用去中心化的方法,你必须决定如何最好地在多个节点上分布和划分你的数据。在大多数数据库中,一旦您选择了分区键,如果要更改就得重建数据库。不用说,分区键需要明智地选择!分区键还控制数据如何在节点之间分布。随着添加节点以提供更大的容量,需要进行重新平衡,以便在新节点之间保存数据和接收请求。当然,数据库文档描述了这些技术的工作原理。
由于分布式数据库的潜在管理问题,基于云的替代方案(例如AWS Dynamodb,谷歌Firestore)通常是首选。当然,这是有代价的!
5、缓存、更多缓存
减少数据库负载的一种方法是尽可能避免访问它。这就是缓存发挥作用的地方。数据库引擎应该充分利用节点缓存。这是一种简单而有用的解决方案,虽然可能很昂贵。
对于频繁读取和很少更改的数据,可以修改处理逻辑,以便首先检查分布式缓存,比如memcached服务器。即使需要远程调用,但如果数据在缓存中,加上快速的网络,这比查询数据库实例要高效。
引入缓存的话,需修改业务处理逻辑。如果要查询的数据不在缓存中,代码必须先查询数据库然后将结果加载进缓存,同时返回给客户端。 您还需要决定何时删除或使缓存结果失效——这取决于您的应用程序向客户端提供过时数据的容忍程度。
在扩展系统时,设计良好的缓存方案绝对是无价的。 如果可以从缓存中处理很大比例的读请求,那么可以在数据库中购买更多的容量,因为它们不涉及处理大多数请求。这意味着您可以避免复杂而痛苦的数据层修改(见前面的规则),同时为越来越多的请求创建容量。
6、监控是可扩展系统的基础
所有团队在经历更大的工作负载时都面临的一个问题是大规模测试。真实的压测很难进行。假设您想测试一个现有的系统,看看如果数据库的大小增加了10倍,它是否仍然能够提供快速的响应时间。首先需要生成大量数据,这些数据与您的数据集和关系的特征相一致。 您还需要生成一个实际的工作负载。读,还是同时读和写?然后,您需要加载和部署您的数据集,并运行负载测试,这些可以使用压测工具。
这是一项艰巨的工作。很难得到所有能代表真实的数据,这样你就能得到有意义的结果。不出所料,很少有人这样做。
另一种选择是监控。对系统的简单监控包括确保基础设施是可操作的。如果资源正在耗尽(如内存或磁盘空间),或者远程调用出现故障,则应该发出警报,以便在真正糟糕的事情发生之前采取补救行动。
上述监控是必要的,但还不够。随着系统的扩展,您需要理解应用程序行为之间的关系。一个简单的例子是,当并发写请求量增加时,数据库写操作是如何执行的。您还希望知道微服务中的断路器何时由于下游延迟增加而触发,负载均衡器何时开始生成新实例,或消息在队列中停留的时间超过指定的阈值。
有无数的解决方案可用于监控。Splunk是一个综合而强大的日志聚合框架。任何云都有自己的监控框架,比如AWS Cloudwatch。它们允许您捕获有关系统行为的指标,并将这些指标显示在统一的仪表板中,以支持对性能的监视和分析。可观测性这一术语通常包含监控和性能分析的整个过程。
至少有两件事需要考虑。
首先,为了深入了解性能,您需要生成与应用程序行为细节相关的定制指标。仔细设计这些指标并在微服务中添加代码,将它们注入到监控框架中,以便在系统仪表板中观察和分析它们。
其次,监控是系统中必要的功能(和成本)。它被打开了。当您需要性能调优和扩展系统时,您捕获的数据将指导您进行调优。
总结
高性能和可扩展通常不是我们许多系统的优先要求。理解、实现和迭代功能需求通常是会消耗所有可用的时间和预算。但有时,在外部事件或意外成功的驱动下,可扩展变得必要,否则系统将不可用。不可用的系统(或由于性能缓慢而不可用的系统)对任何人都没有用处。
像任何复杂的软件架构一样,没有一层不变的规则可以遵循。在精确的系统需求指导下,权衡和折衷对于实现可扩展性是必不可少的。记住上面的规则,通往可扩展性的道路就会减少许多的坑!