从零到百万用户的扩展之路
- Published on
设计一个支持数百万用户的系统是极具挑战性的,它是一段需要持续优化和不断改进的旅程。在这篇博客中,我们将构建一个支持单个用户的系统,并逐步扩展到能够服务于数百万用户。在阅读这篇博客之后,你将掌握一些有助于破解系统设计面试题的技术。
在这段旅程中,你会学到:
- 初始设计:如何从一个简单的系统开始,满足最基本的用户需求。
- 扩展策略:使用不同的技术和架构模式来提升系统的扩展性。
- 性能优化:识别并解决性能瓶颈,确保系统在高负载下依然稳定高效。
- 容错和恢复:设计系统以应对故障,并快速恢复,保证高可用性。
- 监控和调试:实施有效的监控和调试策略,确保系统的健康运行。
通过这些内容,你不仅能更好地理解如何从零开始构建一个大规模系统,还能在系统设计面试中脱颖而出。接下来,我们将逐步深入探讨每个阶段的关键技术和最佳实践。
单服务器设置
千里之行,始于足下,构建一个复杂的系统也不例外。我们需要从一个简单的开始,一切都运行在一台服务器上。这一阶段的目标是构建一个功能完整、易于维护的初始系统。图1展示了单服务器设置的示意图,在这个设置中,所有东西都运行在同一台服务器上:包括Web应用、数据库、缓存等。
为了理解这一设置,研究请求流程和流量来源是很有帮助的。首先,让我们看看请求流程(图2):
- 用户通过域名(如 api.mysite.com)访问网站。通常,域名系统(DNS)是由第三方提供的付费服务,并不托管在我们自己的服务器上。
- 互联网协议(IP)地址会返回给浏览器或移动应用程序。在示例中,返回的IP地址是15.125.23.214。
- 一旦获得IP地址,超文本传输协议(HTTP)请求会直接发送到你的Web服务器。
- Web服务器返回用于渲染的HTML页面或JSON响应。
接下来,让我们来探讨流量来源。Web服务器的流量来源有两个:Web应用和移动应用。
Web应用:它使用服务器端语言(如Java、Python等)处理业务逻辑和存储等,并使用客户端语言(如HTML和JavaScript)进行展示。
移动应用:HTTP协议是移动应用和Web服务器之间的通信协议。JavaScript对象表示法(JSON)通常用作API响应格式,因为它简单易用。下面是一个JSON格式的API响应示例:
// GET /users/12 – Retrieve user object for id = 1
{
"userId": 1,
"userName": "john_doe",
"email": "john_doe@example.com",
"profile": {
"age": 30,
"gender": "male",
"location": "New York"
}
}
数据库
随着用户基础的增长,一台服务器已无法满足需求,我们需要多台服务器:一台用于处理Web/移动流量,另一台用于数据库(图3)。将Web/移动流量(Web层)和数据库(数据层)服务器分离,可以使它们独立扩展。
数据库的选择
你可以在传统的关系型数据库和非关系型数据库之间进行选择。让我们来探讨它们的区别。
关系型数据库也称为关系型数据库管理系统(RDBMS)或SQL数据库。最流行的有MySQL、Oracle数据库、PostgreSQL等。关系型数据库以表格和行的形式表示和存储数据。你可以使用SQL在不同的数据库表之间执行连接操作。
非关系型数据库也称为NoSQL数据库。流行的有CouchDB、Neo4j、Cassandra、HBase、Amazon DynamoDB等。这些数据库分为四类:键值存储、图存储、列存储和文档存储。非关系型数据库一般不支持连接操作。
对于大多数开发人员来说,关系型数据库是最佳选择,因为它们已经存在了超过40年,并且在历史上表现良好。然而,如果关系型数据库不适合你的特定用例,那么探索关系型数据库之外的选项是很重要的。非关系型数据库可能是正确的选择,如果:
- 你的应用程序需要超低延迟。
- 你的数据是非结构化的,或者你没有任何关系数据。
- 你只需要序列化和反序列化数据(如JSON、XML、YAML等)。
- 你需要存储大量数据。
关系型数据库的特点:
- 数据结构化 关系型数据库使用表格来组织数据,数据结构清晰,便于管理和查询。
- 支持事务 关系型数据库支持ACID(原子性、一致性、隔离性、持久性)事务,确保数据操作的可靠性。
- 强大的查询能力 使用SQL可以方便地进行复杂的查询和数据分析,支持连接操作,适合关系数据。
非关系型数据库的特点:
- 高性能 非关系型数据库通常具有较低的延迟和较高的吞吐量,适合高性能需求的应用。
- 灵活的数据模型 非关系型数据库支持多种数据模型,如键值对、文档、列族和图结构,适合非结构化或半结构化数据。
- 可扩展性 非关系型数据库通常更容易横向扩展,可以处理大规模的数据和高并发请求。
选择合适的数据库:
- 如果你的应用程序主要处理关系数据,并且需要执行复杂的查询和事务操作,关系型数据库是理想的选择。
- 如果你的应用程序需要处理大量非结构化数据、高性能需求或者需要简单的键值存储,非关系型数据库可能更合适。
垂直扩展 VS 水平扩展
垂直扩展,也称为“向上扩展”(scale up),指的是通过增加服务器的处理能力(如CPU、内存等)来提升性能。水平扩展,也称为“向外扩展”(scale-out),则是通过增加更多服务器到资源池中来进行扩展。
当流量较低时,垂直扩展是一个很好的选择,其主要优势在于简单。然而,垂直扩展存在一些严重的限制:
- 硬性限制 垂直扩展有一个硬性限制,不可能无限制地向单台服务器添加CPU和内存。
- 缺乏故障转移和冗余 垂直扩展不具备故障转移和冗余功能。如果一台服务器宕机,整个网站/应用程序也会随之完全宕机。
对于大规模应用程序,由于垂直扩展的局限性,水平扩展更为理想。
在前面的设计中,用户直接连接到Web服务器。如果Web服务器离线,用户将无法访问网站。在另一种情况下,如果许多用户同时访问Web服务器并达到了其负载限制,用户通常会体验到响应变慢或无法连接到服务器。负载均衡器是解决这些问题的最佳技术。
垂直扩展的特点:
- 简单易用 通过增加服务器的CPU、内存和存储等资源来提升性能。
- 快速实现 只需在现有服务器上进行硬件升级,无需修改现有架构。
- 有限的扩展能力 受到硬件的物理限制,无法无限制地增加资源。
水平扩展的特点:
- 高可扩展性 通过增加更多服务器来处理更多流量,理论上可以无限扩展。
- 高可用性 实现故障转移和冗余,即使一台服务器宕机,其他服务器仍能提供服务。
- 负载均衡 通过负载均衡器将流量均匀分配到多台服务器上,提高系统的处理能力和稳定性。
负载均衡器
负载均衡器(Load Balancer)的作用是将进入的流量均匀地分配到定义的一组负载均衡的Web服务器中。图4展示了负载均衡器的工作原理。
在这个架构中,负载均衡器充当了客户端和多个后端服务器(Web服务器)之间的中介。它的主要功能是根据预定义的算法(如轮询、最少连接等),将进入的网络请求平均分发到后端服务器上。
如图4所示,用户直接连接到负载均衡器的公共IP地址。通过这种设置,客户端不再能直接访问Web服务器。为了提升安全性,服务器之间的通信采用私有IP地址。私有IP地址仅在同一网络中的服务器之间可达,无法通过互联网访问。负载均衡器通过私有IP地址与Web服务器进行通信。
在图4中,添加了负载均衡器和第二个Web服务器后,我们成功解决了故障转移问题,并提升了Web层的可用性。具体细节如下:
- 故障转移处理:如果服务器1宕机,所有流量将会路由到服务器2,从而防止网站宕机。我们还可以向服务器池中添加一个新的健康Web服务器,以平衡负载。
- 流量快速增长:如果网站流量迅速增长,两台服务器可能无法处理这些流量。负载均衡器可以优雅地处理这个问题。只需向Web服务器池中添加更多服务器,负载均衡器将自动开始向这些服务器发送请求。
现在Web层看起来很不错,那数据层呢?当前设计只有一个数据库,因此不支持故障转移和冗余。数据库复制是解决这些问题的常见技术。让我们来详细看一下。
数据库复制
引用维基百科的描述:“数据库复制可以在许多数据库管理系统中使用,通常在原始数据库(主数据库)和副本(从数据库)之间建立主/从关系” 。
主数据库通常只支持写操作。从数据库从主数据库获取数据的副本,只支持读操作。所有涉及数据修改的命令,如插入、删除或更新,必须发送到主数据库。大多数应用程序通常需要更高比例的读操作而不是写操作,因此系统中从数据库的数量通常比主数据库多得多。图5展示了一个具有多个从数据库的主数据库。
数据库复制的优势包括:
- 更好的性能: 在主从模型中,所有的写入和更新操作都发生在主节点上,而读取操作则分布在从节点上。这种模型通过允许更多查询并行处理,提高了性能。
- 可靠性: 如果你的数据库服务器之一受到自然灾害(如台风或地震)的破坏,数据仍然可以得到保留。你不需要担心数据丢失,因为数据已经复制到多个位置。
- 高可用性: 通过在不同的位置复制数据,即使一个数据库离线,你的网站仍然可以运行,因为可以访问存储在其他数据库服务器中的数据。
在前面的内容中,我们讨论了负载均衡器如何提高系统的可用性。在这里我们提出同样的问题:如果一个数据库离线了会怎么样?图5中讨论的架构设计可以处理这种情况:
- 如果只有一个从数据库可用并且它离线了,读操作将暂时指向主数据库。一旦问题解决,一个新的从数据库将取代旧的数据库。如果有多个从数据库可用,读操作会重定向到其他健康的从数据库,并且会增加一个新的数据库服务器来替换旧的服务器。
- 如果主数据库离线,一个从数据库将被提升为新的主数据库。所有的数据库操作将临时在新的主数据库上执行,同时一个新的从数据库会立即取代旧的数据库进行数据复制。在生产系统中,提升新的主数据库更为复杂,因为从数据库中的数据可能不是最新的。需要通过运行数据恢复脚本来更新缺失的数据。虽然一些其他的复制方法,如多主复制和环形复制可能会有所帮助,但这些设置更加复杂。有兴趣的读者可以参考列出的参考资料 (Wikipedia contributors, 2024)。
图6显示了在添加负载均衡器和数据库复制后的系统设计。
让我们来看一下这个设计:
- 用户从DNS获取负载均衡器的IP地址。
- 用户使用这个IP地址连接到负载均衡器。
- HTTP请求被路由到Server 1或Server 2之一。
- Web服务器从从数据库中读取用户数据。
- Web服务器将所有数据修改操作路由到主数据库。这包括写入、更新和删除操作。
现在,您对Web层和数据层有了坚实的理解,是时候提高负载和响应时间了。可以通过添加缓存层和将静态内容(JavaScript、CSS、图像、视频文件)转移到内容交付网络(CDN)来实现这一点。
缓存
缓存是一个临时存储区域,用于将昂贵的响应结果或频繁访问的数据存储在内存中,以便后续请求能够更快地得到响应。正如图6所示,每当加载新的网页时,都会执行一个或多个数据库调用来获取数据。频繁地调用数据库会显著影响应用程序的性能,而缓存可以缓解这个问题。
缓存层
缓存层是一个临时的数据存储层,比数据库快得多。拥有独立的缓存层的好处包括提升系统性能、减少数据库工作负载以及能够独立扩展缓存层。图7展示了一个可能的缓存服务器设置:
收到请求后,Web 服务器首先检查缓存中是否有可用的响应。如果有,它会将数据发送回客户端。如果没有,则查询数据库,将响应存储在缓存中,并将其发送回客户端。这种缓存策略称为读取穿透缓存。根据数据类型、大小和访问模式,还可以使用其他缓存策略。
与缓存服务器交互非常简单,因为大多数缓存服务器提供了常见编程语言的 API。以下代码片段展示了典型的 Memcached API 用法:
SECONDS = 1
cache.set('myKey', 'hi there', 3600 * SECONDS)
cached_value = cache.get('myKey')
这段代码演示了如何使用 Memcached 的 API 进行数据的设置和获取操作。
使用缓存的建议
使用缓存系统时需要考虑以下几点:
决定何时使用缓存:在数据频繁读取但很少修改的情况下考虑使用缓存。由于缓存数据存储在易失性内存中,缓存服务器不适合持久化数据。例如,如果缓存服务器重新启动,内存中的所有数据都会丢失。因此,重要数据应该保存在持久化数据存储中。
过期策略:实施过期策略是一个良好的做法。一旦缓存数据过期,它会从缓存中移除。当没有过期策略时,缓存数据将永久存储在内存中。建议不要将过期时间设置得太短,否则系统会过于频繁地从数据库重新加载数据。同时,也不建议将过期时间设置得太长,以免数据过时。
一致性:这涉及保持数据存储和缓存的同步。由于数据存储和缓存之间的数据修改操作不在单个事务中,因此可能出现不一致性。在跨多个区域进行扩展时,维护数据存储和缓存之间的一致性是具有挑战性的。
减少故障影响:单个缓存服务器可能会成为潜在的单点故障(SPOF)。根据维基百科的定义,“单点故障(SPOF)是系统的一部分,如果发生故障,将导致整个系统停止工作” 。因此,建议在不同数据中心部署多个缓存服务器,以避免单点故障。另一种推荐的方法是通过过量分配所需内存的百分比来提供缓冲,以应对内存使用增加的情况。
- 驱逐策略 :一旦缓存满了,试图向缓存添加新项的请求可能会导致现有的项被移除。这称为缓存驱逐。最近最少使用(LRU)是最流行的缓存驱逐策略。其他驱逐策略,如最不经常使用(LFU)或先进先出(FIFO),可以根据不同的使用情况进行采用。
内容交付网络(CDN)
内容交付网络(CDN)是一个分布式的网络系统,由多个位于全球不同位置的服务器节点组成。其主要目的是通过在靠近用户的边缘节点缓存和交付静态内容,从而加速网站和应用程序的内容传输。CDN可以有效地降低用户访问网站时的加载时间,提升网站的性能和用户体验。内容交付网络(CDN)在现代互联网架构中扮演着至关重要的角色,特别是在全球化用户访问大流量网站时。以下是对CDN的进一步拓展和深入理解:
- 静态内容缓存和加速:CDN主要用于缓存和加速静态内容,例如图片、视频、样式表(CSS)、JavaScript文件等。通过将这些内容分发到全球各地的CDN边缘节点,用户可以从距离更近的服务器获取内容,从而显著减少加载时间和提升用户体验。
- 动态内容缓存的新概念:随着互联网应用的复杂性增加,CDN开始支持动态内容的缓存。动态内容缓存不仅限于简单的静态文件,还包括根据请求路径、查询字符串、Cookie和请求头生成的动态HTML页面。这种缓存技术使得即使在高流量情况下,网站也能快速响应请求,提升了网站的性能和可伸缩性。
- 全球网络覆盖和就近服务:CDN提供商通常在全球各地部署数百甚至数千个边缘节点。这些节点位于各大城市、互联网交换点和云服务提供商数据中心中,确保用户无论身处何处,都能享受到快速的内容访问。例如,一个位于美国西海岸的CDN节点可以使得美国东海岸的用户和欧洲的用户同样能够快速访问网站内容。
- 业务应用场景:除了静态内容加速外,CDN还被广泛应用于视频直播、软件分发、游戏内容加速等场景。例如,视频流服务商可以利用CDN提供高质量的视频内容传输,而在线游戏开发商则可以通过CDN加速游戏内容和更新的分发,从而提升全球用户的游戏体验。
- CDN与安全性:CDN不仅仅是一个加速工具,它还提供了一定程度的安全防护。通过分发静态内容,CDN可以缓解DDoS(分布式拒绝服务攻击)和其他恶意攻击,保护源服务器免受大流量和攻击性负载的影响。
图 10 展示了 CDN 的工作流:
- 用户 A 请求图像: 用户 A 使用如下 CDN 提供商提供的图像 URL 请求图像:https://mysite.cloudfront.net/logo.jpg 或 https://mysite.akamai.com/image-manager/img/logo.jpg 。
- CDN 服务器检查缓存: CDN 服务器首先检查自身的缓存是否已经有了 image.png 的副本。
- 未命中缓存: 如果 CDN 服务器的缓存中没有 image.png 的副本(缓存未命中),CDN 服务器会发起请求,从源服务器获取该文件。源服务器可以是用户指定的 Web 服务器,或者是在线存储服务,比如 Amazon S3。
- 从源服务器获取图像: 源服务器接收到来自 CDN 服务器的请求,并将 image.png 文件返回给 CDN 服务器。在返回的 HTTP 响应中,源服务器可以包含一个可选的 HTTP 头部 Time-to-Live(TTL)。TTL 表示图像在 CDN 缓存中的有效时间,通常以秒为单位计算。
- CDN 缓存图像: CDN 服务器接收到从源服务器返回的 image.png 文件后,会将该图像存储在自己的缓存中。图像将被保存在 CDN 的各个全球节点中,这些节点靠近用户,以提高图像传输的速度和效率。
- 返回图像给用户 A: CDN 服务器将缓存的 image.png 图像返回给用户 A 的浏览器或设备。用户 A 能够快速加载图像,因为它来自距离最近的 CDN 节点,而不是源服务器。
- 用户 B 请求相同图像: 在 TTL 有效期内,如果用户 B 发送了对相同 image.png 图像的请求,CDN 将直接从缓存中返回该图像。这样可以减少对源服务器的请求,加快了图像的加载速度,并减少了网络流量的消耗。
使用CDN的建议
当考虑使用CDN时,以下几点是需要详细考虑和说明的重要因素:
- 成本考虑: CDN服务通常基于数据的进出量收费。这意味着您需要支付CDN提供商用于传输数据的费用。对于那些不经常被访问的资源,将它们保留在CDN中可能并不划算,因为这会增加不必要的成本。因此,您应该仔细评估哪些资源适合存放在CDN中,哪些资源应该移出CDN以节省费用。
- 设置适当的缓存过期时间: 缓存过期时间的选择至关重要。如果设置的过期时间过长,可能导致用户访问的内容不再是最新的版本,从而影响用户体验。另一方面,如果过期时间过短,可能会频繁地从源服务器重新加载内容到CDN,增加网络流量和源服务器的负载。因此,建议根据内容的更新频率和重要性来设置适当的缓存过期策略。
- CDN的备用方案: 考虑到CDN服务可能出现临时中断或故障,您需要制定应对策略以确保用户访问的连续性和稳定性。一种常见的做法是确保您的网站或应用程序具有自动切换到备用资源的能力,例如当检测到CDN服务中断时,能够及时切换到源服务器获取资源。
- 使文件失效的策略: 如果您需要在文件到期之前从CDN中移除特定文件,通常有两种主要的方法:
- 使用CDN提供商提供的API来使特定的CDN对象失效。这允许您在需要时立即移除缓存中的文件。
- 使用对象版本化策略来管理文件版本。通过在URL中添加版本号或其他标识符,您可以有效地控制不同版本的文件在CDN中的存储和访问。这种方法尤其适用于那些需要频繁更新的静态资源,如网页图像或脚本文件。为了对对象进行版本化,您可以在URL中添加参数,比如版本号。例如,向查询字符串中添加版本号2:
image.png?v=2
。
图 11 展示了在添加 CDN 和 cache 之后的设计图:
- 静态资产(如JS、CSS、图像等)不再由Web服务器提供。它们通过CDN获取,以提升性能。
- 通过缓存数据,减轻了数据库的负载。
无状态web服务器
现在是考虑水平扩展Web层的时候了。为了实现这一点,我们需要将状态数据(例如用户会话数据)从Web层移出。一个良好的实践是将会话数据存储在持久化存储中,比如关系型数据库或NoSQL数据库中。集群中的每个Web服务器都可以从数据库中访问状态数据。这就是所谓的无状态Web层设计。
有状态的结构
一个有状态服务器和无状态服务器之间存在一些关键区别。有状态服务器会记住客户端数据(状态),从一个请求到下一个请求保持状态信息。而无状态服务器则不会保留任何状态信息。
图12展示了有状态的结构的样例:
在图12中,用户A的会话数据和个人资料图像存储在服务器1上。为了验证用户A的身份,HTTP请求必须路由到服务器1。如果请求发送到其他服务器,如服务器2,身份验证将失败,因为服务器2不包含用户A的会话数据。类似地,用户B的所有HTTP请求必须路由到服务器2;用户C的所有请求必须发送到服务器3。
问题在于,来自同一客户端的每个请求必须路由到同一台服务器。这可以通过大多数负载均衡器中的“粘性会话”来实现;然而,这会增加开销。使用这种方法,添加或删除服务器变得更加困难,处理服务器故障也更加具有挑战性。
无状态的结构
图13展示了有状态的结构的样例:
在这种无状态架构中,来自用户的HTTP请求可以被发送到任何Web服务器,这些服务器从共享数据存储中获取状态数据。状态数据存储在共享数据存储中,并且不存储在Web服务器中。无状态系统更简单、更健壮,并且易于扩展。
图14展示了带有无状态Web层的更新设计:
图14中,我们将会话数据从Web层移出,并将其存储在持久化数据存储中。共享数据存储可以是关系型数据库、Memcached/Redis、NoSQL等。选择NoSQL数据存储是因为它易于扩展。自动扩展意味着根据流量负载自动添加或移除Web服务器。在状态数据从Web服务器中移出后,可以通过根据流量负载自动添加或移除服务器来轻松实现Web层的自动扩展。
您的网站迅速增长,并吸引了大量国际用户。为了提高可用性并在更广泛的地理区域内提供更好的用户体验,支持多个数据中心至关重要。
数据中心
图15展示了一个具有两个数据中心的示例设置。在正常运行中,用户被地理DNS路由,也称为地理路由,到最近的数据中心,其中的流量在US-East,的流量在US-West。地理DNS是一种DNS服务,允许根据用户的位置将域名解析为IP地址。
在任何一个数据中心发生重大故障的情况下,我们会将所有流量引导到一个健康的数据中心。在图16中,数据中心2(美国西部)处于离线状态,100%的流量被引导到数据中心1(美国东部)。
要实现多数据中心设置,必须解决几个技术挑战:
流量重定向是指将用户请求引导到合适的数据中心,以确保最佳的性能和可用性。以下是一些具体的方法和工具:
- GeoDNS:GeoDNS 是一种 DNS 服务,它根据用户的地理位置将域名解析到最近的数据中心的 IP 地址。这可以减少网络延迟,提供更快的响应时间。例如,Akamai 和 Cloudflare 提供的 GeoDNS 服务可以根据用户的位置动态调整流量路由。
- 负载均衡:全球负载均衡器(如 AWS Global Accelerator 和 Google Cloud Load Balancing)可以跨多个数据中心分配流量,确保流量被分配到最健康和最接近的服务器上。
- 自动故障切换:在某个数据中心出现重大故障时,自动故障切换机制可以将流量快速重定向到另一个健康的数据中心,确保服务的连续性。
数据同步涉及确保不同数据中心之间的数据一致性和可用性。这是一个复杂且关键的任务,尤其是在面对高延迟和网络分区时。具体措施包括:
- 数据复制:跨数据中心复制数据是确保数据高可用性的常见策略。可以采用异步复制和同步复制两种方式。异步复制通常具有更低的延迟,但可能会有数据丢失的风险,而同步复制则能确保数据一致性,但会增加延迟。
- 冲突解决:在多个数据中心进行写操作时,可能会产生数据冲突。需要设计合理的冲突解决机制,例如使用时间戳或版本号来解决数据冲突。
- 一致性模型:选择合适的一致性模型(如最终一致性、强一致性或因果一致性)对于系统性能和数据一致性至关重要。Netflix 通过异步多数据中心复制实现了高可用性和一致性。
在多数据中心环境中,测试和部署变得更加复杂和重要。以下是一些具体的做法:
- 跨区域测试:在不同地理位置进行测试,以确保应用程序在不同网络条件下的性能和可用性。可以使用工具如 AWS CloudFormation 或者 Terraform 来自动化跨区域测试环境的搭建。
- 持续集成/持续部署(CI/CD):自动化部署工具(如 Jenkins、GitLab CI/CD、AWS CodePipeline 等)可以帮助保持所有数据中心的服务一致性。通过 CI/CD 流水线,可以在代码提交后自动部署到各个数据中心,并进行自动化测试,确保所有环境的一致性和稳定性。
- 蓝绿部署和金丝雀发布:这些部署策略可以在最小化风险的情况下进行更新和发布。蓝绿部署允许同时运行两个版本的应用程序,一个用于生产,一个用于测试,而金丝雀发布则逐步将新版本推出给少数用户,确保没有重大问题后再全面推广。
消息队列
消息队列是一个持久组件,存储在内存中,支持异步通信。它充当缓冲区并分发异步请求。消息队列的基本架构非常简单。输入服务(称为生产者或发布者)创建消息,并将其发布到消息队列中。其他服务或服务器(称为消费者或订阅者)连接到队列,并执行消息中定义的操作。这种模型如图17所示:
解耦使得消息队列成为构建可扩展且可靠应用程序的首选架构。有了消息队列,当消费者无法处理消息时,生产者可以将消息发布到队列中。而即使生产者不可用,消费者也可以从队列中读取消息。
考虑以下使用场景:你的应用程序支持照片定制,包括裁剪、锐化、模糊等。这些定制任务需要时间来完成。在图18中,Web服务器将照片处理任务发布到消息队列中。照片处理工作者从消息队列中提取任务,异步执行照片定制任务。生产者和消费者可以独立扩展。当队列的大小变大时,可以增加更多的工作者以减少处理时间。然而,如果队列大部分时间都是空的,则可以减少工作者的数量。
图18展示了这一使用场景的具体实现:
- Web服务器:接收用户请求,将照片处理任务(如裁剪、锐化、模糊等)发布到消息队列中。
- 消息队列:临时存储照片处理任务,确保任务不会丢失,并可以在消费者准备好处理时提供任务。
- 照片处理工作者:从消息队列中提取任务,异步执行照片处理操作。一旦任务完成,处理结果可以返回给Web服务器或直接存储在指定位置。
消息队列的优势
- 独立扩展:生产者和消费者可以根据需要独立扩展。例如,当队列变大时,可以增加工作者数量;当队列变小时,可以减少工作者数量。
- 异步处理:通过异步处理,系统可以处理大量并发请求,而不会影响整体性能。
- 容错性:即使某一部分系统出现故障,消息队列可以确保任务不会丢失,从而提高系统的可靠性。
日志记录、度量和自动化
当你在运行一个小型网站时,日志记录、度量和自动化支持虽然是好的实践,但并非必需。然而,现在你的网站已经发展成为一个服务大型业务的平台,投资这些工具变得至关重要。
日志记录
监控错误日志非常重要,因为它有助于识别系统中的错误和问题。你可以在每个服务器级别监控错误日志,或者使用工具将它们聚合到一个集中化服务中,以便于搜索和查看。
度量
收集不同类型的度量指标帮助我们获得业务洞察并了解系统的健康状态。以下是一些有用的度量指标:
- 主机级别指标:CPU、内存、磁盘I/O等。
- 聚合级别指标:例如,整个数据库层、缓存层的性能等。
- 关键业务指标:日活跃用户数、用户留存率、收入等。
自动化
当系统变大且复杂时,我们需要构建或利用自动化工具来提高生产力。持续集成(CI)是一种好的实践,其中每次代码检查通过自动化进行验证,使团队能够及早发现问题。此外,自动化你的构建、测试、部署过程等,能够显著提高开发人员的生产力。
下面是更新后的设计图。由于空间限制,图中只显示了一个数据中心。
- 设计包括一个消息队列,这有助于使系统更加松耦合和具有故障恢复能力。
- 日志记录、监控、度量和自动化工具也包含在内。
数据库扩展
数据库扩展有两种主要方法:纵向扩展和横向扩展。
纵向扩展
纵向扩展,也称为向上扩展,是通过为现有机器增加更多的资源(CPU、RAM、硬盘等)来实现扩展。有一些功能强大的数据库服务器。根据Amazon Relational Database Service(RDS),你可以获得具有24 TB RAM的数据库服务器。这种强大的数据库服务器可以存储和处理大量数据。例如,stackoverflow.com
在2013年每月有超过1000万独立访客,但它只有一个主数据库。然而,纵向扩展也存在一些严重的缺点:
- 硬件限制:你可以为数据库服务器增加更多的CPU、RAM等,但硬件总有其极限。如果你拥有庞大的用户群,一个服务器可能不够用。
- 单点故障风险:纵向扩展增加了单点故障的风险,因为所有数据都集中在一个服务器上。
- 高成本:纵向扩展的总体成本较高,功能强大的服务器非常昂贵。
横向扩展
横向扩展,也称为分片,是通过增加更多的服务器来实现扩展。图20比较了纵向扩展和横向扩展的区别。
分片将大型数据库分割成更易于管理的小部分,称为分片。每个分片共享相同的数据库模式,但每个分片上的实际数据是唯一的。
图21展示了分片数据库的一个例子。用户数据根据用户ID分配到不同的数据库服务器。每次访问数据时,使用一个哈希函数来找到相应的分片。在我们的例子中,使用user_id % 4
作为哈希函数。如果结果等于0,则使用分片0来存储和获取数据。如果结果等于1,则使用分片1。其他分片的逻辑也是相同的。
图 22 展示了在共享数据库中的用户表:
当考虑实施分片策略时,选择适当的分片键(或分区键)是至关重要的。分片键决定了如何将数据分布到不同的分片中,直接影响了系统的性能和扩展能力。举例来说,假设我们有一个社交媒体平台,用户数据存储在数据库中。我们可以选择将用户数据按照地理位置分片,例如按照用户注册时的国家或地区码分片,这样可以确保每个分片包含的用户数据大致相等,并且分布在全球不同的数据库服务器上。
数据重分片
在使用分片策略时,会面临需要重新平衡数据的情况。例如,某个分片由于某一区域的用户增长迅速而导致数据量超过预期。为了避免数据超载,我们可能需要更新分片函数,并且将一些数据从一个分片移动到另一个更空闲的分片。一种常见的方法是使用一致性哈希算法,这种算法能够尽可能地减少数据迁移,从而降低影响。
热点键问题
热点键问题指的是某些特定数据或实体集中在一个或少数几个分片上,导致这些分片承受了不均衡的读写负载。例如,在一个电子商务平台中,某些畅销产品的订单数据可能会集中在同一个分片上。这会导致该分片的服务器负载过高,从而影响整体系统的性能。为了解决这个问题,我们可以考虑采用数据预分片策略,将热点数据预先分配到多个分片中,或者使用自动化机制监测并平衡数据分布。
连接和反规范化
一旦数据库被分片到多个服务器上,跨分片执行连接操作变得复杂,因为连接需要在多个分片之间传输数据,增加了系统的复杂性和延迟。为了提高查询效率,常见的解决方案是采用反规范化技术。这意味着在数据库设计时将相关数据冗余存储在单个表中,避免跨分片的连接操作。例如,将用户的基本信息和订单历史记录冗余存储在同一个表中,这样可以在不需要跨分片查询的情况下快速访问所有相关数据。
实际应用与拓展
在实际应用中,分片技术广泛应用于需要处理大量数据和高并发请求的系统中,例如互联网公司的大型应用、金融交易系统以及电子商务平台。通过选择适当的分片键和实施有效的分片策略,可以显著提升系统的扩展性、性能和可用性。
此外,随着NoSQL数据库的发展和普及,例如MongoDB、Cassandra等,这些数据库提供了更灵活的分布式架构和高可扩展性,适合处理非结构化或半结构化数据,从而进一步优化分片和数据管理策略。
在图23中,我们展示了分片数据库以支持迅速增长的数据流量。同时,一些非关系型功能被移至NoSQL数据存储,以减少数据库负载。
总结
对系统进行扩展以支持数百万用户是一个持续迭代的过程,需要不断优化和调整。以下是该博客讨论的关键策略总结:
保持Web层无状态化: 将会话数据和其他状态信息存储在共享数据存储中(如数据库或NoSQL),使每个Web服务器可以独立处理请求。这确保了可扩展性和容错性。
在每个层级构建冗余: 冗余确保如果一个组件失败,系统中有备用组件可供使用。包括冗余服务器、负载均衡器和跨多个数据中心的数据复制。
尽可能缓存数据: 缓存频繁访问的数据可以减轻数据库负载并提高响应速度。实施读取穿透缓存等策略可以显著提升性能。
支持多个数据中心: 使用GeoDNS路由确保用户被引导至最近的数据中心,减少延迟并提升用户体验。在数据中心故障时,可以重新路由流量以确保服务连续性。
将静态资产托管在CDN中: 内容交付网络(CDN)全球分发静态资产(如图片、CSS和JavaScript文件),减轻原始服务器负载并提高内容传输速度。
通过分片扩展数据层: 根据分片键将数据分区存储在多个数据库中,分布负载并实现水平扩展。有效的分片策略避免热点并确保数据均衡分布。
将层级拆分为独立服务: 将单块架构拆分为微服务允许每个服务独立扩展和部署,促进敏捷性和可扩展性。
监控系统并使用自动化工具: 持续监控性能指标、日志和用户交互,可以提供系统健康状况的洞察,并帮助及早发现问题。自动化工具简化部署、扩展和管理任务,提升效率和可靠性。
通过实施这些策略,您可以构建一个稳健且可扩展的系统,能够支持数百万用户,同时保持性能、可靠性和响应速度。要实现超越此水平的扩展,需要持续优化,应对新挑战,并利用新兴技术保持应用程序在动态环境中的领先地位。恭喜您掌握了这些基础概念!