第一章:从零扩展到百万用户 (Scale From Zero To Millions Of Users)
设计一个支持数百万用户的系统极具挑战性,这是一段需要不断优化和持续改进的旅程。在本章中,我们将构建一个最初支持单个用户的系统,并逐步扩展它以服务数百万用户。读完本章后,您将掌握一系列技巧,这些技巧将帮助您破解系统设计面试中的问题。
单服务器架构 (Single server setup)
千里之行,始于足下,构建复杂系统也是如此。为了从简单的开始,所有内容都运行在一台服务器上。图1-1展示了单服务器架构的示意图,其中所有内容都运行在一台服务器上:Web应用、数据库、缓存等。
为了理解这种架构,研究请求流程和流量来源是有帮助的。让我们首先来看一下请求流程(图1-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服务器之间的通信协议。由于JSON格式的简单性,它通常被用作API响应格式。以下是一个JSON格式的API响应示例:
GET /users/12 – Retrieve user object for id = 12
{
"id": 12,
"firstName": "John",
"lastName": "Smith",
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": 10021
},
"phoneNumbers": [
"212 555-1234",
"646 555-4567"
]
}
数据库 (Database)
随着用户量的增长,一台服务器已不足以支撑需求,我们需要多台服务器:一台用于处理Web/移动端流量,另一台用于数据库(如图1-3所示)。将Web/移动端流量(Web层)与数据库(数据层)服务器分离,允许它们独立扩展。
使用哪种数据库?
您可以选择传统的关系型数据库或非关系型数据库。让我们来看看它们的区别。
关系型数据库也称为关系型数据库管理系统(RDBMS)或SQL数据库。最流行的有MySQL、Oracle数据库、PostgreSQL等。关系型数据库以表和行的形式表示和存储数据。您可以使用SQL在不同的数据库表之间执行连接操作。
非关系型数据库也称为NoSQL数据库。流行的有CouchDB、Neo4j、Cassandra、HBase、Amazon DynamoDB等。这些数据库分为四类:键值存储、图存储、列存储和文档存储。非关系型数据库通常不支持连接操作。
对于大多数开发者来说,关系型数据库是最佳选择,因为它们已经存在了40多年,并且在历史上表现良好。然而,如果关系型数据库不适合您的具体用例,那么探索关系型数据库之外的选项是至关重要的。如果您的应用程序需要以下条件,非关系型数据库可能是正确的选择:
- 您的应用程序需要超低延迟。
- 您的数据是非结构化的,或者您没有任何关系型数据。
- 您只需要序列化和反序列化数据(JSON、XML、YAML等)。
- 您需要存储海量数据。
垂直扩展与水平扩展
垂直扩展,通常称为“向上扩展”,指的是通过为服务器增加更多的计算资源(如CPU、内存等)来提升其性能。水平扩展,也称为“向外扩展”,则是通过增加更多的服务器来扩展资源池的规模。
当流量较低时,垂直扩展是一个不错的选择,其优势在于简单。然而,垂直扩展也存在明显的局限性:
- 垂直扩展有硬性上限。无法无限制地向单台服务器添加CPU和内存。
- 垂直扩展缺乏故障转移和冗余机制。如果服务器宕机,网站或应用将完全不可用。
- 由于垂直扩展的这些限制,水平扩展对于大规模应用更为理想。
在前面的设计中,用户是直接连接到Web服务器的。如果Web服务器离线,用户将无法访问网站。另一个场景是,当许多用户同时访问Web服务器并且达到服务器的负载上限时,用户通常会遇到响应变慢或无法连接的情况。负载均衡是解决这些问题的最佳技术手段。
负载均衡器 (Load balancer)
负载均衡器将传入的流量均匀地分配给负载均衡集中的Web服务器。如图1-4所示,展示了负载均衡器的工作原理。
如图1-4所示,用户直接连接到负载均衡器的公共IP地址。通过这种设置,Web服务器不再直接对客户端可达。为了增强安全性,服务器之间使用私有IP进行通信。私有IP是仅在同一网络中的服务器之间可达的IP地址,无法在互联网上访问。负载均衡器通过私有IP与Web服务器进行通信。
在图1-4中,添加了负载均衡器和第二台Web服务器后,我们成功解决了故障转移问题,并提高了Web层的可用性。具体细节如下:
- 如果服务器1宕机,所有流量将被路由到服务器2,从而避免网站完全不可用。我们还将向服务器池中添加一台新的健康Web服务器,以平衡负载。
- 如果网站流量快速增长,而两台服务器无法处理这些流量,负载均衡器可以优雅地处理此问题。只需将更多服务器添加到Web服务器池中,负载均衡器将自动开始将请求发送到这些服务器。
现在Web层的设计看起来不错,那么数据层呢?当前的设计只有一个数据库,因此不支持故障转移和冗余。数据库复制是一种常用的技术,可以解决这些问题。让我们来看看。
数据库复制 (Database replication)
引用自维基百科:“数据库复制可以用于许多数据库管理系统,通常在原始数据库(主数据库)和副本(从数据库)之间存在主/从关系”[3]。
主数据库通常仅支持写操作。从数据库从主数据库获取数据副本,仅支持读操作。所有修改数据的命令,如插入、删除或更新,必须发送到主数据库。大多数应用程序要求读取与写入的比例更高;因此,系统中的从数据库数量通常大于主数据库的数量。图1-5展示了一个主数据库与多个从数据库的关系。
数据库复制的优点:
更好的性能:在主从模型中,所有的写入和更新操作发生在主节点上,而读取操作则分布在从节点上。该模型提高了性能,因为它允许更多的查询同时处理。
可靠性:如果您的某个数据库服务器因自然灾害(例如台风或地震)而被摧毁,数据仍然可以得到保护。您不必担心数据丢失,因为数据会复制到多个位置。
高可用性:通过在不同位置复制数据,即使某个数据库离线,您的网站仍然可以正常运行,因为您可以访问存储在其他数据库服务器中的数据。
在前面的部分,我们讨论了负载均衡器如何帮助提高系统可用性。我们在这里提出同样的问题:如果其中一个数据库离线怎么办?图1-5中讨论的架构设计可以处理这种情况:
如果只有一个从数据库可用,而它离线,读取操作将临时指向主数据库。一旦发现问题,将用一个新的从数据库替换旧的。如果有多个从数据库可用,读取操作将重定向到其他健康的从数据库,并用新的数据库服务器替换旧的。
如果主数据库离线,则将提升一个从数据库成为新的主数据库。所有数据库操作将暂时在新的主数据库上执行。将立即用一个新的从数据库替换旧的,从而实现数据复制。
在生产系统中,提升新的主数据库更为复杂,因为从数据库中的数据可能不是最新的。缺失的数据需要通过运行数据恢复脚本来更新。虽然一些其他的复制方法,如多主复制和循环复制,可以提供帮助,但这些设置更加复杂,讨论超出了本书的范围。有兴趣的读者应参考所列的参考文献[4][5]。
图1-6展示了在添加负载均衡器和数据库复制后系统的设计。
让我们来看看这个设计:
- 用户通过 DNS 获取负载均衡器的 IP 地址。
- 用户使用该 IP 地址连接到负载均衡器。
- HTTP 请求被路由到服务器 1 或服务器 2。
- Web 服务器从从数据库中读取用户数据。
- Web 服务器将任何数据修改操作路由到主数据库。这包括写入、更新和删除操作。
现在,您对 Web 和数据层有了扎实的理解,是时候改善负载/响应时间了。这可以通过添加缓存层并将静态内容(JavaScript/CSS/图像/视频文件)转移到内容分发网络(CDN)来实现。
缓存 (Cache)
缓存是一个临时存储区域,用于存储昂贵响应的结果或频繁访问的数据,以便后续请求能够更快地得到服务。如图 1-6 所示,每次加载新网页时,都会执行一个或多个数据库调用以获取数据。重复调用数据库会大大影响应用程序的性能。缓存可以缓解这个问题。
缓存层 (Cache tier)
缓存层是一个临时数据存储层,其速度远快于数据库。设置单独的缓存层的好处包括提升系统性能、减少数据库负载以及能够独立扩展缓存层。图 1-7 显示了缓存服务器的一个可能配置:
在接收到请求后,web 服务器首先检查缓存中是否有可用的响应。如果有,服务器将数据发送回客户端。如果没有,服务器会查询数据库,将响应存储到缓存中,然后再发送回客户端。这种缓存策略称为直读缓存(read-through cache)。根据数据类型、大小和访问模式,还有其他缓存策略可供选择。之前的研究对不同缓存策略的工作原理进行了详细解释[6]。
与缓存服务器的交互非常简单,因为大多数缓存服务器为常见编程语言提供了 API。以下代码片段展示了典型的 Memcached API:
SECONDS = 1
cache.set('myKey', 'hi there', 3600 * SECONDS)
cache.get('myKey')
使用缓存时的注意事项
- 决定何时使用缓存:当数据频繁读取但不经常修改时,考虑使用缓存。由于缓存数据存储在易失性内存中,缓存服务器并不适合持久化数据。例如,如果缓存服务器重新启动,内存中的所有数据都将丢失。因此,重要数据应保存在持久数据存储中。
- 过期策略:实施过期策略是一个良好的实践。一旦缓存数据过期,它将从缓存中删除。当没有过期策略时,缓存数据将永久存储在内存中。建议不要将过期时间设置得太短,因为这会导致系统频繁地从数据库重新加载数据。同时,也不应将过期时间设置得太长,因为数据可能会变得过时。
- 一致性:这涉及到保持数据存储和缓存之间的同步。由于对数据存储和缓存的数据修改操作不是在一个事务中进行的,因此可能会出现不一致。在跨多个区域扩展时,保持数据存储和缓存之间的一致性是具有挑战性的。有关更多细节,请参阅 Facebook 发布的论文《Scaling Memcache at Facebook》[7]。
- 减轻故障:单个缓存服务器代表潜在的单点故障(SPOF),维基百科对其定义如下:“单点故障(SPOF)是指系统的一个部分,如果它发生故障,将使整个系统停止工作”[8]。因此,建议在不同数据中心部署多个缓存服务器以避免 SPOF。另一个推荐的方法是按一定比例超配所需内存。这可以在内存使用增加时提供缓冲。
- 驱逐策略:一旦缓存满了,任何添加新项目到缓存的请求可能会导致现有项目被移除。这被称为缓存驱逐。最常用的缓存驱逐策略是最近最少使用(LRU)。其他驱逐策略,如最不常用(LFU)或先进先出(FIFO),可以根据不同的使用场景进行采用。
内容分发网络 - CDN (Content delivery network - CDN)
CDN 是一个地理分散的服务器网络,用于交付静态内容。CDN 服务器缓存静态内容,如图像、视频、CSS、JavaScript 文件等。
动态内容缓存是一个相对较新的概念,超出了本书的范围。它使基于请求路径、查询字符串、Cookie 和请求头的 HTML 页面能够被缓存。有关更多信息,请参考参考文献[9]中提到的文章。本书主要集中在如何使用 CDN 缓存静态内容。
CDN 的高层工作原理如下:当用户访问网站时,离用户最近的 CDN 服务器将提供静态内容。直观地说,用户离 CDN 服务器越远,网站加载的速度就越慢。例如,如果 CDN 服务器位于旧金山,那么洛杉矶的用户获取内容的速度会比欧洲的用户快。图 1-9 是一个很好的例子,展示了 CDN 如何改善加载时间。
图 1-10 展示了 CDN 工作流程。
- 用户 A 尝试通过图像 URL 获取 image.png。该 URL 的域名由 CDN 提供商提供。以下两个图像 URL 是示例,用于演示在 Amazon 和 Akamai CDN 上的图像 URL 格式:
- 如果 CDN 服务器的缓存中没有 image.png,CDN 服务器会向源服务器请求该文件,源服务器可以是一个网络服务器或类似 Amazon S3 的在线存储。
- 源服务器将 image.png 返回给 CDN 服务器,并附带可选的 HTTP 头部 Time-to-Live (TTL),该头部描述了图像缓存的时长。
- CDN 缓存图像并将其返回给用户 A。图像将在 CDN 中缓存,直到 TTL 过期。
- 用户 B 发送请求以获取相同的图像。
- 只要 TTL 尚未过期,图像就会从缓存中返回。
使用 CDN 的注意事项
成本:CDN 由第三方提供商运行,您会因数据进出 CDN 而产生费用。缓存不常用的资产不会带来显著的好处,因此您应该考虑将其移出 CDN。
设置合适的缓存过期时间:对于时间敏感的内容,设置缓存过期时间非常重要。缓存过期时间不应过长或过短。如果过长,内容可能不再新鲜;如果过短,则可能导致内容频繁从源服务器重新加载到 CDN。
CDN 回退:您应考虑网站/应用如何应对 CDN 故障。如果出现临时的 CDN 中断,客户端应能够检测到问题并向源请求资源。
使文件失效:您可以通过执行以下操作之一在文件过期之前将其从 CDN 中移除:
- 使用 CDN 供应商提供的 API 使 CDN 对象失效。
- 使用对象版本控制提供不同版本的对象。要对对象进行版本控制,可以向 URL 添加一个参数,例如版本号。例如,在查询字符串中添加版本号 2:image.png?v=2。
图 1-11 显示了在添加 CDN 和缓存后的设计。
- 静态资产(如 JS、CSS、图像等)不再由 Web 服务器提供,而是从 CDN 获取,以提高性能。
- 通过缓存数据,数据库的负载得以减轻。
无状态 Web 层 (Stateless web tier)
现在是考虑横向扩展 Web 层的时候了。为此,我们需要将状态(例如用户会话数据)移出 Web 层。一个好的做法是将会话数据存储在持久存储中,如关系数据库或 NoSQL 数据库。集群中的每个 Web 服务器都可以从数据库中访问状态数据。这被称为无状态 Web 层。
有状态架构 (Stateful architecture)
有状态服务器和无状态服务器之间存在一些关键差异。有状态服务器会记住客户端数据(状态),以便在请求之间保持状态。而无状态服务器则不保留任何状态信息。图 1-12 展示了有状态架构的一个示例。
在图 1-12 中,用户 A 的会话数据和头像存储在服务器 1 中。要验证用户 A,HTTP 请求必须路由到服务器 1。如果请求发送到其他服务器,例如服务器 2,身份验证将失败,因为服务器 2 不包含用户 A 的会话数据。同样,用户 B 的所有 HTTP 请求必须路由到服务器 2;用户 C 的所有请求必须发送到服务器 3。
问题在于,来自同一客户端的每个请求都必须路由到相同的服务器。这可以通过大多数负载均衡器中的粘性会话来实现;然而,这增加了额外的开销。采用这种方法,添加或移除服务器变得更加困难。处理服务器故障也是一个挑战。
无状态架构 (Stateless architecture)
图 1-13 显示了无状态架构。
在这种无状态架构中,来自用户的 HTTP 请求可以发送到任何 Web 服务器,这些服务器从共享数据存储中获取状态数据。状态数据存储在共享数据存储中,避免存储在 Web 服务器中。无状态系统更简单、更可靠且可扩展。
图 1-14 显示了带有无状态 Web 层的更新设计。
在图 1-14 中,我们将会话数据移出 Web 层并存储在持久性数据存储中。共享数据存储可以是关系数据库、Memcached/Redis、NoSQL 等。选择 NoSQL 数据存储是因为它易于扩展。自动扩展意味着根据流量负载自动添加或删除 Web 服务器。在将状态数据移出 Web 服务器后,Web 层的自动扩展可以通过根据流量负载添加或删除服务器来轻松实现。
您的网站迅速增长,吸引了大量国际用户。为了提高可用性并在更广泛的地理区域提供更好的用户体验,支持多个数据中心至关重要。
数据中心 (Data centers)
图 1-15 显示了一个包含两个数据中心的示例设置。在正常操作中,用户通过 geoDNS 路由,也称为地理路由,连接到最近的数据中心,流量分配为 x% 在美东和 (100 - x)% 在美西。geoDNS 是一种 DNS 服务,允许根据用户的地理位置将域名解析为 IP 地址。
在任何重大数据中心故障的情况下,我们会将所有流量指向一个健康的数据中心。在图 1-16 中,数据中心 2(美西)处于离线状态,100% 的流量被路由到数据中心 1(美东)。
要实现多数据中心设置,必须解决几个技术挑战:
流量重定向:需要有效的工具将流量引导到正确的数据中心。可以使用 GeoDNS 根据用户的位置将流量导向最近的数据中心。
数据同步:来自不同地区的用户可能使用不同的本地数据库或缓存。在故障转移的情况下,流量可能会被路由到一个数据不可用的数据中心。一种常见的策略是在多个数据中心之间复制数据。之前的一项研究展示了 Netflix 如何实现异步多数据中心复制[11]。
测试和部署:在多数据中心设置中,重要的是在不同的位置测试你的网页/应用程序。自动化部署工具对于确保所有数据中心的服务一致性至关重要[11]。
为了进一步扩展我们的系统,我们需要解耦系统的不同组件,以便它们可以独立扩展。消息队列是许多现实世界分布式系统采用的关键策略来解决这个问题。
消息队列 (Message queue)
消息队列是一种持久化组件,存储在内存中,支持异步通信。它作为缓冲区分发异步请求。消息队列的基本架构很简单。输入服务(称为生产者/发布者)创建消息,并将其发布到消息队列中。其他服务或服务器(称为消费者/订阅者)连接到队列,并执行消息中定义的操作。该模型如图 1-17 所示。
解耦使消息队列成为构建可扩展和可靠应用程序的首选架构。通过消息队列,当消费者无法处理时,生产者可以将消息发布到队列中。即使生产者不可用,消费者也可以从队列中读取消息。
考虑以下用例:您的应用程序支持照片定制,包括裁剪、锐化、模糊等。这些定制任务需要时间来完成。在图 1-18 中,Web 服务器将照片处理作业发布到消息队列。照片处理工作者从消息队列中获取作业,并异步执行照片定制任务。生产者和消费者可以独立扩展。当队列的大小变大时,可以添加更多的工作者以减少处理时间。但是,如果队列大多数时间为空,则可以减少工作者的数量。
日志记录、指标、自动化 (Logging, metrics, automation)
当一个小型网站运行在几台服务器上时,日志记录、指标和自动化支持是良好的实践,但并不是必需的。然而,随着网站的发展以服务于大型企业,投资于这些工具就显得至关重要。
日志记录:监控错误日志非常重要,因为它有助于识别系统中的错误和问题。您可以在每台服务器级别监控错误日志,或者使用工具将它们聚合到集中式服务中,以便于搜索和查看。
指标:收集不同类型的指标有助于我们获得业务洞察,并了解系统的健康状态。以下一些指标非常有用:
- 主机级别指标:CPU、内存、磁盘 I/O 等。
- 聚合级别指标:例如,整个数据库层、缓存层的性能等。
- 关键业务指标:每日活跃用户、留存率、收入等。
自动化:当系统变得庞大和复杂时,我们需要构建或利用自动化工具来提高生产力。持续集成是一种良好的实践,每次代码提交都会通过自动化进行验证,使团队能够及早发现问题。此外,自动化构建、测试、部署过程等可以显著提高开发人员的生产力。
添加消息队列和不同的工具
图 1-19 显示了更新后的设计。由于空间限制,图中仅显示了一个数据中心。
- 该设计包括一个消息队列,帮助使系统更加松散耦合并具有故障恢复能力。
- 包括了日志记录、监控、指标和自动化工具。
随着数据每天不断增长,您的数据库变得越来越繁忙。是时候扩展数据层了。
数据库扩展
数据库扩展有两种广泛的方法:垂直扩展和水平扩展。
垂直扩展
垂直扩展,也称为向上扩展,是通过为现有机器添加更多的计算能力(CPU、RAM、磁盘等)来进行扩展。有一些强大的数据库服务器。例如,根据亚马逊关系数据库服务(RDS)[12],您可以获得一台拥有24TB RAM的数据库服务器。这种强大的数据库服务器能够存储和处理大量数据。例如,stackoverflow.com在2013年拥有超过1000万的月独立访客,但它只有一个主数据库[13]。然而,垂直扩展存在一些严重的缺点:
- 您可以为数据库服务器添加更多的CPU、RAM等,但硬件是有限的。如果您的用户基础很大,单台服务器就不够用了。
- 存在更高的单点故障风险。
- 垂直扩展的整体成本较高。强大的服务器价格昂贵。
水平扩展
水平扩展,也称为分片,是指增加更多服务器的做法。图1-20对比了垂直扩展和水平扩展。
分片将大型数据库分割成更小、更易于管理的部分,称为分片(shards)。每个分片共享相同的模式(schema),但每个分片上的实际数据是唯一的。
图1-21展示了分片数据库的一个示例。用户数据根据用户ID分配给数据库服务器。每当访问数据时,都会使用哈希函数来找到相应的分片。在我们的示例中,使用 user_id % 4 作为哈希函数。如果结果为0,则使用分片0来存储和获取数据;如果结果为1,则使用分片1。其他分片也适用相同的逻辑。
图1-22展示了分片数据库中的用户表。
实施分片策略时最重要的因素是选择分片键。分片键(也称为分区键)由一个或多个列组成,决定数据如何分布。如图1-22所示,“user_id” 就是分片键。分片键通过将数据库查询路由到正确的数据库,使您能够高效地检索和修改数据。在选择分片键时,最重要的标准之一是选择一个可以均匀分布数据的键。
分片是一个很好的数据库扩展技术,但它并不是完美的解决方案。它给系统带来了复杂性和新的挑战:
重新分片数据:当发生以下情况时,需要进行重新分片:
- 由于快速增长,单个分片无法再容纳更多数据;
- 由于数据分布不均,某些分片可能会比其他分片更快地达到数据枯竭。当发生数据枯竭时,需要更新分片函数并重新分配数据。为了应对这个问题,常用的一种技术是一致性哈希,相关内容将在第5章中讨论。
名人问题:这也被称为热点键问题。对特定分片的过度访问可能导致服务器过载。设想一下,Katy Perry、Justin Bieber和Lady Gaga的数据都集中在同一个分片中。在社交应用中,这个分片将会因为读操作的激增而不堪重负。为了解决这个问题,我们可能需要为每位名人分配一个单独的分片。每个分片甚至可能需要进一步的细分。
连接和反规范化:一旦数据库在多个服务器上进行了分片,跨数据库分片执行连接操作就变得困难。一种常见的解决方法是对数据库进行反规范化,以便在单个表中执行查询。
在图1-23中,我们对数据库进行了分片,以支持快速增长的数据流量。同时,一些非关系型功能被迁移到NoSQL数据存储中,以减轻数据库的负担。这里有一篇文章介绍了NoSQL的许多应用案例[14]。
数百万用户及其未来
系统的扩展是一个迭代过程。基于我们在本章中学到的内容进行迭代,可以使我们取得很大进展。然而,要扩展到超过数百万用户,我们还需要进一步优化和采用新的策略。例如,您可能需要优化系统并将其解耦为更小的服务。本章中所学的所有技术都为应对新的挑战提供了良好的基础。为总结本章,我们列出了支持数百万用户的系统扩展策略:
- 保持Web层无状态
- 在每个层次建立冗余
- 尽可能缓存数据
- 支持多个数据中心
- 将静态资产托管在CDN中
- 通过分片扩展数据层
- 将层次拆分为独立服务
- 监控系统并使用自动化工具
恭喜您走到这里!现在给自己一个鼓励,干得好!
参考文献
[1] Hypertext Transfer Protocol: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
[2] Should you go Beyond Relational Databases?: https://blog.teamtreehouse.com/should-you-go-beyond-relational-databases
[3] Replication: https://en.wikipedia.org/wiki/Replication_(computing)
[4] Multi-master replication: https://en.wikipedia.org/wiki/Multi-master_replication
[5] NDB Cluster Replication: Multi-Master and Circular Replication: https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster-replication-multi-master.html
[6] Caching Strategies and How to Choose the Right One: https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/
[7] R. Nishtala, "Facebook, Scaling Memcache at," 10th USENIX Symposium on Networked Systems Design and Implementation (NSDI ’13): https://research.facebook.com/publications/scaling-memcache-at-facebook/
[8] Single point of failure: https://en.wikipedia.org/wiki/Single_point_of_failure
[9] Amazon CloudFront Dynamic Content Delivery: https://aws.amazon.com/cloudfront/dynamic-content/
[10] Configure Sticky Sessions for Your Classic Load Balancer: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-sticky-sessions.html
[11] Active-Active for Multi-Regional Resiliency: https://netflixtechblog.com/active-active-for-multi-regional-resiliency-c47719f6685b
[12] Amazon EC2 High Memory Instances: https://aws.amazon.com/ec2/instance-types/high-memory/
[13] What it takes to run Stack Overflow: http://nickcraver.com/blog/2013/11/22/what-it-takes-to-run-stack-overflow
[14] What The Heck Are You Actually Using NoSQL For: http://highscalability.com/blog/2010/12/6/what-the-heck-are-you-actually-using-nosqlfor.html