分而治之
2004年,ServiceNow的创始团队(最初称为Glidesoft)构建了一个称为“滑翔”(Glide)的通用工作流平台。在寻找可以应用该平台的行业时,团队发现建立在信息技术基础设施库(ITIL)上的信息技术服务管理(ITSM)领域有机会可以通过PaaS服务(平台即服务)一展身手。在这个领域里已经存在着竞争对手,像Remedy这样的以本地软件形式存在的潜在替代者,团队认为像Salesforce公司这样成功的客户关系管理(CRM)解决方案对在线ITSM解决方案有很好的启发意义。
2006年,为了能更好地代表其在ITSM解决方案领域为买家需求提供解决方案,公司更名为ServiceNow并在2007年开始盈利。与许多初创公司不同,ServiceNow在其创立初期就体会到了面向扩展设计、实施和部署的价值。最初的解决方案设计包括故障隔离(参见第9章)和Z轴客户拆分(本章将涉及)。这种故障隔离和客户拆分允许公司通过扩展获得早期的盈利能力,同时避免了很多早期SaaS和PaaS产品常见的噪音临近问题。此外,该公司重视由多租户模式所带来的成本效益,尽管他们沿着客户边界创建故障隔离区,但是他们仍然为不需要完全隔离的较小客户设计解决方案,使这些客户可以在数据管理系统(DBMS)内利用多租户系统。最后,该公司既重视外部视角提供的洞察力,也珍惜经验丰富的员工所固有的价值。
ServiceNow与AKF合作伙伴通过多项合作来帮助他们思考未来的架构需求,最终聘请了AKF的一位创始合伙人汤姆·凯文来充实他们已经很有才华的技术团队。“我们从产品发布之日起就拥有令人难以置信的可扩展性。”汤姆说,“沿着客户边界使用AKF的Z轴扩展去拆分,以确保我们能够满足早期的需求。但随着客户基数的增长,客户平均规模的不断增加而且超越了早期的小型使用者,我们开始服务更大的财富500强公司,工作量的特征也在改变,每个客户的平均用户数量急剧增加。所有这些导致每个客户要执行更多的事务并且存储更多的数据。此外,我们不断扩展功能的范围,每次发布都为客户带来更大的价值。这种功能扩展意味着对大型和小型客户的系统都提出了更大的需求。最后,我们在MySQL的单个数据库下运行多个模式或数据库遇到了一个小问题。具体地说,在每个数据库实例上有30个大容量租户时,MySQL中的目录功能(有时在技术上称为信息模式)开始出现资源争用现象。
汤姆·凯文在构建基于网络的产品方面积累了独特的经验,从如日中天的Gateway电脑到像eBay和PayPal这样疯狂的互联网初创公司,同时他还有数个其他AKF客户的经验,这些积累使他特别适合帮助解决ServiceNow的挑战。汤姆解释道,“数据库目录问题很容易解决。对于非常大的客户,我们直接为每个客户分配一个专用的数据库,从而减少隔离区中的故障突发半径。中型客户可能有30个以下的租户,小型客户可以继续使用大量租户共享的系统(更多内容参见第9章)。AKF扩展立方体既有助于抵消日益增长的客户规模,也能满足急剧膨胀的快速功能扩展和价值创造的需求。对于具有海量事务处理需求的大客户,我们通过将数据复制到只读数据库整合了X轴。报表通常只读不写,属于计算密集型和I/O密集型,利用只读数据库的配置,我们可以在复制的数据库上执行SQL语句,这对在线事务处理(OLTP)数据库没有任何影响。报告功能代表Y轴拆分(服务/功能或基于资源),我们通过Y轴的服务拆分,实现额外的基于服务的故障隔离、更大的数据缓存、更快的研发人员吞吐量。所有这些X、Y和Z轴拆分使我们在基础设施和为任何类型的客户购买类似的商业化系统中保持一致。需要更多的处理能力吗?X轴将允许我们轻松而且快速地扩展以增加交易量。如果发现数据库的数据操作开始变得迟缓,架构允许我们降低租户的密度(Z轴),或者通过拆分(Y轴)把某些服务迁移到其他类似的硬件上。
本章讨论通过克隆和复制的方法扩展数据库和服务,分离功能或服务以及跨存储和应用分拆相似的数据集系统。有了这三种方法,就能够把几乎任何系统或数据库扩展到接近无限的水平。在这里使用“接近”一词略显保守,但是在我们跨越数百家公司和数千个系统的经验中,目前这些技术还没有过失败的先例。为了帮助大家更加直观地了解这三种扩展的方法,我们采用了AKF扩展立方体来帮助讨论,这个立方体是我们专门抽象出来用于解释系统扩展方法的。图2-1显示了AKF扩展立方体,它是以我们合作伙伴的名字(AKF Partner)来命名的。
图2-1 AKF扩展立方体
AKF扩展立方体的核心是三条简单的轴,每条轴都有一套相关的可扩展性规则。立方体是表示从最小规模(立方体的左前下角)到接近无限可扩展性(立方体的右后上角)的扩展路径的好方法。有时,去掉立方体受限的空间可以更容易看到这三条轴。图2-2显示了这三条轴及其配套的规则。本章将对三个规则进行详细的讨论。
并不是每个公司都需要AKF扩展立方体所有的能力(所有三条轴)。对于我们的许多客户,X、Y或Z轴之一的拆分就可以满足他们十多年的需要。但是当你取得像ServiceNow这样的病毒式快速增长产品的成功时,就很可能需要本章将要讨论的两个或多个拆分。
图2-2 三轴扩展
规则7——X轴扩展
内容: 通常叫水平扩展,通过复制服务或数据库以分散事务处理带来的负载。
场景:
数据库读写比例很高(可以达到至少5∶1甚至更高——越高越好)。
事务增长超过数据增长的系统。
用法:
克隆服务的同时配置负载均衡器。
确保使用数据库的代码清楚读和写之间的区别。
原因: 以复制数据和功能为代价获得事务的快速扩展。
要点: X轴拆分实施速度快,研发成本低,事务处理扩展效果好。然而,从运维角度来看,数据的运营成本比较高。
在扩展问题的解决方案中最困难的部分经常是数据库或持久存储层。这个问题的起源可以追溯到埃德加·康德在1970年发表的论文“大型共享数据银行的数据关系模型”[1],关系型数据库管理系统(RDBMS)概念的引入归功于此。顾名思义,当今最流行的关系型数据库(如Oracle、MySQL和SQL Server)允许数据元素之间存在着关系。这些关系既可以存在于表内,也可以存在于表与表之间。OLTP系统中的大多数表都可以规范为第三范式[2],每个表的所有记录都有相同的字段,非关键字段必须完全依赖于主键,而不能只依赖于主键的一部分,而且所有的非关键字段都必须直接依赖于主键。每个数据都和表中的其他数据相关联。表与表之间也会存在着外键关系。因为ACID属性,许多应用都依靠数据库来支持和强制这些关系(见表2-1)。要求数据库保持和强制这些关系意味着如果不投入大量的技术资源,这种数据库将很难拆分。
表2-1 数据库的ACID属性
属性 描 述
原子性 要么完全执行事务的所有操作,要么完全不执行任何操作
一致性 当事务开始执行操作时,数据库将处于一致的状态
隔离性 事务执行时,好像是数据库中唯一在执行的操作
持久性 当事务执行完成后,它对数据库的所有操作都是不可逆转的
数据库扩展的一个技巧是利用大多数应用对数据库的读操作远远多于写操作。我们有一个处理订票业务的客户,平均每完成一个订票交易需要400个查询。每个订票交易是数据库的一个写操作,而每个查询是数据库的一个读操作,由此得出400∶1的读写比例。这种类型的系统可以通过复制只读数据的办法实现扩展。
根据数据对时间的敏感性,我们有几种不同的方法来分散只读数据。时间敏感性指的是与写数据库相比,只读数据库的拷贝有多么新鲜或者有多大比例完全准确。在你大声要求数据必须是即时、实时、同步和完全准确之前,先喘口气估算一下这种系统的成本。尽管完全同步的数据是理想的,但是它成本巨高。另外,它并不是总能带给你所期待的回报。第5章中的规则19将会深度讨论成本与结果对产品可扩展性的影响。
让我们来重新审视那个客户的订票系统,其中每个写操作伴随着400次读操作。因为他们是提供订票服务的,所以你可能会认为显示给客户看的数据将是完全同步的。新手为此可能会准备400份数据拷贝,并与客户订票所需要的那份数据同步。如果只因为与主交易数据库不同步,存在着3秒、30秒或者90秒的时间差,这并不意味着错误,只是有可能不准确。在任意一个时刻,我们那位客户的系统中可能有10万条数据,每天的订票会涉及10%的数据。假设这些订票活动均匀地分布在一天的时间范围里,那么平均每秒钟(0.86秒)会有一个订票业务发生。上天对每个人都是公平的,某个客户想要预订的位置被其他客户抢走了的概率是0.104%(假设数据每90秒同步一次)。当然,即使只有0.1%的概率,客户还是有可能选中已经被人抢走的位置,尽管这不太理想,但是仍然可以在客户把选择的座位放入购物车之前,以在应用中做最终检查的方式来进行规避。诚然,每个应用的数据需求都是不一样的,但是我们希望通过这个讨论,理解如何拒绝所有数据都必须实时同步的想法。
讨论完了数据的时间敏感性,让我们来看看分散数据的几种方法。一种方法是在数据库的前面加缓冲层。让读操作从对象缓存中取数据,而不是通过应用反复查询数据库。只有当相关的数据被标注为过期时,才会查询主要事务型数据库,从而检索数据和更新缓存。如果系统的可用性非常好,我们强烈推荐第一步采用开源的键值存储作为对象缓存方式。
下一步是数据库复制,这超越了应用层和数据库层之间的对象缓存。大多数的关系型数据库系统都能非常好地支持某种类型的复制。许多数据库通过主从方式来实现复制,主数据库是负责写入的主要事务型数据库,而从数据库是主数据库的只读副本。主数据库不断地跟踪数据的更新、插入、删除,并把记录存入一个二进制日志。从数据库从主数据库那里获得二进制日志后,在从数据库上重新执行这些命令。这是一个异步的过程,数据之间的时间延迟,取决于主数据库插入和更新的数据量。以我们的客户为例,每秒同步一次就可以应对每天10%的数据变化。这个数据的变化量足够低,可以维持低延迟的从数据库。这种实施经常包括配置在负载均衡器后面的几个从数据库或者读数据库拷贝。应用向负载均衡器发出读取请求,负载均衡以轮询或者最少连接数的策略把请求传递给从数据库。有些数据库更进一步允许以主主的概念进行复制,其中任意数据库都可以用来读或写。同步的进程有助于确保主数据库之间数据的一致性与连贯性。虽然这项技术已经存在很久,我们更喜欢依赖于单个写数据库的解决方案,这有助于消除混淆和避免数据库之间的逻辑争用。
我们把这种拆分(复制)称为X轴拆分,在图2-1的AKF扩展立方体上标为X轴——水平复制。举个熟悉网络应用托管的许多研发人员都会明白的例子,我们在一个系统的网络层或应用层,可以在负载均衡器的后面运行多个具有相同代码的服务器。请求进入负载均衡器后被分配到众多网络或应用服务器中的任何一个来完成后续的处理。这种应用层上的分布式模型的好处在于你可以在负载均衡器的后面配置几十、几百甚至几千台服务器,而所有这些机器都运行着相同的代码同时处理着类似的请求。
X轴拆分不仅仅可以应用于是数据库。通常可以很容易地克隆网络服务器和应用服务器。这种克隆允许事务在系统之间均匀地分布以实现水平扩展。克隆应用或网络服务相对来说比较容易,允许我们扩大事务的处理量。不幸的是,它并没有真正地帮助我们,当试图扩大数据规模时,我们必须要巧妙地控制才能够处理这些事务。内存中的缓存数据与几个独特客户或一些独特功能相关联可能就会出现瓶颈,结果使我们无法在不显著影响客户响应时间的情况下有效地扩展服务。为了解决这些内存约束,我们将着眼于扩展立方体的Y轴和Z轴。
规则8——Y轴拆分
内容: 有时也称为服务或者资源扩展,本规则聚焦在沿着动词(服务)或名词(资源)的边界拆分数据集、交易和技术团队。
场景:
数据之间的关系不是那么必要的大型数据集。
需要专业化拆分技术资源的大型复杂系统。
用法:
用动词来拆分动作,用名词拆分资源,或者两者混用。
沿着动词/名次定义的边界拆分服务和数据。
原因: 不仅允许事务及其相关的大型数据集有效扩展,也支持团队的有效扩展。
要点: Y轴或者面向数据/服务的拆分允许事务和大型数据集的有效扩展,有益于故障隔离。Y轴拆分也有助于减少团队之间的非必要沟通。
当你放下了关于以服务为导向(SOA)和以资源为导向(ROA)架构的*辩论,转而深入了解其基础前提时,就会发现二者至少有一个共同点。这两个概念都在迫使架构师和工程师思考他们在架构内的职责分工。宏观地看,他们想通过动词(服务)和名词(资源)的概念做到这一点。规则8与扩展立方体的第二个轴不谋而合,采取了相同的方法。简单地说,规则8是通过在网站内部拆分不同的功能和数据从而实现扩展的方法。规则8的简单方法告诉我们用名词或动词或二者的组合来拆分产品。
首先,我们用动词的方法来拆分。如果是相对简单的电子商务网站,我们可以把网站分解为注册、登录、搜索、浏览、查看、添加到购物车和购买几个必要的动词。执行某种事务所需要的数据可以明显不同于执行其他事务所需要的数据。例如,虽然注册和登录需要相同的数据,但各自也存在一些独特的数据。注册可能需要检查用户的首选ID是否已被其他人选用,而登录可能不需要完整地了解每一个其他用户的ID。注册可能需要把大量的数据写入永久性的数据存储,但登录可能是一个验证用户凭据的读取密集型的应用。注册会要求用户存储大量的包括信用卡号码在内的个人可识别信息(PII),而登录不太可能需要访问所有这些信息。
当我们分析搜索和登录这样迥然不同的功能时,这种扩展方法的差异和由此而产生的机会变得甚至更加明显。在登录的情况下,我们主要关注的是验证用户的凭据和建立可能像会话这样的一些东西(第10章中的规则40会详细解释为什么我们要选择“会话”而非“状态”)。登录与用户有关,因此需要缓存和该用户有关的交互数据。另一方面,搜索关注的是寻找一个商品,最关心的是用户的意图(通常在一个搜索框里输入搜索字符串、查询或搜索条件)和目录列出的商品。拆分这些数据可以使我们在有限的内存中缓存更多的数据,因此缓存的命中率更高,系统可以更快地处理事务。在后端的持久化系统(如数据库)中拆分这些数据,将使我们能够在这些系统中投入更多的“内存”空间,更快地响应客户的请求(应用服务器)。更好地利用系统资源促使这两个系统有更快的响应速度。显然,我们现在可以很容易地扩展这些系统而且较少受内存的限制。此外,通过和规则7(X轴扩展)一样的方式拆分事务,Y轴增加了事务的可扩展性。
等一下!在推荐产品的情况下,如果我们想要合并用户和产品信息,应该怎么办?请注意,我们刚刚添加了另一个动词——推荐。这给了我们另一个进行数据和事务拆分的机会。我们或许会增加一个推荐服务,根据过去的购买行为进行异步评估有类似购买行为的用户。这可能会在登录功能或搜索功能中相应地准备数据,当用户与系统交互时把这些数据显示出来。或者它是一个来自于浏览器的独立的同步调用,其结果显示在推荐专区。
现在我们来看看如何使用名词进行拆分。继续以电子商务为例,可以确定某些最终将采取行动的资源(不是我们采取的行动)。假设我们的电子商务网站是由产品目录、产品库存、用户账户信息和营销信息等组成。使用名词的方法,我们可能决定把数据按类拆分,然后定义一组有关操作的高层次的原语,如创建、读取、更新和删除。
Y轴拆分对数据集的扩展最有价值,但同时对代码库的扩展也很有用途。随着服务或资源的拆分,我们所执行的操作和支持这些操作所必需的代码也会被拆分。这意味着研发复杂系统的非常大的技术团队可以变成这些系统子集的专家,而且再也不需要担心所有其他的子系统或者成为所有其他子系统的专家。每个团队可以在其服务中建立接口(如API)。如果每个团队都“拥有”自己的代码库,我们就可以减少与布鲁克斯定律相关的通信开销。布鲁克斯定律中的一条是研发者的生产力随着团队规模的增加而减少[3]。协调团队努力的沟通成本是团队规模的平方。因此,随着团队规模的不断加大,研发人员的生产力不断降低,因为研发人员把越来越多的时间花费在协调上。我们可以通过拆分团队和行使所有权来降低这样的开销。当然正是因为拆分了服务,所以才可能相当容易地扩展事务。
规则9——Z轴拆分
内容: 经常根据客户的独特属性(例如ID、姓名、地理位置等)进行拆分。
场景: 非常大而且类似的数据集,如庞大而且增长快速的客户群,或者当响应时间对在地理上广泛分布的客户变得很重要的时候。
用法: 根据所知道的客户的属性(例如ID、名,地理位置或设备)对数据和服务进行拆分。
原因: 客户的快速增长超过了其他形式的数据增长,或者在扩展时,需要在某些客户群之间进行必要的故障隔离。
要点: Z轴拆分对扩大客户基数的效果明显,也用在其他那些无法使用Y轴拆分的大型数据集上。
规则9通常被称为分片,是关于把一个数据集或服务分割成几块的。通常这些块的大小相同,如果保持几个大小不一的块或碎片有价值,那么也可以这么做。保持块的尺寸大小不一的一个理由是为了适应应用发布,这样可以通过先把代码发布到含有少量客户的一个小块来控制风险,当感觉已经发现并解决了主要的问题后,再发布给含有大量客户的其他块。这也是让我们观察和发现问题的一个重要方法——先把代码发布到规模较小的块上,如果所发布的产品没有达到预期的效果(或者你想通过早期的发布了解用户对某个功能的使用情况),可以在大规模发布之前迅速修正和优化产品。
分片经常通过我们已知的请求者或客户的某些信息特征来完成。假设我们是一个考勤卡和提供考勤跟踪服务的SaaS提供商。我们负责跟踪客户每名员工的时间和出勤情况,这些都是超过1000名员工的企业级客户。我们或许能够确定很容易地把系统按照公司分片,这意味着每个公司都可以拥有自己专用的网络、应用和数据库服务器。假设我们想利用多租户模式来降低成本,同时还想让多个小公司共享一个分片。拥有许多员工的大公司可能有专用硬件,而员工较少小的小公司可能聚集在大量分片上。我们已经借助员工和公司之间存在着关联的事实建立了可扩展的分片系统,这将使我们可以通过采用更小、成本更低的硬件来实现水平扩展(我们将在下一章进一步讨论规则10中提到的水平扩展)。
也许我们是手机广告服务的提供商。在这种情况下,我们很有可能知道一些关于用户的终端设备和运营商的信息。我们可以根据这两种数据的显著特点来进行数据的分片。如果我们是一个电子商务公司,可能会根据用户的地理位置分片,以便更有效地利用配送中心的可用库存,并使电子商务网站给予最快的响应时间。或者,基于近因、频率和购买变现来创建数据分片,使我们能够均匀地分散用户。或者,如果所有其他的方法都失败了,也许我们可以通过对在注册时指定给用户的ID取模或散列算法来产生分片。
为什么我们会决定把类似的东西分片?对于超高速增长的公司,答案显而易见。响应任何用户请求的速度至少部分是由本地或远程缓存的命中率决定的。这个速度告诉我们在任意给定的系统上可以处理多少个事务,也决定了我们需要处理多少个这样的系统。在极端情况下,如果没有数据的分片,事务处理可能会因为要为单个用户的一个答案试图遍历大量的单片数据而变得异常痛苦和缓慢。当速度变得最为重要,并且响应任何请求所涉及的数据量都很大时,拆分不同的东西(规则8)和拆分类似的东西(规则9)就很有必要了。
拆分类似的东西显然不仅局限于客户,客户只是在我们的咨询实践中最常见和最容易按照规则9实施的。有时候,我们也建议拆分产品目录。但是,当拆分不同的目录,比如草坪座椅和尿布时,我们经常会将它们归为拆分不同的东西。我们也曾帮助客户通过取模或哈希算法拆分他们的系统事务ID。在这些情况下,我们真的对请求者一无所知,但我们知道单调递增的数字。可以在记录事务以留待未来参考的系统中进行这种类型的拆分,比如保留错误记录以待未来评估的系统。
总结
我们认为使用三个简单的规则就可以助你的扩展所向无敌。沿着X、Y和Z轴的扩展都各有优势。从软件设计和研发的角度考虑,通常X轴扩展的成本最低,Y和Z轴的扩展方案的设计有点儿挑战性,但有更多的灵活性,可以进一步拆分服务、客户甚至技术团队。毋庸置疑,系统和平台的扩展还有更多方法,但是如果武装了这三条规则,几乎没有可扩展性相关的问题会挡你的路。
通过克隆扩展——通过克隆或复制数据和服务使你很容易地扩展事务。
通过拆分不同的东西来扩展——通过名词或动词标识来拆分数据和服务。如果正确完成,可以有效地扩展事务和数据集。
通过拆分类似的东西来扩展——通常是客户数据集。依据客户属性拆分客户数据,形成独特和隔离的数据分片或者泳道(参见第9章),以实现事务和数据的扩展。
注释
1. Edgar F. Codd, “A Relational Model of Data for Large Shared Data Banks,” 1970, www.
seas.upenn.edu/~zives/03f/cis550/codd.pdf.
2. Wikipedia, “Third Normal Form,” http://en.wikipedia.org/wiki/Third_normal_form.
3. Wikipedia, “*s’ Law,” https://en.wikipedia.org/wiki/*s’_law.