温故而知新-可伸缩架构的最佳实践

这些经验起源于ebay,后来在国内经淘宝发扬光大。最近几年,ebay好像没有声音了,不少年轻的架构师可能已经不知道ebay了,早个几年,它仍是世界的“淘宝”,“淘宝”是中国的ebay,相似于“百度”是中国的google。
在ebay,全球数亿的用户量,天天超过10亿的pv,数据量已PB计算,这些最佳实践能够说是ebay全部开发和运维的集体经验结晶,也是ebay为整个互联网做出的极大贡献。
我拿着这些最佳实践,在不一样场合作了几回分享,如今再翻出来,从新复习一遍,也分享给天天为可伸缩,高并发拼搏的同窗。
 
如下内容,尽量的照搬ebay分享的原有内容,加了部分本身实践过程当中的体会(蓝色部分)。
 
最佳实践#一、按功能分隔
 
相关的功能部分应该合在一块儿,不相关的功能部分应该分割开来——前几年,你们习惯叫SOA或者功能拆分,最近几年你们喜欢叫微服务。按功能拆分后,将会带来如下几个好处:
一、不想关的功能,将其应用逻辑、数据存储独立成一个应用而单独部署,在应对流量、数据量大增时,就能够对其进行单独的扩容而不影响其余模块。
二、开发管理,在大规模团队开发时,能够将团队分多个团队,每一个小组负责其中一个应用,小组内部的协调就会方便不少。这点好处,也许你们容易忽略。
 
举个我经历过的反面教材:
某个大型实体零售公司,作科技转型,作电商。早期的系统商品、交易、会员、促销等全部模块全在一个大包里面,100多人到200人的开发团队全在一个代码工程下工做,任何一次代码更新,都会有大量的冲突,好不容易解决冲突,想启动开发环境进行调试,半个小时还没起来,开发机资源几乎所有耗尽。各位自行脑补是一个什么场景。过完春节后,离职申请堆了好高一摞,工位最多看到1/3的熟面孔。
 
最佳实践#二、水平拆分
 
按功能分割对咱们的帮助很大,但单凭它还不足以获得彻底可伸缩的架构。即便将功能一一解耦,单项功能的资源需求随着时间增加,仍然有可能超出单一系 统的能力。咱们经常提醒本身,“没有分割就没有伸缩”。在单项功能内部,咱们须要能把工做负载分解成许多咱们有能力驾驭的小单元,让每一个单元都能维持良好状态。这就是水平分割出场的时候了。
在应用层次,因为 eBay 将各类交互都设计成无状态的,因此水平分割是垂手可得之事。用标准的负载均衡服务器来路由进入的流量。全部应用服务器都是均等的,并且任何服务器都不会维持事务性的状态,所以负载均衡能够任意选择应用服务器。若是须要更多处理能力,只须要简单地增长新的应用服务器。
 
应用层无状态,比较简单的实现是将用户id存放cookie中,每次请求会带上cookie,应用层根据cookie从集中存储(数据库、缓存)中获取相关的数据。也许会有一些年轻的架构师和开发同窗有疑问:cookie不安全啊,每次传输cookie耗带宽啊,能够作session绑定啊等等,实践当中,常常会被问相似的问题,你们能够留言交流。
举个反面例子:
仍是上面例子的那个公司,因为流量大增,一台服务器确定是没法支撑全部请求的,因此就用了集群,集群内部有100多个节点,每次发布的时候,更新到70几个节点时,就会异常的慢,经常引发发布失败。致使这个的主要缘由是请求是有状态的,集群内部须要维护session同步,不然用户第一次访问到第一个服务器,第二次访问到第二台服务器,就会出问题。
 
数据库层次的问题比较有挑战性,缘由是数据天生就是有状态的。咱们会按照主要的访问路径对数据做水平分割(或称为“sharding”)。例如用户 数据目前被分割到 20 台主机上,每台主机存放 1/20 的用户。随着用户数量的增加,以及每一个用户的数据量增加,咱们会增长更多的主机,将用户分散到更多的 机器上去。商品数据、购买数据、账户数据等等也都用一样的方式处理。用例不一样,咱们分割数据的方案也不一样:有些是对主键简单取模(ID 尾数为 1 的放到第一 台主机,尾数为二的放到下一台,以此类推),有些是按照 ID 的区间分割(1-1M、1-2M 等等),有些用一个查找表,还有些是综合以上的策略。不过具体 的分割方案如何,总的思想是支持数据分割及重分割的基础设施在可伸缩性上远比不支持的优越。
 
数据库分隔,常说的分库分表,一旦作分库分表,会带来不少问题,好比跨库查询、统计等等,这须要你们仔细分析各类应用场景,而后作出最适合的拆分策略。
 

最佳实践 #3:避免分布式事务

看到这里,你可能在疑惑按功能划分数据和水平划分数据的实践如何知足事务要求。毕竟,几乎任何有意义的操做都要更新一个以上的实体——当即就能够举 出用户和商品的例子。正统的广为人知的答案是:创建跨资源的分布式事务,用两段式提交来保证要么全部资源全都更新,要么全都不更新。很不幸,这种悲观方案 的成本很可观。伸缩、性能和响应延迟都受到协调成本的反面影响,随着依赖的资源数量和客户数量的上升,这些指标都会以几何级数恶化。可用性亦受到限制,因 为全部依赖的资源都必须就位。实用主义的答案是,对于不相关的系统,放宽对它们的跨系统事务的保证。
左右逢源是办不到的。保证跨多个系统或分区之间的即时的一致性,一般既无必要,也不现实。Inktomi 的 Eric Brewer 十年前提出的 CAP 公理是这样说的:分布式系统的三项重要指标——一致性(Consistency)、可用性(Availability)和 分区耐受性(Partition-tolerance)——在任意时刻,只有两项能同时成立。对于高流量的网站来讲,咱们必须选择分区耐受性,由于它是实 现可伸缩的根本。对于 24x7 运行的网站,选择可用性也是理所固然的。因而只好放弃即时一致性(immediate consistency)。
在 eBay,咱们绝对不容许任何形式的客户端或者分布式事务——所以毫不须要两段式提交。在某些通过仔细定义的情形下,咱们会将做用于同一个数据库 的若干语句捆绑成单个事务性的操做。而对于绝大部分操做,单条语句是自动提交的。虽然咱们故意放宽正统的 ACID 属性,以至不能在全部地方保证即时一致 性,但现实的结果是大部分系统在绝大部分时间都是可用的。固然咱们也采用了一些技术来帮助系统达到最终的一致性(eventual consistency):周密调整数据库操做的次序、异步恢复事件,以及数据核对(reconciliation)或者集中决算(settlement batches)。具体选择哪一种技术要根据特定用例对一致性的需求来决定。
对于架构师和系统的设计者来讲,关键是要明白一致性并不是“有”和“没有”的单选题。现实中大多数的用例都不要求即时一致性。正如咱们常常根据成本和其余压力因素来权衡可用性的高低,一致性也一样能够量体裁衣,根据特定操做的须要而保证适当程度的一致性。
 
放弃事务控制,对于不少开发同窗来讲难以接受,总会举出一堆的反例来证实必须有事务控制。咱们仔细理解这一句话:“周密调整数据库操做的次序、异步恢复事件,以及数据核对(reconciliation)或者集中决算(settlement batches)”,可能可以帮助理解这一条最佳实践。
举个例子:
系统提供几个微服务模块:库存、促销、订单,订单模块下单的常规逻辑以下:
 
事务开始
一、查库存是否知足 (库存微服务提供的接口)
二、查积分是否知足(库存微服务提供的接口)
三、查优惠券是否知足(库存微服务提供的接口)
四、订单表新增订单信息
五、订单明细表新增订单明细
六、扣减库存
七、扣减积分
八、优惠券设置为已使用
事务提交
 
咱们根据最佳实践“周密调整数据库操做的次序、异步恢复事件”,将下单逻辑调整为:
一、扣减库存(不成功返回)//首先扣减库存,是由于你们都在下单,库存最有可能扣减失败
二、扣减积分(不成功,回滚库存)//扣减积分,积分属于客户一人,很难产生冲突,固然也会有一个帐号多个地方登录,同时下单,但可能性小不少。
三、设置优惠券为已使用(不成功,回滚库存、回滚积分)//设置优惠券同积分相似。
四、单库事务开始
1)订单表新增订单信息
2)订单明细表新增订单明细
五、提交事务(事务失败,回滚库存、回滚积分、回滚优惠券)
六、若是以上某一个回滚步骤失败,记录回滚日志,异步处理恢复,处理失败,监控报警,人工干预。
 
经过以上调整,避免了分布式事务,同时保证了最终一致性。性能获得了大幅度的提高。坏处是增长了不少回滚操做,代码确实要多不少,大多数状况下,回滚逻辑不会被执行到。
保证最终一致性的策略还有不少,你们仔细分析,总会想到不少策略,这里重点强调一点,代码通过仔细的设计、完善测试的状况下,出现异常的状况是极少数的,咱们应该平衡成本产出比,对于极少的异常状况,经过很是规方法处理,是能够接受的。
 

最佳实践 #4:用异步策略解耦程序

提升可伸缩性的另外一项关键措施是积极地采起异步策略。若是组件 A 同步调用组件 B,那么 A 和 B 就是紧密耦合的,而紧耦合的系统其可伸缩性特征是各部分必须共同进退——要伸缩 A 必须同时伸缩B。同步调用的组件在可用性方面也面临着一样的问题。咱们回到最基本的逻辑:若是 A推出B,那么非 B 推出非 A。也就 是说,若 B 不可用,则 A 也不可用。若是反过来 A 和 B 的联系是异步的,无论是经过队列、多播消息、批处理仍是什么其余手段,它们就能够分别地伸缩。并且,此时A和B 的可用性特征是相互独立的——即便 B 受困或者死掉,A 仍然可以继续前进。
整个基础设施从上到下都应该贯彻这项原则。即便在单个组件内部也可经过 SEDA(分阶段的事件驱动架构,Staged Event-Driven Architecture)等技术实现异步性,同时保持一个易于理解的编程模型。组件之间也遵照一样的原则——尽量避免同步带来的耦合。在多数状况下, 两个组件在任何事件中都不会有直接的业务联系。在全部的层次,把过程分解为阶段(stages or phases),而后将它们异步地链接起来,这是伸缩的关键。
 
这一条简单理解就是,有些操做客户是不须要实时知道结果的,那这种操做就能够异步处理,这样带来的好处是:把耗性能的,客户无需实时知道结果的操做异步以后,整个系统的实时响应获得大幅提高。举个例子:
下单流程:
一、下单
二、付款
三、配货
四、物流
五、收货
客户只关心下单是否成功、付款是否成功,至于怎么配货,怎么物流,客户是不怎么关心的,或者不须要付款成功后就立刻告诉客户的。这样,配货逻辑就能够异步操做了。
反例:
仍是以前例子里提到的同一个项目,货物存放在不一样的仓库、库位,还要分批次,配货的逻辑就是:
根据客户信息判断应该从哪一个仓库、库位、批次出货,而后进行库存扣减,由于数据量巨大,这是一个很耗性能的操做,
下单逻辑里,实时执行了配货的逻辑,致使整个下单过程很慢,搞一个促销,下单量一上来,系统就宕机了。
解决方案就是:
下单成功后,发送MQ,配货逻辑接收到MQ消息,执行配货逻辑。也许你会问,那怎么扣减库存,异步以后,告诉用户下单成功了,可是后面没货了怎么办?请仔细理解客户并不关心货是哪一个仓库、哪一个库位,哪一个批次的,理解了这个场景,天然就很容易解决了。
 

最佳实践 #5:将过程转变为异步的流

用异步的原则解耦程序,尽量将过程变为异步的。对于要求快速响应的系统,这样作能够从根本上减小请求者所经历的响应延迟。对于网站或者交易系统, 牺牲数据或执行的延迟时间(完成所有工做的实践)来换取用户的延迟时间(用户获得响应的时间)是值得的。活动跟踪、单据开付、决算和报表等处理过程显然都 应该属于后台活动。主要用例过程当中经常有不少步骤能够进一部分解成异步运行。任何能够晚点再作的事情都应该晚点再作。
还有一个同等重要的方面认识到的人很少:异步性能够从根本上下降基础设施的成本。同步地执行操做迫使你必须按照负载的峰值来配备基础设施——即便在 任务最重的那一天里任务最重的那一秒,设施也必须有能力当即完成处理。而将昂贵的处理过程转变为异步的流,基础设施就不须要按照峰值来配备,只须要知足平 均负载。并且也不须要当即处理全部的请求,异步队列能够将处理任务分摊到较长的时间里,于是起到削峰的做用。系统的负载变化越大,曲线越多尖峰,就越能从 异步处理中得益。
 
这一条和第四条差很少,但第四条更强调两个模块以前的解耦,第五条强调系统性能削峰。
 

最佳实践 #6:虚拟化全部层次

虚拟化和抽象化无所不在,计算机科学里有一句老话:全部问题均可以经过增长一个间接层次来解决。操做系统是对硬件的抽象,而许多现代语言所用的虚拟 机又是对操做系统的抽象。对象 - 关系映射层抽象了数据库。负载均衡器和虚拟 IP 抽象了网络终端。当咱们经过分割数据和程序来提升基础设施的可伸缩性,为各 种分割增长额外的虚拟层次就成为重中之重。
在 eBay,咱们虚拟化了数据库。应用与逻辑数据库交互,逻辑数据库再按照配置映射到某个特定的物理机器和数据库实例。应用也抽象于执行数据分割的 路由逻辑,路由逻辑会把特定的记录(如用户 XYZ)分配到指定的分区。这两类抽象都是在咱们本身开发的 O/R 层上实现的。这样虚拟化以后,咱们的运营团队 能够按须要在物理主机群上从新分配逻辑主机——分离、合并、移动——而彻底不须要接触应用程序代码。
搜索引擎一样是虚拟化的。为了获得搜索结果,一个聚合器组件会在多个分区上执行并行的查询,但这个高度分割的搜索网格在客户看来只是单一的逻辑索引。
以上种种措施并不仅是为了程序员的方便,运营上的灵活性也是一大动机。硬件和软件系统都会故障,请求须要从新路由。组件、机器、分区都会不时增减、 移动。明智地运用虚拟化,可以使高层的设施对以上变化可贵糊涂,你也就有了腾挪的余地。虚拟化使基础设施的伸缩成为可能,由于它使伸缩变成可管理的。
 
这一条实践当中用得最可能是一、虚拟IP,常常应用在双击互备,二、容器虚拟化好比docker,将全部的物理资源组成资源池,根据不一样模块的资源利用率,动态调整资源。
 

最佳实践 #7:适当地使用缓存

最后要适当地使用缓存。这里给出的建议不必定广泛适用,由于缓存是否高效极大地依赖于用例的细节。说到底,要在存储约束、对可用性的需求、对陈旧数 据的容忍程度等条件下最大化缓存的命中率,这才是一个高效的缓存系统的最终目标。经验证实,要平衡众多因素是极其困难的,即便暂时达到目标,状况也很可能 随着时间而改变。
最适合缓存的是不多改变、以读为主的数据——好比元数据、配置信息和静态数据。在 eBay,咱们积极地缓存这种类型的数据,而且结合使用“推”和“ 拉”两种方法保持系统在必定程度上的更新同步。减小对相同数据的重复请求能达到很是显著的效果。频繁变动、读写兼有的数据很难有效地缓存。在 eBay,我 们大多有意识地回避这样的难题。咱们一直不对请求间短暂存在的会话数据做任何缓存。也不在应用层缓存共享的业务对象,好比商品和用户数据。咱们有意地牺牲 缓存这些数据的潜在利益,换取可用性和正确性。在此必须指出,其余网站采起了不一样的途径,做了不一样的取舍,也一样取得了成功。
好东西也会过犹不及。为缓存分配的内存越多,能用来服务单个请求的内存就越少。应用层经常有内存不足的压力,所以这是很是现实的权衡。更重要的一 点,当你开始依赖于缓存,那么主要系统就只须要知足缓存未命中时的处理要求,天然而然你就会想到能够削减主要系统。但当你这样作以后,系统就彻底离不开缓 存了。如今主要系统没办法直接应付所有流量,也就是说网站的可用性取决于缓存可否 100% 正常运行——潜在的危局。哪怕是例行的操做,好比从新配置缓存资 源、把缓存移动到别的机器、冷启动缓存服务器,都有可能引起严重的问题。
作得好,缓存系统能让可伸缩性的曲线向下弯曲,也就是比线性增加还要好——后续请求从缓存中取数据比从主存储取数据成本低廉。反过来,缓存作得很差 会引入至关多额外的常常耗费,也会妨碍到可用性。我还没见过哪一个系统没机会让缓存大展拳脚的,关键是要根据具体状况找到适当缓存策略。
 
这一条,不少同窗都深有体会,但实践过程当中,每每作得不太好。后面可能会写一篇如何使用缓存的文章。使用缓存的过程当中,咱们应该牢记如下几点:
一、缓存不该该在多个节点以前同步,早些年缓存同步方案很常见,好比session在集群内部同步。
二、必须设计一个好的机制,保证缓存数据已数据库的同步。
三、缓存就应该干缓存的事情,不该该把缓存当成持久化存储使用,不少同窗讨论redis如何持久化,如何使用持久化,这会带来不少不少不可预计的问题和系统设计复杂度。我认为redis的持久化特性,主要是应对灾备和恢复场景的,至少我是这么用的。
四、缓存失效不该该致使系统流程失败,好比缓存失效了,读不到缓存,应用应该可以从DB读取数据进行计算。
五、不该该以缓存中的数据做为最终决策的依据,好比说缓存了库存数据,不能缓存中的库存扣减成功就能下单成功。
 
 

总结

可伸缩性有时候被叫作“非功能性需求”,言下之意是它与功能无关,也就比较不重要。这么说简直错到了极点。个人观点是,可伸缩性是功能的先决条件——优先级为 0 的需求,比一切需求的优先级都高。
 
任何一个场景,须要不一样的架构策略,没有一个架构策略能解决全部问题,也没有最好的架构,只有最合适的架构。不少同窗一上来就会把淘宝、阿里的各类框架、最佳实践全用上,每每忽略了当前的业务场景、业务规模、预算、团队水平等等,而致使最终效果不佳。
相关文章
相关标签/搜索