在eBay,我们每天都在争论的主要架构力量之一是可扩展性。它为我们制定的每一个架构和设计决策着色和推动。全球有数亿用户,每天超过20亿的页面浏览量,以及我们系统中的数PB数据,这不是一个选择 - 它是必需的。
在可扩展的体系结构中,资源使用应该随负载线性增加(或更好),其中可以在用户流量,数据量等中测量负载。在性能与单个工作单元相关的资源使用情况下,可伸缩性是关于如何随着工作单元数量或大小的增加,资源使用情况发生变化。换句话说,可伸缩性是价格 - 性能曲线的形状,而不是其在该曲线中的一点处的值。
可伸缩性有许多方面 - 事务性,操作性,开发性工作。在本文中,我将概述我们随着时间的推移学习的几个关键最佳实践,以扩展基于Web的系统的事务吞吐量。大多数这些最佳实践对您来说都很熟悉。有些人可能没有。所有这些都来自开发和运营eBay网站的人们的集体经验。
最佳实践#1:按功能划分
无论您将其称为SOA,功能分解还是简单的良好工程,相关的功能都属于一体,而不相关的功能则属于不同。此外,不相关的功能可以解耦得越多,就越需要彼此独立地扩展它们。
在代码级别,我们都会一直这样做。 JAR文件,包,包等都是我们用来隔离和抽象一组功能的机制。
在应用层,eBay将不同的功能划分为单独的应用程序池。销售功能由一组应用程序服务器提供,竞标功能由另一组应用程序服务器提供,另一组应用程序服务器进行搜索。总的来说,我们将大约16,000个应用服务器组织到220个不同的池中。这允许我们根据其功能的需求和资源消耗,彼此独立地扩展每个池。它进一步允许我们隔离和合理化资源依赖性 - 例如,销售池只需要与相对较小的后端资源子集进行通信。
在数据库层,我们遵循相同的方法。 eBay上没有单一的整体数据库。相反,有一组用于用户数据的数据库主机,一个用于项目数据的集合,一个用于购买数据的集合等。 - 在400个物理主机上共有1000个逻辑数据库。同样,这种方法允许我们为每种类型的数据独立地扩展数据库基础结构。
最佳实践#2:水平分割
虽然功能分区使我们成为一种方式,但对于完全可扩展的架构而言,它本身并不足够。由于一个功能可能与另一个功能分离,因此单个功能区域的需求可能并且将随着时间的推移而超过任何单个系统。或者,正如我们想提醒自己的那样,“如果你不能拆分它,你就无法扩展它。”因此,在特定的功能区域内,我们需要能够将工作量分解为可管理的单元,其中每个单元保持良好的性价比。这是水平分割的来源。
在应用层,eBay的交互是设计无状态的,水平分割是微不足道的。使用标准负载平衡器来路由传入流量。因为所有应用程序服务器都是相同的,并且没有保留任何事务状态,所以它们中的任何一个都可以。如果我们需要更多处理能力,我们只需添加更多应用服务器。
数据库层出现了更具挑战性的问题,因为根据定义数据是有状态的。在这里,我们沿着主要访问路径水平分割(或“分片”)数据。例如,用户数据当前分为20个主机,每个主机包含1/20的用户。随着用户数量的增长,以及我们为每个用户存储的数据增长,我们会添加更多主机,并进一步细分用户。同样,我们对项目,购买,帐户等使用相同的方法。不同的用例使用不同的方案来划分数据:一些基于密钥的简单模数(以1结尾的项目ID转到一个主机,以2结尾的那些等等,其中一些在一系列ID(0-1M,1-2M等)上,一些在查找表上,一些在这些策略的组合上。然而,无论分区方案的细节如何,一般的想法是支持数据分区和重新分区的基础设施将比不支持分区和重新分区的基础设施更具可扩展性。
最佳实践#3:避免分布式事务
此时,您可能想知道如何通过事务保证在功能和水平方面对数据进行分区。毕竟,几乎任何有趣的操作都会更新多种类型的实体 - 用户和物品会立即浮现在脑海中。正统的答案是众所周知且易于理解的 - 使用两阶段提交在各种资源之间创建分布式事务,以保证所有资源的所有更新都发生或不发生。不幸的是,这种悲观的方法带来了巨大的成本。协调成本会对扩展,性能和延迟产生负面影响,随着您增加依赖资源和传入客户端的数量,协调成本会恶化。可用性同样受限于所有相关资源可用的要求。务实的答案是放宽不相关系统的交易保证。
事实证明,你不能拥有一切。特别是,通常既不需要也不可能保证跨多个系统或分区的立即一致性。大约10年前由Inktomi的Eric Brewer提出的CAP定理指出,分布式系统的三个非常理想的特性 - 一致性(C),可用性(A)和分区容差(P) - 你只能选择两个一度。对于高流量网站,我们必须选择分区容差,因为它是扩展的基础。对于24x7网站,我们通常会选择可用性。因此,即时一致性必须让位。
在eBay,我们绝对不允许任何类型的客户端或分布式事务 - 没有两阶段提交。在某些明确定义的情况下,我们将单个数据库上的多个语句组合成单个事务操作。但是,在大多数情况下,单个语句是自动提交的。虽然这种对正统ACID属性的有意放松并不能保证在任何地方都能立即保持一致,但实际情况是大多数系统在绝大多数情况下都是可用的。当然,我们采用各种技术来帮助系统达到最终的一致性:仔细排序数据库操作,异步恢复事件以及协调或结算批次。我们根据特定用例的一致性要求选择技术。
对于架构师和系统设计师来说,关键的一点是,不应将一致性视为一个全有或全无的命题。大多数真实世界的用例根本不需要立即一致性。正如可用性不是全部或全部,我们经常将其与成本和其他力量进行权衡,同样我们的工作也会根据特定操作的要求定制适当的一致性保证。
最佳实践#4:异步解耦功能
扩展的下一个关键要素是积极使用异步。如果组件A同步调用组件B,则A和B紧密耦合,并且该耦合系统具有单一的可伸缩性特征 - 为了扩展A,您还必须扩展B.同样有问题的是它对可用性的影响。回到逻辑101,if A implies B, then not-B implies not-A。换句话说,如果B下降则A下降。相反,如果A和B异步集成,无论是通过队列,多播消息传递,批处理过程还是其他方式,每个都可以独立于另一个进行缩放。此外,A和B现在具有独立的可用性特征 - 即使B关闭或受困,A仍可继续前进。
这个原则可以而且应该在基础设施上下应用。诸如SEDA(分阶段事件驱动架构)之类的技术可用于在单个组件内部进行异步,同时保留易于理解的编程模型。在组件之间,原理是相同的 - 尽可能避免同步耦合。通常情况下,这两个组件在任何情况下都没有业务直接对话。在每个级别,将处理分解为阶段或阶段,并将它们异步连接,对于扩展至关重要。
最佳实践#5:将处理转移到异步流程
现在您已异步解耦,请将尽可能多的处理移动到异步端。在快速回复请求的系统中,这可以大大减少请求者所经历的延迟。在web站点或交易系统中,用数据或执行延迟(我们完成所有工作的速度有多快)换取用户延迟(用户得到响应的速度有多快)是值得的。活动跟踪,计费,结算和报告是属于后台的处理的明显示例。但是,处理主要用例的重要步骤通常可以分解为异步运行。任何可以等待的东西都应该等待。
同样重要但同样重要的是异步可以大大降低基础设施成本。同步执行操作会迫使您根据峰值负载扩展基础架构 - 它需要在最后一秒处理最糟糕的第二天。但是,将昂贵的处理转移到异步流可以让您根据平均负载而不是峰值来扩展基础架构。队列不是需要立即处理所有请求,而是随着时间的推移扩展处理,从而抑制峰值。系统负载越尖锐或变化越大,这种优势就越大。
最佳实践#6:在所有级别进行虚拟化
虚拟化和抽象无处不在,遵循旧的计算机科学格言,即每个问题的解决方案都是另一个层次的间接。操作系统抽象硬件。许多现代语言中的虚拟机抽象了操作系统。对象关系映射层抽象数据库。负载平衡器和虚拟IP抽象网络端点。当我们通过功能和数据划分来扩展我们的基础架构时,这些分区的额外虚拟化水平变得至关重要。
例如,在eBay,我们虚拟化数据库。应用程序与数据库的逻辑表示交互,然后通过配置将其映射到特定的物理机器和实例。应用程序类似地从拆分路由逻辑中抽象出来,拆分路由逻辑将特定记录(例如,用户XYZ的记录)分配给特定分区。这两种抽象都是在我们自己开发的O / R层实现的。这允许操作团队在物理主机之间重新平衡逻辑主机,通过分离它们,合并它们或移动它们 - 所有这些都不需要触及应用程序代码。
我们同样虚拟化搜索引擎。为了检索搜索结果,聚合器组件在多个分区上并行化查询,并使高度分区的搜索网格作为一个逻辑索引显示给客户端。
这里的动机不仅是程序员的便利性,还有操作灵活性。硬件和软件系统发生故障,需要重新路由请求。添加,移动和删除组件,计算机和分区。通过明智地使用虚拟化,您的基础架构的更高级别可以幸免于未发现这些变化,因此您可以自由地制作它们。虚拟化使得扩展基础架构成为可能,因为它使得扩展可管理。
最佳实践#7:正确缓存
扩展的最后一个组成部分是明智地使用缓存。这里的具体建议不太普遍,因为它们往往高度依赖于用例的细节。在一天结束时,高效缓存系统的目标是在存储限制内最大化缓存命中率,满足可用性要求以及对陈旧性的容忍度。事实证明,这种平衡可能非常难以实现。一旦受到打击,我们的经验表明,它也很可能随着时间而改变。
例如,最明显的缓存机会来自缓慢变化的读取主要数据 - 元数据,配置和静态数据。在eBay,我们积极地缓存这类数据,并使用拉动和推送方法的组合,以使系统在面对更新时合理地保持同步。减少对相同数据的重复请求可以而且确实产生重大影响。更具挑战性的是快速变化的读写数据。在大多数情况下,我们故意在eBay上回避这些挑战。我们传统上没有在请求之间进行任何临时会话数据的缓存。我们同样不会在应用程序层中缓存共享业务对象,如项目或用户数据。我们明确地根据可用性和正确性来缓存这些数据的潜在好处。应该注意的是,其他网站确实采取不同的方法,做出不同的权衡,并且也是成功的。
毫不奇怪,很可能有太多好事。为缓存分配的内存越多,可用于为单个请求提供服务的可用性就越少。在通常受内存限制的应用层中,这是一个非常真实的权衡。但更重要的是,一旦你开始依赖缓存,并采取了极其诱人的步骤来缩小主要系统以处理缓存未命中,那么没有它,你的基础设施可能无法生存。一旦您的主系统无法再直接处理负载,您的站点的可用性现在取决于缓存的100%正常运行时间 - 这是一种潜在的危险情况。即使像重新平衡,移动或冷启动缓存这样的常规操作也会成为问题。
如果操作正确,一个好的缓存系统可以将您的缩放曲线弯曲到线性以下 - 后续请求从缓存中便宜地检索数据,而不是相对更昂贵的主存储。另一方面,缓存不佳会带来大量额外的开销和可用性挑战。我还没有看到一个没有重要缓存机会的系统。但关键是要确保您的缓存策略适合您的情况。
总结
可伸缩性有时被称为“非功能性需求”,暗示它与功能无关,并强烈暗示它不那么重要。没有东西会离事实很远。相反,我想说,可扩展性是功能的先决条件 - 一个“优先级为0”的要求,如果有的话。
我希望您发现这些最佳实践的描述很有用,并且它们可以帮助您以新的方式思考您自己的系统,无论其规模如何。
参考
- eBay's Architectural Principles (video)
- Werner Vogels on scalability
- Dan Pritchett on You Scaled Your What?
- The Coming of the Shard
- Trading Consistency for Availability in Distributed Architectures
- Eric Brewer on the CAP Theorem
- SEDA: An Architecture for Well-Conditioned, Scalable Internet Services