本文根据白辉在 2016ArchSummit 全球架构师(深圳)峰会上的演讲整理而成。ArchSummit 北京站即将在 12 月 2 日开幕,更多专题讲师信息请到北京站官网查询。
非常荣幸在这里跟大家一起来探讨“海量服务架构探索”相关专题的内容。
我叫白辉,花名是七公。2014 年之前主要在阿里B2B 负责资金中心、评价、任务中心等系统。2015 年加入蘑菇街,随着蘑菇街的飞速成长,经历了网站技术架构的大
变革。今天分享的内容来自于去年我们做的事情,题目用了一个关键词是“篱笆”,篱笆的英文是Barrier,是指2015 年蘑菇街面临的问题和艰巨的困难。我们越过了这些篱笆,取得了很好的成果。
引言
今天分享的内容主要分为五部分。第一部分,概述电商系统发展中期面临的一般性问题。第二部分,如何解决面临的问题,主要的策略是做拆分、做服务化。第三、四部分,服务化之后业务的大增长、网站流量飞速的增加、“双11”大促等的挑战很大,我们做了服务的专项系统优化以及稳定性治理。第五部分,进行了总结和展望。
电商系统发展中期面临的一般性问题
我们先看第一部分的内容。
我总结了一下,一般电商系统发展到中期都会面临三个方面的问题(如图)。第一方面是业务问题。比如,一开始做业务的时候可能很随意,一是并不考虑业务模型、系统架构,二是业务之间的耦合比较严重,比如交易和资金业务,有可能资金和外部第三方支付公司的交互状态耦合在交易系统里,这些非常不利于业务发展。第二方面是系统问题。2014 年我们面临单体应用,400 人开发一个大应用,扩展性很差,业务比较难做。第三方面是支撑问题,比如关于环境、开发框架和质量工具等。这些是电商系统发展到中期都会面临的问题,中期的概念是用户过了千万,PV 过了1 亿。
我们来看一下蘑菇街2015 年初面临的问题。蘑菇街2015 年用户过亿,PV 过10 亿,业务在超高速发展,每年保持3 倍以上的增长。电商促销、交易、支付等业务形态都在快速膨胀,我们需要快速支持业务发展,而不是成为业务的瓶颈。那么就是要去做系统的拆分和服务化。
系统拆分与服务化过程
第二部分的内容,是关于蘑菇街系统拆分与服务化的历程。
按照如下几条思路(见图),我们进行系统拆分以及服务化。最开始,大家在同一个应用里开发一些业务功能,都是选择速度最快的方式,所有的DB 和业务代码都是在一起的。首先我们将DB 做垂直拆分。第二步是做业务系统垂直拆分,包括交易、资金等。第三步是在系统拆完了之后要考虑提供什么样的API 来满足业务的需求?这里我们要做数据建模+ 业务建模,数据建模方面包括数据表的设计和扩展支持,数据模型应该非常稳定;业务建模方面,使用标准和灵活的API,而且尽量不用修改代码或者改少量代码就能支持业务需求。第四步是需要将业务逻辑下沉到服务,Web 层专注于展示逻辑和编排,不要涉及过多业务的事情。然后用SOA 中间件建设服务化系统。最后会做一些服务的治理。
来看一个API 服务化的例子,在做服务化之前和做服务化之后,交易创建下单业务有什么不一样。服务化之前我们面临的问题有:入口分散,如果要在底层做任何一个微小的改动,十几个入口需要几十个人配合修改,这是非常不合理的一种方式;多端维护多套接口,成本非常高;还有稳定性的问题,依赖非常复杂,维护很难。我刚到蘑菇街的时候,一次大促活动就导致数据库崩溃,暴露了系统架构很大的问题和总量上的瓶颈。按照上面提到几条思路去做服务化,看看有了哪些改善?首先是API 统一,多个端、多个业务都用统一的API 提供;其次是依赖有效管理起来,大事务拆分成多个本地小事务;最后降低了链路风险,逻辑更加清晰,稳定性更好。
2015 年 3 月我来到蘑菇街之后,先制订了服务化的规范,探讨了到底什么是标准的服务化。在做服务化的过程中,发现大家代码风格完全不一样,所以制定编码规范非常重要。2015 年 8 月,我们完成了各个模块的改造,包括用户、商品、交易、订单、促销、退款等,然后有了服务化架构 1.0 的体系。在此基础之上,我们进一步做了提升流量和稳定性等更深度的建设。2015 年 9 月,我们实施了分库分表和链路性能提升优化,2015 年 10 月做了服务治理和服务保障。
接下来,以服务架构和服务体系建设为主线,讲一下去年整个网站架构升级的过程。
在服务化 1.0 体系完成之后,我们得到了一个简单的体系,包含下单服务、营销服务、店铺服务、商品服务和用户服务,还有简单的 RPC 框架 Tesla。当时,我们并没有做很多性能优化的事情,但是通过业务流程化简和逻辑优化,每秒最大订单数从 400 提升到 1K,基础服务也都搭建了起来。
有了 1.0 初步的服务化体系之后,更进一步,我们一是要继续深入网站如资金等的服务化,二是要做服务内部的建设,比如容量、性能,这也是接下来要讲的内容。
购买链路的性能提升
这个链路(见图)是比较典型的电商链路,有商品页、下单、支付、营销和库存等内容。一开始每个点都有瓶颈,每个瓶颈都是一个篱笆,我们要正视它,然后翻越它。
我们先来看第一个篱笆墙:下单的瓶颈。
2015 年“3.21”大促的时候,DB 崩溃了,这个瓶颈很难突破。下一个订单要插入很多条数据记录到单 DB 的 DB 表。我们已经用了最好的硬件,但是瓶颈依然存在,最主要的问题就是 DB 单点,需要去掉单点,做成可水平扩展的。流量上来了,到 DB 的行写入数是 2 万 / 秒,对 DB 的压力很大。写应该控制在一个合理的量,DB 负载维持在较低水平,主从延时也才会在可控范围内。所以 DB 单点的问题非常凸显,这座大山必须迈过去,我们做了一个分库分表组件 TSharding 来实施分库分表。
将我们写的分库分表工具与业界方案对比,业界有淘宝 TDDL Smart Client 的方式,还有 Google 的 Vitess 等的 Proxy 方式,这两种成熟方案研发和运维的成本都太高,短期内我们接受不了,所以借鉴了 Mybatis Plugin 的方式,但 Mybatis Plugin 不支持数据源管理,也不支持事务。我大概花了一周时间写了一个组件——自研分库分表组件 TSharding( https://github.com/baihui212/tsharding ),然后快速做出方案,把这个组件应用到交易的数据库,在服务层和 DAO 层,订单容量扩展到千亿量级,并且可以继续水平扩展。TSharding 上线一年之后,我们将其开放出来。
第二个篱笆墙就是营销服务 RT 的问题。促销方式非常多,包括各种红包、满减、打折、优惠券等。实际上促销的接口逻辑非常复杂,在“双 11”备战的时候,面对这个复杂的接口,每轮链路压测促销服务都会发现问题,之后优化再压测,又发现新的问题。我们来一起看看遇到的各种问题以及是如何解决的。首先是压测出现接口严重不可用,这里可以看到 DB 查询频次高,响应很慢,流量一上来,这个接口就崩溃了。那怎么去排查原因和解决呢?
首先是 SQL 优化,用工具识别慢 SQL,即全链路跟踪系统 Lurker。
这张图我简单介绍一下。遇到 SQL 执行效率问题的时候,就看是不是执行到最高效的索引,扫表行数是不是很大,是不是有 filesort。有 ORDER BY 的时候,如果要排序的数据量不大或者已经有索引可以走到,在数据库的内存排序缓存区一次就可以排序完。如果一次不能排序完,那就先拿到 1000 个做排序,然后输出到文件,然后再对下 1000 个做排序,最后再归并起来,这就是 filesort 的大致过程,效率比较低。所以尽量要走上索引,一般类的查询降低到 2 毫秒左右可以返回。
其次是要读取很多优惠规则和很多优惠券,数据量大的时候 DB 是很难扛的,这时候我们要做缓存和一些预处理。特别是查询 DB 的效率不是很高的时候,尽量缓存可以缓存的数据、尽量缓存多一些数据。但如果做缓存,DB 和缓存数据的一致性是一个问题。在做数据查询时,首先要看本地缓存有没有开启,如果本地缓存没有打开,就去查分布式缓存,如果分布式缓存中没有就去查 DB,然后从 DB 获取数据过来。需要尽量保持 DB、缓存数据的一致性,如果 DB 有变化,可以异步地做缓存数据失效处理,数据百毫秒内就失效掉,减少不一致的问题。
另外,如果读到本地缓存,这个内存访问比走网络请求性能直接提升了一个量级,但是带来的弊端也很大,因为本地缓存没有办法及时更新,平时也不能打开,因为会带来不一致问题。但大促高峰期间我们会关闭关键业务数据变更入口,开启本地缓存,把本地缓存设置成一分钟失效,一分钟之内是可以缓存的,也能容忍短暂的数据不一致,所以这也是一个很好的做法。同样的思路,我们也会把可能会用到的数据提前放到缓存里面,做预处理。在客户端进行数据预处理,要么直接取本地数据,或者在本地直接做计算,这样更高效,避免了远程的 RPC。大促期间我们就把活动价格信息预先放到商品表中,这样部分场景可以做本地计价,有效解决了计价接口性能的问题。
再就是读容量问题,虽然缓存可以缓解压力,但是 DB 还是会有几十 K 的读压力,单点去扛也是不现实的,所以要把读写分离,如果从库过多也有延时的风险,我们会把数据库的并行复制打开。
我们来看一下数据。这是去年“双 11”的情况(如图)。促销服务的 RT 得到了有效控制,所以去年“双 11”平稳度过。
接下来讲一个更基础、更全局的优化,就是异步化。比如说下单的流程,有很多业务是非实时性要求的,比如下单送优惠券,如果在下单的时候同步做,时间非常长,风险也更大,其实业务上是非实时性或者准实时性的要求,可以做异步化处理,这样可以减少下单对机器数量的要求。另外是流量高峰期的一些热点数据。大家可以想象一下,下单的时候,一万个人竞争同一条库存数据,一万个节点锁在这个请求上,这是多么恐怖的事情。所以我们会有异步队列去削峰,先直接修改缓存中的库存数目,改完之后能读到最新的结果,但是不会直接竞争 DB,这是异步队列削峰很重要的作用。还有,数据库的竞争非常厉害,我们需要把大事务做拆分,尽量让本地事务足够小,同时也要让多个本地事务之间达到一致。
异步是最终达到一致的关键,异步的处理是非常复杂的。可以看一下这个场景(见图),这是一个 1-6 步的处理过程,如果拆分成步骤 1、2、3、4、end,然后到 5,可以异步地做;6 也一样,并且 5 和 6 可以并行执行。同时,这个步骤走下来链路更短,保障也更容易;步骤 5 和 6 也可以单独保障。所以异步化在蘑菇街被广泛使用。
异步化之后面临的困难也是很大的,会有分布式和一致性的问题。交易创建过程中,订单、券和库存要把状态做到绝对一致。但下单的时候如果先锁券,锁券成功了再去减库存,如果减库存失败了就是很麻烦的事情,因为优化券服务在另外一个系统里,如果要同步调用做券的回滚,有可能这个回滚也会失败,这个时候处理就会非常复杂。我们的做法是,调用服务超时或者失败的时候,我们就认为失败了,就会异步发消息通知回滚。优惠券服务和库存服务被通知要做回滚时,会根据自身的状态来判断是否要回滚,如果锁券成功了券就回滚,减库存也成功了库存做回滚;如果库存没有减就不用回滚。所以我们是通过异步发消息的方式保持多个系统之间的一致性;如果不做异步就非常复杂,有的场景是前面所有的服务都调用成功,第 N 个服务调用失败。另外的一致性保障策略包括 Corgi MQ 生产端发送失败会自动重试保证发成功,消费端接收 ACK 机制保证最终的一致。另外,与分布式事务框架比起来,异步化方案消除了二阶段提交等分布式事务框架的侵入性影响,降低了开发的成本和门槛。
另一个场景是,服务调用上会有一些异步的处理。以购物车业务为例,购物车列表要调用 10 个 Web 服务,每一个服务返回的时间都不一样,比如第 1 个服务 20 毫秒返回,第 10 个服务 40 毫秒返回,串行执行的效率很低。而电商类的大多数业务都是 IO 密集型的,而且数据量大时还要分批查询。所以我们要做服务的异步调用。比如下图中这个场景,步骤 3 处理完了之后 callback 马上会处理,步骤 4 处理完了 callback 也会马上处理,步骤 3 和 4 并不相互依赖,且处理可以同时进行了,提高了业务逻辑执行的并行度。目前我们是通过 JDK7 的 Future 和 Callback 实现的,在逐步往 JDK8 的 Completable Future 迁移。这是异步化在网站整体的应用场景,异步化已经深入到我们网站的各个环节。
刚才我们讲了链路容量的提升、促销 RT 的优化,又做了异步化的一些处理。那么优化之后怎么验证来优化的效果呢?到底有没有达到预期?我们有几个压测手段,如线下单机压测识别应用单机性能瓶颈,单链路压测验证集群水位及各层核? 系统容量配比,还有全链路压测等。
这是去年“双 11”之前做的压测(见图),达到了 5K 容量的要求。今年对每个点进一步深入优化,2016 年最大订单提升到了 10K,比之前提升了 25 倍。实际上这些优化可以不断深入,不仅可以不断提高单机的性能和单机的 QPS,还可以通过对服务整体上的优化达到性能的极致,并且可以引入一些廉价的机器(如云主机)来支撑更大的量。
我们为什么要做这些优化?业务的发展会对业务系统、服务框架提出很多很高的要求。因此,我们对 Tesla 做了这些改善(见图),服务的配置推送要更快、更可靠地到达客户端,所以有了新的配置中心 Metabase,也有了 Lurker 全链路监控,服务和服务框架的不断发展推动了网站其他基础中间件产品的诞生和发展。2015 年的下半年我们进行了一系列中间件的自研和全站落地。
我们得到了服务架构 1.5 的体系(见图),首先是用户服务在最底层,用户服务 1200K 的 QPS,库存 250K,商品服务 400K,营销 200K,等等。
接下来我们看一下这一阶段,Tesla 开始做服务管控,真正成为了一个服务框架。我们最开始做发布的时候,客户端、服务端由于做的只是初级的 RPC 调用,如果服务端有变更,客户端可能是几秒甚至数十秒才能拉到新配置,导致经常有客户投诉。有了对服务变更推送更高的要求后,我们就有了 Matabase 配置中心,服务端如果有发布或者某一刻崩溃了,客户端马上可以感知到,这样就完成了整个服务框架连接优化的改进,真正变成服务管控、服务治理框架的开端。
购买链路的稳定性提升
有了上面讲到的服务化改进和性能提升之后,是不是大促的时候看一看监控就行了?其实不是。大流量来的时候,万一导致整个网站崩溃了,一分钟、两分钟的损失是非常大的,所以还要保证服务是稳的和高可用的。只有系统和服务是稳定的,才能更好地完成业务指标和整体的经营目标。
下面会讲一下服务 SLA 保证的内容。
首先 SLA 体现在对容量、性能、程度的约束,包括程度是多少的比例。那么要保证这个 SLA 约束和目标达成,首先要把关键指标监控起来;第二是依赖治理、逻辑优化;第三是负载均衡、服务分组和限流;第四是降级预案、容灾、压测、在线演练等。这是我们服务的关键指标的监控图(见上图)。支付回调服务要满足 8K QPS,99% 的 RT 在 30ms 内,但是图中监控说明 SLA 未达到,RT 程度指标方面要优化。
服务的 SLA 保证上,服务端超时和限流非常重要。如果没有超时,很容易引起雪崩。我们来讲一个案例,有次商品服务响应变慢,就导致上层的其他服务都慢,而且商品服务积压了很多请求在线程池中,很多请求响应过慢导致客户端等待超时,客户端早就放弃调用结果结束掉了,但是在商品服务线程池线程做处理时拿到这个请求还会处理,客户都跑了,再去处理,客户也拿不到这个结果,最后还会造成上层服务请求的堵塞,堵塞原因缓解时产生洪流。
限流是服务稳定的最后一道保障。一个是 HTTP 服务的限流,一个是 RPC 服务的限流。我们服务的处理线程是 Tesla 框架分配的,所以服务限流可以做到非常精确,可以控制在服务级别和服务方法级别,也可以针对来源做限流。
我们做了这样一系列改造之后,服务框架变成了有完善的监控、有负载均衡、有服务分组和限流等完整管控能力的服务治理框架。服务分组之后,如果通用的服务崩溃了,购买链路的服务可以不受影响,这就做到了隔离。这样的一整套服务体系(如图)就构成了我们的服务架构 2.0,最终网站的可用性做到了 99.979%,这是今年 6 月份的统计数据。我们还会逐步把服务的稳定性和服务质量做到更好。
总结及下一步展望
最后总结一下,服务框架的体系完善是一个漫长的发展过程,不需要一开始就很强、什么都有的服务框架,最早可能就是一个 RPC 框架。服务治理慢慢随着业务量增长也会发展起来,服务治理是服务框架的重要组成部分。另外,Tesla 是为蘑菇街业务体系量身打造的服务框架。可以说服务框架是互联网网站架构的核心和持续发展的动力。选择开源还是自建,要看团队能力、看时机。我们要深度定制服务框架,所以选择了自研,以后可能会开源出来。
服务框架是随着业务发展不断演变的,我们有 1.0、1.5 和 2.0 架构的迭代。要前瞻性地谋划和实施,要考虑未来三年、五年的容量。有一些系统瓶颈可能是要提前解决的,每一个场景不一样,根据特定的场景选择最合适的方案。容量和性能关键字是一切可扩展、Cache、IO、异步化。目前我们正在做的是服务治理和 SLA 保障系统化,未来会做同城异地的双活。
谢谢大家!
感谢陈兴璐对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论