vlambda博客
学习文章列表

分布式系统架构:简介

分布式系统架构:简介

基本的系统架构

事实上,所有大规模的系统都是从小规模开始的,并因其成功而不断增长。从开发框架开始,比如Ruby on Rails或Django等,这是很常见的,也是很明智的,因为他可以促进快速开发,使系统快速启动和运行。图1显示了一个典型的、非常简单的系统软件架构,他与快速开发框架非常相似。他包括客户端层、应用程序服务层和数据库层。如果使用Rails或类似的工具,还会得到一个框架,该框架将用于Web应用程序处理的模型-视图-控制器(MVC)模式和生成SQL查询的对象-关系映射器(ORM)硬连接在一起。

图1

使用这种架构,用户可以从他们的移动应用程序或Web浏览器向应用程序提交请求。Internet网络将这些请求传递给运行在某个公司或商业云数据中心的机器上的应用服务。通信使用标准的应用程序级网络协议,通常是HTTP。

应用程序服务运行支持应用程序编程接口(API)的代码,客户端使用该接口格式化数据并向其发送HTTP请求。在收到请求后,服务执行与所请求的API相关联的代码。在这个过程中,他可以从数据库或其他外部系统读取或写入数据,这取决于API的语义。当请求完成时,该服务将结果发送给客户端,以在其应用程序或浏览器中显示。

许多系统在概念上看起来就是这样的。应用程序服务代码利用服务器执行环境,该环境允许同时处理来自多个用户的多个请求。在这个场景中,有无数的应用服务器技术被广泛使用——Java的JEE和Spring, Python的Flask。

这种方法导致了通常所说的单片架构随着应用程序的功能变得越来越丰富,单一组件的复杂性也会随之增加。所有API处理程序都构建在相同的服务器代码体中。这最终会使快速修改和测试变得困难,并且由于所有API实现都在同一个应用程序服务中运行,执行占用空间可能变得非常大。

如果请求负载相对较低,这个应用程序体系结构就足够了。该服务具有以一致的低延迟处理请求的能力。但是,如果请求负载持续增长,这意味着延迟将会增加,因为服务的CPU/内存容量不足,无法满足并发请求量,因此需要更长的时间来处理请求。在这种情况下,我们的单个服务器就会超载,成为瓶颈。

在这种情况下,扩展的第一个策略通常是“扩展”应用服务硬件。例如,如果你的应用程序在AWS上运行,可能会将服务器从普通的t3升级。一个t3.2xlarge实例有4个(虚拟的)cpu和16gb的内存,这个t3.2xlarge实例使可用的cpu和内存数量翻倍。

扩大规模很简单。虽然,这只是在硬件上花了更多的钱,但这对你来说是可伸缩的。

然而,无论你有多少cpu和多少内存,对于许多应用程序来说,负载将不可避免地增长到足以淹没单个服务器节点的水平。这时你就需要一个新的策略,即向外扩展,或水平扩展。

向外扩展(Scale Out)

向外扩展依赖于在体系结构中复制服务并在多个服务器节点上运行多个副本的能力。从理论上讲,如果我们有N个副本,每个服务器节点处理{# Requests /N}。这个简单的策略提高了应用程序的能力,从而提高了可伸缩性。

要成功地向外扩展应用程序,在我们的设计中需要两个基本元素。如图2所示:

分布式系统架构:简介

图2
  • 负载均衡器(Load balancer)

    所有用户请求都被发送到负载均衡器,负载均衡器选择一个服务副本目标来处理请求。选择目标服务存在各种策略,所有策略的核心目标都是保持每个资源都同样忙碌。负载平衡器还将来自服务的响应转发回客户机。大多数负载均衡器都属于一类称为反向代理的Internet组件,他控制客户端请求对服务器资源的访问。作为中介,反向代理为请求添加额外的网络跳,因此需要极低的延迟,以最小化他们带来的开销。有许多现成的负载平衡解决方案以及特定于云提供商的解决方案。

  • 无状态服务(Stateless services)

    为了使负载均衡有效并均匀地共享请求,负载均衡器必须能够自由地将来自同一客户端的连续请求发送给不同的服务实例进行处理。这意味着服务中的API实现必须不保留与单个客户端会话相关联的状态。当用户访问应用程序时,服务创建一个用户会话,并在内部管理一个惟一会话,以确定用户交互的顺序并跟踪会话状态。会话状态的一个经典示例是购物车。为了有效地使用负载均衡器,表示用户购物车当前内容的数据必须存储在某个地方—通常是数据存储—以便任何服务副本在作为用户会话的一部分接收请求时都可以访问该状态。在图2中,他被标记为会话存储。

向外扩展很有吸引力,因为从理论上讲,可以不断添加新的(虚拟)硬件和服务来处理增加的请求负载,并保持请求延迟一致和低延迟。一旦看到延迟上升,就部署另一个服务器实例。这不需要使用无状态服务更改代码,因此成本相对较低——你只需为部署的硬件付费。

Scale out还有另一个非常吸引人的特点。如果其中一个服务失败,他正在处理的请求将丢失。但是由于失败的服务不管理会话状态,这些请求可以简单地由客户机重新发出,并发送到另一个服务实例进行处理。这意味着应用程序对服务软件和硬件中的故障具有弹性,从而提高了应用程序的可用性。

不幸的是,与任何工程解决方案一样,简单的向外扩展也有局限性。随着添加新的服务实例,请求处理能力可能会无限增长。然而,在某些阶段,实际情况将会发生变化,单个数据库提供低延迟查询响应的能力将会减弱。慢的查询将意味着客户机的响应时间更长。

如果请求的到达速度持续快于处理速度,那么某些系统组件将会因资源耗尽而超载并失败,客户机将会看到异常和请求超时。实际上,你的数据库已经成为一个瓶颈,为了进一步扩展你的应用程序,你必须进行工程处理。

使用缓存扩展数据库

通过增加数据库服务器中的cpu、内存和磁盘的数量来扩展系统,可以实现很长的一段时间。扩展是一种非常常见的数据库可伸缩性策略。

大型数据库需要高度熟练的数据库管理员的持续关注和关注,以保持优化和快速运行。在这项工作中有很多技巧—例如查询调优、磁盘分区、索引、节点上缓存—因此数据库管理员是非常有价值的人,应该非常友好地对待他们。他们确实可以使你的应用程序服务具有高度响应性。

与扩展相结合,一种非常有效的方法是尽可能不频繁地从服务中查询数据库。这可以通过在扩展的服务层中使用分布式缓存来实现。缓存将最近检索和经常访问的数据库存储在内存中,因此可以在不给数据库增加负担的情况下快速检索他们。例如,下一个小时的天气预报不会改变,但可能会被100个或数千个客户查询。一旦发出预测,你可以使用缓存来存储预测。所有客户端请求将从缓存中读取,直到预测过期。

对于频繁读取而很少更改的数据,可以修改处理逻辑,首先检查分布式缓存,例如Redis或memcached存储。这些缓存技术本质上是分布式的键值存储,使用非常简单的api。该方案如图3所示。注意图2中的“Session Store”已经消失。这是因为你可以使用通用的分布式缓存来存储会话标识符和应用程序数据。

分布式系统架构:简介

图3

访问缓存需要从你的服务进行远程调用。如果你需要的数据在高速缓存中,那么在高速网络中,可以预期到毫秒级的缓存读取。这比查询共享数据库实例的开销要低得多,而且也不需要查询来争夺通常稀少的数据库连接。

引入缓存层还需要修改处理逻辑以检查缓存数据。如果你想要的内容不在缓存中,那么你的代码仍然必须查询数据库,并将结果加载到缓存中,并将其返回给调用者。你还需要决定何时删除或使缓存的结果失效——这取决于你的数据的性质(例如,天气预报自然过期)和你的应用程序对客户端提供过期结果的容忍程度。

一个设计良好的缓存方案在扩展系统方面绝对是无价的。缓存对于很少更改且经常访问的数据非常有效,例如库存目录、事件信息和联系人数据。如果你能够处理来自缓存的很大比例的读请求,比如80%或更多,那么你就可以有效地为数据库购买额外的容量,因为他们永远不会看到很大比例的请求。

尽管如此,许多系统仍然需要快速访问tb级和更大的数据存储,这使得单个数据库变得非常困难。在这些系统中,需要一个分布式数据库。

分布式数据库(Distributing the Database)

到2021年,分布式数据库技术的数量可能比你想象的要多。这是一个复杂的领域,总的来说,主要有两大类:

  • 主要供应商(如Oracle和IBM)的分布式SQL存储。通过将数据存储在由多个数据库引擎副本查询的多个磁盘上,组织可以相对无缝地扩展其SQL数据库。从逻辑上讲,应用程序将这些多个引擎视为单个数据库,从而最小化代码更改。还有一类“天生的分布式”SQL数据库,通常被称为NewSQL存储,属于这一类。
  • 分布所谓的NoSQL存储从整个阵列的供应商。这些产品使用各种数据模型和查询语言将数据分布到运行数据库引擎的多个节点上,每个节点都有自己的本地附加存储。同样,数据的位置对应用程序是透明的,通常由对数据库键使用散列函数的数据模型设计控制。该类别的主要产品有Cassandra、MongoDB和Neo4j。

图4显示了我们的架构是如何整合分布式数据库的。随着数据量的增长,分布式数据库具有能够增加存储节点数量的特性。随着节点的增加(或删除),所有节点管理的数据将重新平衡,以确保每个节点的处理和存储容量得到平等利用。

分布式系统架构:简介

图4

分布式数据库也提高了可用性。他们支持复制每个数据存储节点,因此,如果一个节点出现故障或由于网络问题无法访问,则可以使用另一个数据副本。

如果你正在使用一个云提供商,那么对于你的数据层还有两种部署选择。你可以部署自己的虚拟资源,并构建、配置和管理自己的分布式数据库服务器。或者,你可以利用云托管数据库。后者简化了与管理、监视和扩展数据库相关的管理工作,因为其中许多任务实际上是由你选择的云提供商负责的。

多处理层(Multiple Processing Tiers)

需要扩展的任何现实系统都有许多不同的服务,他们通过交互来处理请求。例如,访问Amazon.com网站上的一个网页可能需要调用超过100种不同的服务,然后才向用户返回响应。

本文中阐述的无状态、负载平衡、缓存架构的美妙之处在于,他可以扩展核心设计原则并构建多层应用程序。在完成请求时,服务可以调用一个或多个依赖服务,这些服务反过来被复制并进行负载平衡。一个简单的示例如图5所示。在服务如何交互以及应用程序如何确保来自依赖服务的快速响应方面存在许多细微差别。

分布式系统架构:简介

图5

这种设计还促进在体系结构的每一层拥有不同的、负载平衡的服务。例如,图6演示了两个复制的面向internet的服务,他们都利用了提供数据库访问的核心服务。每个服务都是负载平衡的,并使用缓存来提供高性能和可用性。这种设计通常用于为Web客户端提供服务和为移动客户端提供服务,每个服务都可以根据他们所经历的负载独立地进行扩展。他通常被称为后端为前端(BFF)模式。

图6

此外,通过将应用程序分解为多个独立的服务,你可以根据服务需求对每个服务进行扩展。例如,如果你看到来自移动用户的请求量增加,而来自Web用户的请求量减少,那么可以为每个服务提供不同数量的实例来满足需求。这是将单个应用程序重构为多个独立服务的主要优势,这些服务可以分别构建、测试、部署和扩展。

提高响应能力(Increasing Responsiveness)

大多数客户端应用程序请求都需要响应。用户可能希望查看给定产品类别的所有拍卖项目,或者查看在给定位置可供出售的房地产。在这些示例中,客户机发送请求并等待,直到接收到响应。发送请求和接收结果之间的时间间隔就是请求的响应时间。你可以通过使用缓存和预计算响应来减少响应时间,但是许多请求仍然会导致数据库访问。

但是,有些更新请求可以在不完全持久化数据库数据的情况下成功响应。服务实现可以利用这种类型的场景来提高响应能力。有关该事件的数据被发送到服务,该服务确认接收并并发地将数据存储在远程队列中,以便随后写入数据库。分布式队列平台可以用来可靠地将数据从一个服务发送到另一个服务,通常但不总是采用先进先出(FIFO)模式。

将消息写入队列通常比写入数据库快得多,这使请求能够更快地成功确认。部署另一个服务来从队列读取消息并将数据写入数据库。

实现此方法的基本架构如图7所示。

图7

只要不是立即需要写操作的结果,应用程序就可以使用这种方法来提高响应性,从而提高可伸缩性。应用程序可以利用许多队列技术。这些排队平台都提供异步通信。生产者服务写入队列,该队列充当临时存储,而另一个消费者服务从队列中删除消息,并对数据库进行必要的更新。

关键是数据最终会被持久化。最终通常意味着最多几秒钟,但采用这种设计的用例应该能够适应较长的延迟,而不会影响用户体验。

系统和硬件的可扩展性(Systems and Hardware Scalability)

如果服务和数据存储在性能不足的硬件上运行,即使是最精心设计的软件架构和代码在可伸缩性方面也会受到限制。通常部署在可伸缩系统中的开放源代码和商业平台旨在利用CPU内核、内存和磁盘等额外的硬件资源。这是实现你所需的性能和可伸缩性,以及尽可能降低成本之间的一种平衡。

也就是说,在某些情况下,升级CPU内核数量和可用内存并不能为你带来更多的可伸缩性。例如:

代码是单线程的。在一个拥有更多内核的节点上运行他不会提高性能。任何时候只需要一个内核。其余的根本没有被利用。

一个多线程代码包含许多序列化的部分,这意味着一次只能有一个线程进行以确保结果是正确的。这种现象可以用阿姆达尔定律来描述。这为我们提供了一种方法,可以根据连续执行的代码数量来计算在添加更多CPU核时代码的理论加速。

Amdahl定律的两个观点是:

  • 如果只有5%的代码是串行执行的,其余的都是并行执行的,那么添加2048个以上的核基本上是没有效果的
  • 如果50%的代码是串行执行的,其余的是并行执行的,那么添加超过8个核基本上没有效果

这说明了为什么高效的多线程代码对实现可伸缩性至关重要。如果你的代码不能像线程一样运行高度独立的任务,那么就连金钱也买不到可伸缩性。这就是为什么我们需要多线程——他是构建可伸缩分布式系统的核心知识组件。

总结

在本文中,简要介绍了将系统扩展为通信服务和分布式数据库的集合可以使用的主要方法。分布式系统中有许多的细节概念,敬请期待下一次的内容。