靠谱 的软件外包伙伴

您的位置:首页 > 新闻动态 > 电商软件开发新进展:每秒订单数25倍提升

电商软件开发新进展:每秒订单数25倍提升

2016-09-28 11:26:43
 
1

电商系统发展中期面临的一般性问题

我总结了一下,一般电商系统发展到中期都会面临三个方面的问题(如图)。

第一方面是业务问题。比如,一开始做业务的时候可能很随意,一是并不考虑业务模型、系统架构,二是业务之间的耦合比较严重,比如交易和资金业务,有可能资金和外部第三方支付公司的交互状态耦合在交易系统里,这些非常不利于业务发展。

第二方面是系统问题。2014年我们面临单体应用,400人开发一个大应用,扩展性很差,业务比较难做。

第三方面是支撑问题,比如关于环境、开发框架和质量工具等。这些是电商系统发展到中期都会面临的问题,中期的概念是用户过了千万,PV过了1亿。

我们来看一下蘑菇街2015年初面临的问题。蘑菇街2015年用户过亿,PV过10亿,业务在超高速发展,每年保持3倍以上的增长。电商促销、交易、支付等业务形态都在快速膨胀,我们需要快速支持业务发展,而不是成为业务的瓶颈。那么就是要去做系统的拆分和服务化。

2

系统拆分与服务化过程

第二部分的内容,是关于蘑菇街系统拆分与服务化的历程。

按照如下几条思路(见上图),我们进行系统拆分以及服务化。最开始,大家在同一个应用里开发一些业务功能,都是选择速度最快的方式,所有的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初步的服务化体系之后,更进一步,我们一是要继续深入网站如资金等的服务化,二是要做服务内部的建设,比如容量、性能,这也是接下来要讲的内容。

3

购买链路的性能提升

这个链路(见图)是比较典型的电商链路,有商品页、下单、支付、营销和库存等内容。一开始每个点都有瓶颈,每个瓶颈都是一个篱笆,我们要正视它,然后翻越它。

我们先来看第一个篱笆墙:下单的瓶颈。

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年的下半年我们进行了一系列中间件的自研和全站落地。

返回首页] [打印] [返回上页]   下一篇