阿里已经不单单有电商业务,今天我们涉猎的非常广泛,布局也非常多。阿里从一家电商公司开始,如果业务已经覆盖到了各个行业,图为4年前,2015年的布局。
按照这样的业务发展速度,如果没有一套完整的技术体系支撑,势必会影响整个业务的发展。
可以看到我们的技术是分层的,在最上的是业务,中间部分是中间件、搜索和大数据等中台系统。整个的大中台体系就是中中间这层通用的技术能够快速支持上层业务的快速发展。只要是用发中台的技术体系,都能够在上面快速的搭建自己的业务。
整个中台包括部分如图,除了中间件、搜索,还有一些数据分析。中间件在业务研发的过程中起到非常重要的作用。这是在我们整个技术架构演变过程中,逐渐形成的一套体系。
淘宝从初创开始到今天,我们的技术架构总体上经历了四代:
第一代是基于LAMP的一套结构。
第二代是基于Java的应用架构。
第三代是基于分布式体系,构建出一整套的分布式架构。
第四代是基于IDC,不但应用能够分布,整数数据中心也能够分布。
那么接下来,我就从0开始,跟大家分享这一段历程。
LAMP结构
整个淘宝网从开始想去创建,到真正上线,总共经历了一个多月的时间。那这一个多月的时间都做了些什么呢?
第一件事情,我们开始做技术选型,决定我们后续怎么发展;第二件事情,如何在一个多月的时间,让我们的网站上线我们购买了一套基于LAMP架构的电商网站,并且拿到源代码,我们对其进行二次开发,比如界面的UI改动,上下title的改动,其中最大的改动就是我们对它的数据库做了读写分离。
Java架构
随着业务量的增长,就会发现一些瓶颈,主要来自于数据库。当时的数据库是MySQL4,还不够稳定,数据库经常会出现死机。因此,我们直接把数据库换成oracle,通过PHP和oracle直接去连接进行操作,但PHP不支持连接池,即使使用一些开源的PHP中间件,让PHP去连接oracle,还是非常不稳定,连接池的中间件经常卡死。
然后我们开始考虑将技术体系转成Java,因为Java在企业级的应用中,有着比较成熟的生态。转化的过程中也还是很坎坷的:第一,我们是一个线上正在运行的系统;第二,系统当时正在大规模的增长。所以说把系统替换成Java,最好的方法就是分块替换。同时发现,oracle的写入量还是比较大的,当时还做了一个search,把产品搜索和店铺搜索放到search里面,这样每一次的请求都打到数据库里面了,这样我们就完成了1.0架构到2.0架构的演进。
分布式架构
随着整个业务的发展,我们又迎来了新问题,洪峰流量给我们带来了巨大的挑战,电商行业在国内已经逐步开始盛行,我们的流量直线上涨,电商的人口红利也开始上涨。随着流量的上涨,我们面临着服务器和数据库的压力。
从技术角度来看,我们面临着两个问题:
首先是淘宝上描述商品的图片特别多,图片问题非常严重,主要取决于我们采用的是商用存储,成本非常高,最高级别版本也很难存储下我们所有的图片;
其次在淘宝上浏览的所有交易都有交易快照,这也是非常耗费存储成本的;
第三,虽然数据库替换成了oracle,随着数据量的增大,我们所有的请求都打到数据库上面,这时数据库的存储也快达到了极限,压力也非常大。
所以我们开始对架构升级。第一,我们增加了内存cache,cache主要是解决数据库压力过大的问题,我们自己研制了一套Key/Value 分布式缓存(TAIR),就是在数据库前端加了一个内存cache,缓解了我们数据库的压力。第二,我们增加了分布式文件系统(TFS),之前的文件系统在商用的时候,成本太高,服务器的量非常大,所以我们研发了自己的一套文件系统。
随着我们的业务量逐渐增大,人也越来越多,这就导致开发维护成本特别高,当时我们是all-in-one系统,所有人改所有的代码都是在这一个系统里面,就会出现以下问题:
技术团队规模500人左右,维护变得越来越复杂
单一War应用,应用包一直增长,更新业务特性越来越慢;数据逐步形成多个孤岛,无法拉通
基于传统应用开发架构,业务爆发,弹性不足,单点故障影响巨大
还有性能问题。随着前端业务量的增大,服务器逐渐增多的时候,oracle也出现了连接数的瓶颈。所以我们必须开始做新的架构,让整个架构往前走一步。
于是,我们迈向了3.0架构。系统进行拆分变小,拆分系统主要是把系统分层。把系统分成三类:
第一类是c类,是中心类,比如说会员、商品、店铺等等,基于这些中心上面开发各自的系统。比如说商品详情、交易下单;
还有一些公共的类,是p类,比如交易平台,这是从业务上进行拆分。几个比较知名的项目,比如千岛湖项目(拆分出交易中心、类目属性中心)、五彩石项目(拆分出店铺中心、商品中心、评价中心)。
伴随着技术架构的改动,我们的业务结构也开始进行改动,开始成立了相应的团队。上图的下半部分就介绍了我们架构的演变过程。开始时,是all in one,1~10在维护一个项目。第二个阶段是10~1000人维护的MVC架构,实现了前后端分离,各司其职。第三个阶段是RPC,就是把各个系统进行拆分,然后各个系统之间进行通信。第四个阶段就是SOA这样一个模式。
重点分享RPC的发展历程。我们把应用拆成才c类、p类、垂直团队类。这些应用本来就是在一个大应用里面,他们之间可以很自然地进行通信,可以直接进行相互调用。但是拆分之后,我们的应用如何进行相互调用?
我们开发了轻量级的HSF框架,它是基于Java interface 的RPC框架,使得开发系统时就像开发本地应用一样去正常的调用Java。使用这个框架可以真正远程的调用到其他的系统上面。
随着应用逐渐发展,我们会依赖中间件或者各个产品之间相互依赖。为了解决Jar包冲突的问题,我们研究出了Pandora容器。这个容器能把所有的加包做隔离,当发生Jar包冲突的时候,它知道优先加载哪个,这样,我们就把Jar包冲突的问题解决了,那服务发现怎么办呢?比如:一个应用A如何知道应用B里面有多少台机器呢,你的ip又是什么?最简单的方式就是静态列表,记录下ip,做轮询策略,先调用A的1号机,再调用2号机。这样就没法实现一个动态的发现。所以在这个过程中,我们有一个动态的配置中心(configserver),在你服务上线的时候,作为provider把服务放到configserver上去,当需要消费这个服务的时候,会看哪些服务可以调用,然后把这个列表拿到。
Configserver会自动把相应的provider的ip推送到consumer上面。然后consumer会自动发现provider的服务,然后彼此会发生一个相互调用关系。如果configserver挂掉之后,你的provider是挂不上去的,但是已经发上去的服务是不受影响的,因为configserver已经把相应的服务推送到consumer上面。当我们把分布式系统变得庞大之后,其实各个系统各司己职就好了。
Oracle其实也产生了性能瓶颈,而MySQL经过了多年发展,已经非常的成熟和稳定了。我们考虑把数据库做拆分,也就是去IOE。对MySQL进行拆分,其实就是按照一定的规则去分库分表。拆分首先就是读写分离,然后做垂直拆分,还有水平拆分。垂直拆分主要是按业务来拆分,当垂直拆分到一定程度之后,有一些大业务还是不能承担这样的数据量,我们只能水平做分库分表,做sharding的拆分。分库分表就基于某一些主键算,如果主键符合什么样的条件,就Sharding到什么服务器上面。但是让每一个业务系统去做的成本是非常高的,一定要有一个中间通用的东西,能够解决数据库水平拆分的问题。
所以我们开发了一套数据库的中间件叫TDDL。TDDL就是在中间件层面支持数据库的水平拆分,业务就是在写单库一样,你不需要感知太多的东西,但是我已经把数据分散到各个数据库里面去了。当时还有一个系统是CORONA,CORONA今天我们已经把他放到云上面去了,它遵循标标准的JDBC协议,应用在写代码的时候还是遵循标准的JDBC协议,完全不需要感知任何的东西,用的也是标准的JDBC包。把请求送到我们的server上面,server去做Sharding处理,整个对应用是完全没有感知的。
有了分库分表,我们如何把oracle的数据迁移到MySQL?对此,我们开发了几个中间件,第一个就是“愚公”,把oracle里面的数据通过“愚公”一点一点地迁移到MySQL里面去,放到各个库里面,同时保证我们的业务不受影响;当我们把数据库做分库分表之后,我们还需要在数据库和缓存之间做一些trigger,当数据变了,需要触发一个事件,可能以前我们需要通过写一个程序来实现,现在我们也沉淀了一套中间件系统——精卫,它会监听每个数据库的变化,当数据库每个记录发生变化之后,它就触发一个事件,接听到这个事件之后,业务方可以根据自己的业务需求写一个精卫的worker,然后放到精卫里面去,触发相应的逻辑。
最典型的就是触发cache逻辑。随着我们IDC架构之后,当我们的数据在单点写完之后,其他的地域如何感知到数据的变化呢?就是通过精卫这样一个系统实现的。当数据库变化了,精卫会触发失效cache,当业务请求再过来的时候,就会把cache里面的数据填充成最新的数据,然后能够让业务看到最新的数据,就不会出现当A单元数据变动了,然后B单元和C单元的cache没有生效的情况。
先是垂直拆分后是水平拆分,接下来就是对应用的拆分。应用拆分是通过HSF这个RPC框架解决应用之间的调用,解决同步调用,接下来还有异步调用。例如,我要创建一个订单,订单后面依赖了200多个系统,如果按照同步调用一步步进行下去,可能最终返回的返回时间会非常长,这时候怎么办呢?我们会选择并发,A去调用B、去调用C、去调用D的这种HSF直接调用。但这时存在一个问题,如果说下游依赖的200多个系统中有一个系统被挂起了,就使整个请求被挂起了,然后接下来就很难进行了,而且这时如果对方的系统出现了严重的问题,会使我后续的请求都被挂起,最终也会把我的系统拖垮。这个时候我们需要一个异步解耦的方式,那么就产生了消息中间件。
消息中间件就是当A要去调用的时候,然后就会发一个消息,然后下游的系统开始订阅这条消息,各自去处理各自的,处理完之后把结果返回给中间件,这样就完成了异步通信过程。那么如果其中某一个系统发生了问题,前端的交易系统创建订单的时候,它只要把消息发出去就不用管了,等所有的事情都处理完再回调它就可以了,就不会关注你如何调用。
图为分布式消息的处理过程。在内部我们主要用的消息是NOTIFY/METAQ。现在我们总体上都会在一个消息中间件上合并,其实并不需要这么多的中间件。应用场景就是分布式最终一致性、应用解耦、异步、并行等一系列问题。从整个物理部署也可以看出来,每个都是集群的,有name server、producer、consumer等,这又解决了一个稳定性问题。我们单点没有这个问题,随便一个server挂掉,其实我们整个还是可以通信的,不会影响到业务的稳定性。
随着我们整个分布式架构的演进,架构变得异常复杂,依赖关系也变得异常复杂。这时候我们就想能不能可视化线上的问题,方便我们知道究竟发生了什么、它们之间的调用关系和调用链路是什么样。于是乎就产生了分布式追踪(EAGLEEYE)。
有了EAGLEEYE,我们就能清楚地知道一个请求过来,是怎么样从入口一直传递到最后,中间都经历了什么,然后哪一块可能是有问题的。像图中这样报错位置会标红,我们就可以清晰的知道是哪个系统出的问题。我们不需要再像以前一样,大家各在排查自己的系统,导致我们处理问题的时间比较长。
异地多活
我们整个架构演进到了4.0架构,其实到分布式架构看似我们已经解决掉了业务上的问题。但是,我们会遇到新的问题,比如说资源问题、业务扩展性还有就是容灾问题。资源中最重要的问题就是资源受限,当我们的机房都在一个地方,这个地方并不能无限扩展,随着我们服务器数量越来越多,那么这个地方可能就放不下我们的服务器。
比如2013年我们买到机器之后,杭州的机房没有地方去放。随着我们搞双十一活动,伴随着销售额和秒级峰值都有很大的提升,我们的成本也会有一定的提升,我们最终也会遇到单地域资源的限制;第二个是扩展性,有些业务可能不能只在这一个地方部署,因为别人访问我会比较慢,需要部署到国外,这时候业务有一个异地部署的需求;第三个就是一个容灾的需求,毕竟天灾人祸都在所难免。比如说同样是在2013年,杭州是40度的高温,我们的机房差点被限电,还好最终没有限。但是这也给了我们一个警示,就是我们必须要对我们的架构进行演进。如果不演进的话,总有一天我们的资源会不够。
架构演进就是不把鸡蛋放到同一个篮子里面,我们开始把我们的业务划分出各个逻辑的单元,可以把它们放到各个地方,然后让我们整个系统分散到全球,各个系统之间也没有过强的依赖,当某一个地域出现问题之后,不会影响到其他地方,我们只需要把流量切换一下就可以。现在我们应用的就是这套架构。
我们按照业务的维度,把业务划分成各个逻辑单元。比如说第一个要做单元化的是交易单元,我们就把整个交易链路划分出来,放到各个逻辑单元里面,然后在水平方向上进行拆分,然后把数据再在水平上做一个区分。单元内的数据就不要发跨单元。如果跨单元就会出现一些问题,比如说容灾问题,如果A挂掉之后,可能不止影响到A,其他单元也会受到影响。如果发生跨单元调用,延时也会比较长,对于最终用户下单的体验也是非常差的。所以我们要遵循单元封闭的逻辑,让每一个单元内的调用关系都封闭在自己的单元内,不要发生跨单元。
在技术架构上,我们对技术做了一个分层,并且定了几个原则,除了单元封闭原则之外,还有全局路由,全局路由解决的是全局用户流量的分配,当一个用户流量进来之后,它会按照我们的路由规则分配到相应的单元里面去。当用户流量进来,会跳到CDN,CDN知道其属于哪个单元。然后到了某一单元之后,接入层会判断其是否属于这个单元,如果不属于,会让其跳到正确的单元里面去。如果属于这个单元就继续往下走,直到走到数据库这一层。如果数据库这一层出现了问题,我们做的是数据库写失败,也不能够让其写成功,所以数据要一致。
这时候对于数据一致又出现了新的问题,比如说我们按照买家订单写到各个单元里面,但是卖家如何发货呢?卖家其实是放到各个中心里面的,买家的所有下单数据都要同步到卖家这里。我们的模式就是最终一致性,就是对时间不是很敏感,我可以慢慢的同步,比如说,买家下了单,过了一两秒之后才能同步到卖家那里,其实这个时间是大家都能够接受的。对于强一致类型的数据,我们就跨单元,在同一个地点去捡数据。就是图上面红色部分——强中心依赖,所以这是我们交易链中核心——跨单元依赖。
我们整个单元化的项目经历了三年。2013年我们开始在杭州做了两个POC验证,验证一下同城按照这种逻辑单元隔离开来,看看能否调用成功。2014年我们在杭州、上海近距离的两个城市之间做了异地多活的尝试。2015年我们开始在千里之外的地域去部署三地四单元架构,其中一个单元是云单元,这是我们为了降低成本,我们开始使用云机器来搞我们双十一的大促。到2016年、2017年我们的单元数越来越多,分布的越来越广,每一个单元都可以做一些相应的尝试。
这就是我们整个异地多活的架构,异地多活解决的就是容灾问题、资源问题还有业务的扩展性问题。只要我们发现资源不够了,我们只需要创建一个新的单元,就可以把容量扩上去。首先调用就把资源统一了,建站平台通过调用就可以快速搭建一个单元。当这个单元通过全链路压测之后,我们整个单元就可以通入使用,这样容量的问题就得到了解决。那么容灾的问题通过全局路由就可以解决。当某个单元出了问题之后,我们只要快速的把流量切换出去就可以。业务扩展性是整个架构天然具备的,我们已经在千里之外把这个单元部署过了。
高可用问题也是我们面临的一个比较大的问题,在2013年以前双十一前几分钟的成功率是很低的,很多人是无法购物的。但是在2013年之后,通过全链路压侧这样一个技术,能够提前模拟双十一零点这一刻的洪峰流量,使得我们能够提前把问题解决掉,所以说整个购物体验越来越顺滑。高可用在整个双十一备战过程中起到一个非常核心的作用。
当我们的业务在分布式和异步化之后,而且流量猛烈上涨之后,我们遇到的最大问题是容量评估:就是我也不知道我需要准备多少机器来抗这些流量,我也不知道我上下游依赖的应用应该准备多少流量,因为我根本不太清楚我们之间详细的调用是什么样子的。用户来的时候不同的调用链路可能调用彼此的次数不一样,所以说容量是很难评估的。所以我们首先模拟双十一零点这一时刻的流量,把这个流量制造出来,看一下场景。通过我们制造的数据,提前把我们双十一的问题暴漏出来。
高可用体系本身就是一套体系,它们之间彼此依赖,是闭环的。比如说我们一个单元的容量只有十万,当我容量超过十万的时候该怎么办呢?其实在双十一的时候,如果数据超过十万,系统会出现一个页面告诉你正在排队——限流。限流是怎么产生的呢?其实就是为了使我们能够更好的服务于我们能够服务的用户范围。对一个业务设定了一个阈值之后,当流量超过了阈值,就开始进行限流。这个时候如果我想提升自己的弹性,应该把那些没有达到的阈值、也就是水位比较低的应用,把它的机器弹过来,弹到水位比较高的应用。
除了这些之外,其实我们的高可用还有很多,比如说关于容量能力,我刚才提到了压测,全链路和单链路,还有线上的单机压测,容量评估。静态架构有灰度发布、Eagleeye跟踪。运行态有xflush/alimonitor、业务防止损BCP/DCP/Holo、限流降级Sentinel、开关平台Switch、预案系统Preplan、流量调度Failover等,还有应用资源管理应用弹性伸缩Athena、资源调度Zeus、运行环境隔离Moses等。
这样我们高可用体系就形成一个闭环。其中一个场景就是,比如我们进行压测,这边会限流,这该怎么办呢?这时候弹性开始往外弹,把整个水位调匀,这样会使我们通过压测。第二个场景就是当我一个应用挂了之后,我把流量切到另外一个地方去了,就去触发限流,如果这时候我们还有资源,我们应该利用弹性,把水位弹上来。
新起点
云会变成如同水电煤一样的基础资源,越来越多的业务会在云上展现,这些业务中会有很多经历如同淘宝一样的发展,我们展,我们将加速这些业务的发展进程,创造更大价值,用技术驱动业务,把我们的技术能力输出到云上去。
现在我们不只服务于我们的双十一,我们也想为其它企业提供技术服务,用技术驱动他们,让他们也能只关心业务就好,不用去过多的关心底层是如何实现的。上面是一些在云端的产品,在阿里云上可以直接看到,像DRDS、EDAS、MQ等。
推荐阅读:
项目是如何死掉的?太过真实!
从零开始搭建创业公司后台技术栈
基于Spring Boot+Cloud构建微云架构
一个人学习、工作很迷茫?
点击阅读原文」加入我们的小圈子!