系统设计(架构师)指南1从零扩展到百万用户
1 从零扩展到百万用户
设计支持数百万用户的系统是一项挑战,是需要不断完善和无止境改进的过程。在本章中,我们将构建一个支持单个用户的系统,并逐步将其扩展到为数百万用户提供服务。
1.1 单服务器设置
下图展示了单服务器设置的示意图,其中所有内容都运行在一台服务器上:网络应用程序、数据库、缓存等。
请求流:
-
用户通过域名访问网站,如 api.mysite.com。通常,域名系统(DNS)是由第三方提供的付费服务,并非由我们的服务器托管。
-
互联网协议 (IP)地址会返回给浏览器或移动应用程序。在本例中,将返回IP地址15.125.23.214。
-
获得IP地址后,超文本传输协议(HTTP)请求将直接发送到您的网络服务器。
-
网络服务器返回HTML页面或JSON响应,以供呈现。
网络服务器的流量来自两个方面:Web应用程序和移动应用程序。
-
Web应用:使用服务器端语言(Python、Go、Java、PHP、Perl、Node.js等)处理业务逻辑和存储等,并使用客户端语言(HTML和JavaScript)进行呈现。
-
移动应用程序: HTTP协议是移动应用程序与网络服务器之间的通信协议。JavaScript Object Notation(JSON)是常用的API响应格式,因其简单易用,可用于传输数据。
1.2 数据库
随着用户群的增长,一台服务器是不够的,我们需要多台服务器:一台用于Web/移动流量,另一台用于数据库。将网络/移动流量(网络层)和数据库(数据层)服务器分开,可以使它们独立扩展。
您可以选择传统的关系数据库或非关系数据库。让我们来看看它们的区别。
关系数据库也称为关系数据库管理系统(RDBMS)或 SQL 数据库。最流行的数据库有PostgreSQL、MySQL、Oracle等。关系数据库以表格和行来表示和存储数据。您可以使用SQL跨不同的数据库表执行连接操作。
1.2.1 选择数据库
非关系型数据库也称为NoSQL数据库。常用的数据库有MongoDB、CouchDB、Neo4j、Cassandra、HBase、Amazon DynamoDB 等。 这些数据库分为四类:键值存储、图存储、列存储和文档存储。
对于大多数开发人员来说,关系数据库是最好的选择,因为关系数据库已经存在了 40 多年,历史上一直运行良好。但是,如果关系数据库不适合您的特定用例,就必须探索关系数据库以外的其他数据库。在以下情况下,非关系型数据库可能是正确的选择:
- 要超低延迟。
- 数据是非结构化的,或者您没有任何关系型数据。
- 只需要序列化和反序列化数据(JSON、XML、YAML 等)。
- 存储大量数据。
1.3 垂直扩展与水平扩展
垂直扩展在服务器上增加更多功率(CPU、内存等)的过程。水平扩展被称为 "向外扩展",通过在资源池中添加更多服务器来实现扩展。
当流量较低时,垂直扩展是不错的选择,垂直扩展的简单性是其主要优势。遗憾的是,它也有严重的局限性。
- 垂直扩展有硬限制。不可能在一台服务器上无限增加CPU和内存。
- 垂直扩展不具备故障切换和冗余功能。如果一台服务器宕机,网站/应用程序也会随之完全宕机。
由于垂直扩展的局限性,对于大型应用程序来说,水平扩展更为理想。
在前面的设计中,用户直接连接到网络服务器。如果网络服务器离线,用户将无法访问网站。在另一种情况下,如果许多用户同时访问网络服务器,并达到网络服务器的负载极限,用户一般会体验到较慢的响应或无法连接到服务器。负载平衡器是解决这些问题的最佳技术。
1.4 负载均衡
负载均衡在负载平衡集定义的网络服务器之间平均分配传入流量。
用户直接连接到负载均衡。在这种设置下,客户端再也无法直接访问网络服务器。为了提高安全性,服务器之间的通信使用专网。
- 如果服务器1离线,所有流量将被路由到服务器2。这可以防止网站离线。
- 如果流量迅速增长,两台服务器不足以处理流。只需添加更多服务器,负载平衡器就会自动开始向它们发送请求。
1.5 数据库复制
数据库复制可用于许多数据库管理系统,通常在原始数据库(主数据库)和副本(从数据库)之间存在主从关系。
主数据库通常只支持写操作。从数据库从主数据库获取数据副本,只支持读操作。所有修改数据的命令(如插入、删除或更新)都必须发送到主数据库。大多数应用程序要求的读取和写入比例要高得多;因此,系统中从数据库的数量通常大于主数据库的数量。
数据库复制的优势:
-更好的性能: 在主从模式中,所有写入和更新操作都在主节点上进行;而读取操作则分布在从节点上进行。这种模式可以并行处理更多查询,因此可以提高性能。
-可靠性: 如果您的一台数据库服务器因台风或地震等自然灾害而毁坏,数据仍会保留。您不必担心数据丢失,因为数据是在多个地点复制的。
-高可用性: 通过在不同地点复制数据,即使数据库离线,您的网站也能继续运行,因为您可以访问存储在另一个数据库服务器中的数据。
如果其中一个数据库离线了怎么办?
-
如果只有一个从数据库可用,但该数据库离线,读取操作将暂时定向到主数据库。一旦发现问题,新的从数据库将取代旧的从数据库。如果有多个从数据库可用,读取操作将重定向到其他健康的从数据库。新的数据库服务器将取代旧的数据库服务器。
-
如果主数据库离线,从数据库将被提升为新的主数据库。所有数据库操作都将在新的主数据库上临时执行。新的从数据库将立即取代旧的从数据库进行数据复制。在生产系统中,推广新的主数据库更为复杂,因为从数据库中的数据可能不是最新的。需要通过运行数据恢复脚本来更新缺失的数据。虽然其他一些复制方法,如多主复制和循环复制也有帮助,但这些设置更为复杂,而且其讨论超出了本书的范围。
-
用户从DNS获取负载均衡器的IP
-
用户使用该IP连接负载均衡器
-
HTTP请求被路由到服务器1或服务器2。
-
Web服务器从从数据库中读取用户数据。
-
Web服务器将任何数据修改操作路由至主数据库。这包括写入、更新和删除操作。
1.6 缓存
缓存是一个临时存储区域,用于将昂贵的响应结果或频繁访问的数据存储在内存中,以便更快地处理后续请求。每次加载新网页时,都会执行一次或多次数据库调用来获取数据。重复调用数据库会极大地影响应用程序的性能。缓存可以缓解这一问题。
缓存是临时数据存储层,速度比数据库快得多。拥有独立缓存层的好处包括:更好的系统性能、减少数据库工作负载的能力以及独立扩展缓存层的能力。上图显示了缓存服务器的可能设置:
网络服务器收到请求后,首先会检查缓存中是否有可用的响应。如果有,则将数据发送回客户端。如果没有,它就会查询数据库,将响应存储在缓存中,然后发送回客户端。这种缓存策略称为直读缓存。根据数据类型、大小和访问模式的不同,还有其他缓存策略可供选择。
与缓存服务器交互非常简单,因为大多数缓存服务器都提供了适用于常见编程语言的API。下面的代码片段展示了典型的 Memcached API:
1.6.1 使用缓存的注意事项
以下是使用缓存系统的一些注意事项:
-
决定何时使用缓存。当数据读取频繁但修改不频繁时,考虑使用缓存。由于缓存数据存储在易失性内存中,因此缓存服务器不是持久化数据的理想选择。例如,如果缓存服务器重新启动,内存中的所有数据都会丢失。因此,重要数据应保存在持久化数据存储中。
-
过期策略。实施过期策略是一种很好的做法。缓存数据一旦过期,就会从缓存中删除。如果没有过期策略,缓存数据将永久保存在内存中。过期日期最好不要太短,因为这会导致系统过于频繁地从数据库中重新加载数据。同时,过期日期也不宜过长,否则数据会变得陈旧。
-
一致性: 这包括保持数据存储和缓存同步。由于数据存储和缓存上的数据修改操作不是在单个事务中进行的,因此可能会出现不一致。在跨多个区域扩展时,保持数据存储和高速缓存之间的一致性具有挑战性。更多详情,请参阅 Facebook 发布的题为 "Scaling Memcache at Facebook "的论文。
-
缓解故障: 单个缓存服务器代表一个潜在的单点故障(SPOF single point of failure),维基百科的定义如下: "单点故障(SPOF)是系统中的一个部分,如果它发生故障,整个系统将停止工作"。因此,为避免单点故障,建议在不同的数据中心安装多个高速缓存服务器。另一种推荐方法是按一定比例超额配置所需的内存。这可以在内存使用量增加时提供缓冲。
- 禁用策略: 一旦缓存已满,任何向缓存添加项目的请求都可能导致现有项目被删除。这就是所谓的缓存驱逐。最近最少使用(LRU Least-recently-used)是最流行的缓存驱逐策略。还可以采用其他驱逐策略,如最不常用(LFU)或先进先出(FIFO First in First Out),以满足不同的使用情况。
1.7 CDN
CDN是一个由地理位置分散的服务器组成的网络,用于传输静态内容。CDN 服务器缓存图片、视频、CSS、JavaScript 文件等静态内容。
动态内容缓存是一个相对较新的概念,超出了本书的讨论范围。它可以根据请求路径、查询字符串、cookie 和请求标头缓存 HTML页面。
以下是CDN的高级工作原理:当用户访问网站时,离用户最近的CDN服务器将传输静态内容。直观地说,用户离CDN服务器越远,网站加载速度就越慢。例如,如果CDN服务器位于旧金山,那么洛杉矶的用户获取内容的速度就会比欧洲的用户快。
工作流:
-
用户尝试使用图片URL获取image.png。URL的域名由CDN提供商提供。
-
如果CDN服务器的缓存中没有image.png,CDN服务器就会从原点请求文件,原点可以是网络服务器或在线存储(如 Amazon S3)。
-
源服务器将image.png返回给CDN服务器,其中包括可选的HTTP标头Time-to-Live (TTL),用于描述图像的缓存时间。
-
CDN缓存图像并将其返回给用户A。
-
用户B发送请求获取相同的图像。
-
只要TTL未过期,图像就会从缓存中返回。
1.7.1 使用CDN的注意事项
-
成本: CDN由第三方提供商运行,您需要为进出CDN的数据传输付费。缓存不经常使用的资产不会带来明显的好处,因此应考虑将它们移出CDN。
-
设置适当的缓存到期时间: 对于时间敏感的内容,设置缓存过期时间非常重要。缓存过期时间既不能太长,也不能太短。如果时间过长,内容可能不再新鲜。如果时间太短,则可能导致重复重新加载从原始服务器到 CDN 的内容。
-
CDN回退: 应考虑网站/应用程序如何应对CDN故障。如果CDN出现暂时中断,客户端应能检测到问题并从源服务器请求资源。
-
无效文件: 您可以通过执行以下操作之一,在文件过期前将其从 CDN 中删除:
- 使用CDN供应商提供的API使CDN对象失效。
- 使用对象版本控制提供不同版本的对象。要对对象进行版本控制,可以在URL中添加一个参数,如版本号。例如,在查询字符串中添加版本号2:image.png?v=2。
- 静态资产(JS、CSS、图片等)不再由Web服务器提供。它们从CDN抓取,以获得更好的性能。
- 通过缓存数据减轻数据库负载。
1.8 无状态Web层
将会话数据存储在关系数据库或NoSQL等持久存储中。集群中的每个网络服务器都可以访问数据库中的状态数据。这就是所谓的无状态Web层。
1.8.1 有状态架构
有状态服务器和无状态服务器有一些主要区别。有状态服务器从一个请求到下一个请求都会记住客户端数据(状态)。无状态服务器不保存任何状态信息。
用户A的会话数据和个人资料图像存储在服务器1中。要对用户A进行身份验证,HTTP请求必须路由到服务器1。如果请求被发送到其他服务器(如服务器2),身份验证就会失败,因为服务器2不包含用户A的会话数据。同样,用户B的所有 HTTP 请求都必须发送到服务器2;用户C的所有请求都必须发送到服务器3。
来自同一个客户端的每个请求都必须路由到同一个服务器。这可以通过大多数负载均衡器中的粘性会话来实现,但这会增加开销。使用这种方法添加或删除服务器要困难得多。处理服务器故障也很有挑战性。
1.8.2 无状态架构
在这种无状态架构中,用户的HTTP请求可以发送到任何网络服务器,由服务器从共享数据存储区获取状态数据。状态数据存储在共享数据存储区,不与网络服务器相连。无状态系统更简单、更健壮、可扩展。
我们将会话数据移出网络层,并将其存储在持久化数据存储区中。共享数据存储可以是关系数据库、Memcached/Redis、NoSQL 等。之所以选择NoSQL数据存储,是因为它易于扩展。自动扩展是指根据流量负载自动添加或移除网络服务器。从网络服务器中移除状态数据后,根据流量负载添加或移除服务器,就能轻松实现网络层的自动扩展。
参考资料
- 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
- 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
- python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
- Linux精品书籍下载 https://www.cnblogs.com/testing-/p/17438558.html
- https://www.drawio.com/doc/faq/
- Hypertext Transfer Protocol: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
- Should you go Beyond Relational Databases?:
https://blog.teamtreehouse.com/should-you-go-beyond-relational-databases - Replication: https://en.wikipedia.org/wiki/Replication_(computing)
- Multi-master replication:
https://en.wikipedia.org/wiki/Multi-master_replication - NDB Cluster Replication: Multi-Master and Circular Replication:
https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster-replication-multi-master.html - Caching Strategies and How to Choose the Right One:
https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/ - R. Nishtala, "Facebook, Scaling Memcache at," 10th USENIX Symposium on Networked
Systems Design and Implementation (NSDI ’13). - Single point of failure: https://en.wikipedia.org/wiki/Single_point_of_failure
- Amazon CloudFront Dynamic Content Delivery:
https://aws.amazon.com/cloudfront/dynamic-content/ - Configure Sticky Sessions for Your Classic Load Balancer:
https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-sticky-sessions.html - Active-Active for Multi-Regional Resiliency:
https://netflixtechblog.com/active-active-for-multi-regional-resiliency-c47719f6685b - Amazon EC2 High Memory Instances:
https://aws.amazon.com/ec2/instance-types/high-memory/ - What it takes to run Stack Overflow:
http://nickcraver.com/blog/2013/11/22/what-it-takes-to-run-stack-overflow - What The Heck Are You Actually Using NoSQL For:
http://highscalability.com/blog/2010/12/6/what-the-heck-are-you-actually-using-nosql-
for.html
1.9 数据中心
下图显示了两个数据中心的设置示例。在正常运行时,用户会被geoDNS-routed(也称为地理路由到最近的数据中心,其中大明东部的流量占x%,大明西部的流量占(100 - x)%。地理DNS是一种DNS服务,可根据用户的位置将域名解析为IP地址。
如果数据中心发生重大故障,我们会将所有流量引导到健康的数据中心。
要实现多数据中心设置,必须解决几个技术难题:
- 流量重定向
需要有效的工具将流量导向正确的数据中心。可使用GeoDNS根据用户所在位置将流量导向最近的数据中心。
- 数据同步
不同地区的用户可能使用不同的本地数据库或缓存。在故障切换情况下,流量可能会被路由到数据不可用的数据中心。一种常见的策略是在多个数据中心之间复制数据。
- 测试和部署
通过多数据中心设置,在不同地点测试网站/应用程序非常重要。自动部署工具对于在所有数据中心保持服务一致性至关重要 。
为了进一步扩展我们的系统,我们需要将系统的不同组件解耦,以便它们可以独立扩展。消息队列是现实世界中许多分布式系统解决这一问题的关键策略。
1.10 消息队列
消息队列是一个存储在内存中的持久组件,支持异步通信。它充当缓冲器并分发异步请求。消息队列的基本架构很简单。被称为生产者/发布者的输入服务创建消息,并将其发布到消息队列。其他服务或服务器(称为消费者/订阅者)连接到队列,并执行消息定义的操作。
解耦使消息队列成为构建可扩展和可靠应用程序的首选架构。有了消息队列,当消费者无法处理消息时,生产者可以将消息发布到队列中。即使生产者不可用,消费者也能从队列中读取消息。
考虑以下用例:您的应用程序支持照片定制,包括裁剪、锐化、模糊等。这些定制任务需要时间来完成。下图中,网络服务器将照片处理任务发布到消息队列。照片处理工作者从消息队列中拾取任务,并异步执行照片定制任务。生产者和消费者可以独立扩展。当队列规模变大时,就会增加更多的工人来缩短处理时间。但是,如果队列大部分时间是空的,则可以减少工人数量。
1.11 日志、指标、自动化
在几个服务器上运行小型网站时,日志记录、指标和自动化支持是很好的做法,但并非必需。但是,现在您的网站已经发展成为一个大型企业,投资这些工具就显得非常必要了。
- 日志记录
监控错误日志非常重要,因为它有助于识别系统中的错误和问题。您可以监控每个服务器级别的错误日志,也可以使用工具将它们汇总到一个集中服务中,以便于搜索和查看。
-
指标: 收集不同类型的指标有助于我们获得业务洞察力并了解系统的健康状况。以下一些指标非常有用:
- 主机级指标: CPU、内存、磁盘 I/O 等。
- 聚合级指标:例如,整个数据库层、缓存层等的性能。
- 关键业务指标:日活跃用户、留存率、收入等。
-
自动化
当系统变得庞大而复杂时,我们需要构建或利用自动化工具来提高生产率。持续集成是一种很好的做法,它通过自动化验证每个代码的签入,使团队能够及早发现问题。此外,将构建、测试和部署流程等自动化也能显著提高开发人员的工作效率。
- 该设计包含一个消息队列,有助于使系统更加松散耦合和具有故障弹性。
- 包括日志、监控、度量和自动化工具。
1.12 数据库扩展
数据库扩展有两大方法:垂直扩展和水平扩展。
1.12.1 垂直扩展
纵向扩展,也称为向上扩展,是指在现有机器上增加更多功率(CPU、RAM、DISK 等)。有一些功能强大的数据库服务器。根据亚马逊关系数据库服务(RDS),你可以获得一个拥有24TB内存的数据库服务器。这种强大的数据库服务器可以存储和处理大量数据。例如,stackoverflow.com 在2013年的月独立访客数超过1000万,但它只有一个主数据库。不过,纵向扩展也有一些严重的缺点:
- 您可以为数据库服务器增加更多的 CPU、内存等,但这是有硬件限制的。如果用户基数大,单个服务器是不够的。
- 单点故障风险更大。
- 垂直扩展的总体成本较高。功能强大的服务器成本更高。
1.12.2 横向扩展
水平扩展也称为分片,是增加更多服务器的做法。
分片将大型数据库分成更小、更易于管理的部分,称为分片。每个分片共享相同的模式,但每个分片上的实际数据都是独一无二的。
下图显示了分片数据库的示例。用户数据是根据用户ID分配给数据库服务器的。每次访问数据时,都会使用哈希函数找到相应的分片。在我们的示例中,user_id % 4被用作散列函数。如果结果等于0,则0分区用于存储和获取数据。如果结果等于1,则使用分区1。同样的逻辑也适用于其他分区。
实施分片策略时需要考虑的最重要因素是分片键的选择。分片键(称为分区键)由一列或多列组成,决定了数据的分配方式。下图"user_id "就是分片键。通过分片键,可以将数据库查询路由到正确的数据库,从而有效地检索和修改数据。在选择分片键时,最重要的标准之一是选择一个能均匀分布数据的键。
分片是一种扩展数据库的好技术,但远非完美的解决方案。它给系统带来了复杂性和新的挑战:
-
数据重分片: 当出现以下情况时,需要对数据进行重新分片:1)由于数据增长过快,单个分片无法再容纳更多数据。2) 由于数据分布不均,某些分区可能比其他分区更快出现分区耗尽。当分片耗尽时,需要更新分片功能并移动数据。第5章将讨论的一致性散列是解决这一问题的常用技术。
-
名人问题:这也叫热点密钥问题。对特定分片的过度访问会导致服务器超载。试想一下,凯蒂-佩里、贾斯汀-比伯和 Lady Gaga 的数据都在同一分片上。对于社交应用来说,读取操作将使该分区不堪重负。为了解决这个问题,我们可能需要为每个名人分配一个分区。每个分区甚至可能需要进一步划分。
-
连接和去规范化: 一旦数据库被分到多个服务器上,就很难在数据库分片之间执行连接操作。常见的解决方法是去规范化数据库,以便在单个表中执行查询。
我们对数据库进行分片,以支持快速增长的数据流量。同时,一些非关系型功能被转移到NoSQL数据存储中,以减少数据库负载。
1.13 更多用户
扩展系统是一个迭代过程。迭代本章所学的内容可以让我们走得更远。要扩展到数百万用户以上,还需要更多的微调和新策略。例如,你可能需要优化系统并将系统解耦为更小的服务。本章所学的所有技术应能为应对新挑战打下良好基础。在本章的最后,我们总结了如何扩展系统以支持数百万用户:
- 保持网络层无状态
- 在每个层建立冗余
- 尽可能多地缓存数据
- 支持多个数据中心
- 在CDN中托管静态资产
- 通过分片扩展数据层
- 将层级拆分为单个服务
- 监控系统并使用自动化工具