2014年7月29日星期二

Architect_014:淘宝十年架构演变(摘录+整理)

本文是根据《淘宝技术这十年》精心整理的文章,主要是整理了淘宝十年架构演变过程,着重记录了当时遇到的问题以及解决办法,淘宝不是神,它发展的每一步都遇到了我们曾经遇到或者将会遇到的问题,他们很好地解决了这些问题,才有了今天的淘宝。
这些经验值得记录整理下来,希望能对大家有所借鉴。
借用书中的一句话:“好的架构是进化来的,不是设计来的”。
借用书中的另一句话:“好的功能也是进化来的,不是设计来的”。

1. 2003年
(1)4月
购买了PHPAuction,一个符合LAMP(Linux+ Apache+MySQL+PHP)架构的拍卖应用。

问题:只有一个数据库,容量小,且读写都在一起,影响读写效率。
办法:将MySQL数据库改为一个主库、两个从库,并且读写分离。
好处:存储容量增加了,并且有了备份,数据安全性增加了,读写分离使得读写效率得以提升(写要比读更加消耗资源,分开后互不干扰)。

淘宝网架构图1.0:




(2)7月 
问题:商品搜索的功能占用数据库资源太大了(用like搜索的,很慢)。
办法:引入搜索引擎iSearch。

(3)10月
问题:一台服务器不堪重负。
办法:服务器由最初的一台变成了三台,一台负责发送Email、一台负责运行数据库、一台负责运行 WebApp。

(4)12月,注册用户23万,日均PV31万。

2. 2004年
(1)1月
问题:MySQL4在写数据的时候会把表锁住。
当Master同步数据到Slave的时候,会引起Slave写,这样在Slave 的读操作都要等待。
还有一点是会发生Slave上的主键冲突,经常会导致同步停止,这样,你发布的一些东西明明已经成功了,但就是 查询不到。另外,当年的MySQL不比如今的MySQL,在数据的容量和安全性方面也有很多先天的不足(和Oracle相比)。

问题:随着访问量和数据量的飞速上涨,流量和交易量的迅速上涨让MySQL撑不住了。
办法:更换数据库,把MySQL更换为Oracle。访问方式和SQL语法都要跟着变。

淘宝网架构图2.0: 


问题:Oracle数据库的性能和并发访问能力为何如此强大?
Oracle的性能和并发访问能力之所以如此强大,有一个关键性的设计——连接池,连接池中放的是长连接,是进程级别的,在创建进程的时候,它就要独占一部分内存空间。也就是说,这些连接数在固定内存的Oracle Server 上是有限的,任何一个请求只需要从连接池中取得一个连接即可,用完后释放,这不需要频繁地创建和断开连接,而连接的创 建和断开的开销是非常大的。

问题:长连接多了,会把数据库拖挂;短连接多了,连接用完释放,还是会浪费一定的资源。
办法:使用连接池代理服务:SQL Relay(用PHP写的),并进行Oracle数据库调优、SQL调优。

问题:数据量继续增长,本地存储无法满足。
办法:购买NetApp(Network Appliance,美国网域存储技术有限公司)的 NAS(Network Attached Storage,网络附属存储)作为数据库的存储设备,再加上Oracle RAC(Real Application Clusters,实时应用集群)来实现负载均衡。

问题:NAS的NFS(Network File System)协议传输的延迟很严重。
办法:购买Dell和EMC合作的SAN低端存储,性能一下提升了十几倍,这才比较稳定了。

问题:数据量继续增长,本地存储无法满足。
办法:存储的节点一拆二、二拆四。

(2) 3月
问题:SQL Relay 经常死锁,SQL Relay 进程Hung住。
办法:没有办法,只有重启。

问题:如何与银行网关进行对接?
虽然银行实现了网上支付,但是接口五花八门。用户付钱不一定能扣款成功,扣款成功不一定能够保证通知淘宝,通知淘宝不一定能保证通知到,通知到了不一定保证不重复通知。
办法:没有办法,只有一家一家的对接,然后慢慢产生了“支付宝”的雏形。
最终使用Notify消息通知中间件彻底解决了这个问题,见后面。

(4)5月
问题:Oracle数据库已经调优到极限了,但是SQL Relay的问题依然无法解决。
办法:换开发语言,把PHP换成Java。

问题:用什么办法把一个庞大的网站从PHP语言迁移到 Java?而且要求在迁移的过程中,不停止服务,原来系统的bugfix 和功能改进不受影响。
办法:给业务分模块,一个模块一个模块地渐进式替换。如用户模块, 老的member.taobao.com继续维护,不添加新功能,新功能在新的模块上开发,跟老的模块共用一个数据库,开发完毕之后放到 不同的应用集群上,另开一个域名member1.taobao.com,同时再替换老的功能,替换一个,就把老的模块上的功能关闭一个,逐渐把用户引导到member1.taobao.com,等所有的功能都替换完之 后,关闭member.taobao.com。

问题:Java MVC 框架选哪个?
办法:使用阿里巴巴自己的MVC 框架:WebX。其中页面模板:JSP + Velocity,持久层:ibatis + Hibernate,控制层:EJB + Spring。

问题:如何进一步缓解数据库的压力?
办法:使用搜索引擎 iSearch,把数据库里的数据dump(倾倒)成结构化的文本文件后,放在硬盘上,提供Web应用以约定的参数和语法来查询这些数据。这看起来不难,难的是数以亿计的信息,怎么做到快速更新呢? 这好比你做了一个网站,在百度上很快就能搜到,你一定很满意了。但如果你发布一件商品,在淘宝上过1个小时还搜不到,你肯定要郁闷了。另一个难点是如何保证非常高的容量和并发量?再往后面就要考虑断句和语义分析的问题,以及推荐算法等更加智能的问题。

淘宝网架构图3.0:


(3)7月
问题:数据量进一步上涨,Oracle RAC撑不住了。
办法:购买IBM小型机。存储方面,从EMC低端CX存储到Sun oem hds高端存储,再到EMC dmx高端存储,一级一级地往上跳。
至此,进入IOE时代:IBM 小型机、Oracle 数据库、EMC 存储。
“能用钱能解决的问题,都不是难题。”

(4)12月
注册会员400万,日均4千多万PV。

3. 2005年

问题:如何解决数据库的横向扩展性?
办法:分库分表。
一台Oracle的处理能力是有上限的,它的连接池有数量限制,查询速度与容量成反比。在数据量上亿、 查询量上亿的时候,就到它的极限了。要突破这种极限,最简单 的方式就是多用几个Oracle数据库。但一个封闭的系统做扩展, 不像分布式系统那样直接加机器就可以了。
我们的做法是把用户 的信息按照ID来存放到两个数据库中(DB1和DB2),把商品的信息和卖家信息放在两个对应的数据库中,把商品类目等通用信 息放在第三个库中(DBcommon)。这么做的目的是除了增加了 数据库的容量之外,还有一个就是做容灾,即万一一个数据库挂 了,整个网站上还有一半的商品可以买。

问题:分库分表后,数据如何路由?
比如我是一个买家,买的商品有DB1的,也有DB2的,要查看“我已买到的宝 贝”的时候,应用程序怎么办?必须到两个数据库中分别查询对应的商品。要按时间排序怎么办?两个库中“我已买到的宝贝” 全部查出来在应用程序中做合并。另外,分页怎么处理?关键字查询怎么处理?专业点的说法就是数据的Join没法做了。
办法:编写数据库路由框架:DBRoute,统一处理了数据的合并、排序、分 页等操作,让程序员像使用一个数据库一样操作多个数据库里的数据。

问题:分库分表后,数据如何容灾?

问题:系统如何精简代码。
办法:使用Spring替代EJB。

12月,注册用户1390万, 日均8931万PV。

4. 2006年

问题:除了搜 索引擎、分库分表,还有什么办法能提升系统的性能?
办法:缓存和CDN(内容分发网络)。

问题:哪些信息适合放到缓存中?
办法:使用基于Berkeley DB的缓存,把很多不太变动的只读信息放了进去。
一开始先把卖家的信息放里面,然后把商品属性放里面,再把店铺信息放里面,但是像商品详情这类字段太大的放进去受不了。

问题:大字段信息放到哪里?
说到商品详情,这个字段比较恐怖,有人统计过,淘宝商品详情打印出来平均有5米长,在系统里 其实放在哪里都不招人待见。它最早与商品的价格、运费等信息放在一个表中,拖慢了整张表的查询速度,而很多时候查询商品信息是不需要查看详情的。
办法:把商品详情单独放在数据库的另外一张表中,再往后,这个大字段被从数据库中请了出来,先是放入了缓存系统,到现在是放进了文件系统TFS中。

问题:商品详情中的浏览次数如何存储?
这个数字每刷新一次,页面就要“写入”存储一次,这种高频度实时更新的数据能用缓存吗?通常来说,这种是必须放进数据库的,但是把浏览次数写入数据库,发布上线1个小时后,数据库就挂掉了,每天几亿次的写入,数据库承受不了。
办法:在Apache上增加了一个模块,根本不经过Web容器,直接写入缓存。
  
问题:更新数据时,如何通知缓存?
办法:

问题:当商用CDN也无法支撑流量时怎么办?
办法:从商用CDN:ChinaCache到自建CDN网络:淘宝CDN。

淘宝网架构图4.0:

12月,注册用户,日均1.5亿PV。

5. 2007年

(1)6月

问题:海量图片存储问题。
淘宝对图片存储的需求:文件比较小,并发量高,读操作远大于写操作,访问随机,没有文件修改的操作,要求存储成本低,能容灾,能备份。
办法:使用用分布式存储系统,由于文件大小比较统 一,可以采用专有文件系统;由于并发量高,读写随机性强,需 要更少的I/O操作。考虑到成本和备份,需要用廉价的存储设备;考虑到容灾,需要能平滑扩容。

淘宝文件系统 TFS 1.0 架构:


集群由一对Name Server和多台Data Server构成,Name Server的两台服务器互为双机,这就是集群文件系统中管理节点的概念。在这个系统中:
  • 每个Data Server运行在一台普通的Linux主机上。
  • 以Block文件的形式存放数据文件(一个Block的大小一般为 64MB)。
  • Block存储多份是为了保证数据安全。
  • 利用ext3文件系统存放数据文件。
  • 磁盘raid5做数据冗余。
  • 文件名内置元数据信息,用户自己保存TFS文件名与实际文件的对照关系,这使得元数据量特别小。
淘 宝TFS文件系统在核心设计上最大的取巧在于:传统的集群系统中元数据只有1份,通常由管理节点来管理,因而很容易成为瓶颈。而对于淘宝网的用户来说,图片文件究竟用什么名字来保 存他们并不关心,因此,TFS在设计规划上考虑在图片的保存文件名上暗藏了一些元数据信息,例如,图片的大小、时间、访问 频次等信息,包括所在的逻辑块号。而在实际的元数据上,保存的信息很少,因此,元数据结构非常简单。仅仅只需要一个FileID 就能够准确定位文件在什么地方。由于大量的文件信息都隐藏在 文件名中,整个系统完全抛弃了传统的目录树结构,因为目录树 开销最大。拿掉后,整个集群的高可扩展性可极大地提高。实际上,这一设计理念和目前业界的“对象存储”较类似。

淘宝文件系统 TFS 1.3 架构:



工程师们重点改善了心跳和同步的性能,心跳和同步在几秒钟之内就可完成切换,同时进行了一些新的优化,包括元数据存储在内存中、清理磁盘空间等。性能上也做了如下优化:
  • 采用ext4文件系统,并且预分配文件,减少ext3等文件系统数据碎片带来的性能损耗。
  •  单进程管理单块磁盘的方式,摒除RAID5机制。
  •  带有HA机制的中央控制节点,在安全稳定和性能复杂度之 间取得平衡。
  •  缩减元数据大小,将更多的元数据加载入内存,提升访问速度。
  • 跨机架和IDC的负载均衡及冗余安全策略。
  • 完全平滑扩容。
整个图片服务机器的拓扑结构图

整个图片存储系统就像一个庞大的服务器,有处理单元、缓 存单元和存储单元。
在TFS前端,还部署着200多台图片文件服务器,用Apache实现,用于生成缩略图的运算,缩略图都是实时生成的。
图片文件服务器的前端则是一级缓存和二级缓存,前面还有全局负载均衡的设置,用于解决图片的访问热点问题。图片的访问热点一定存在,重要的是,让图片尽量在缓存中命中。目前,淘宝网在各个运营商的中心点设有二级缓存,整体系统中心店设有一级缓存,加上全局负载均衡,传递到后端TFS的流量就已经非常均衡和分散了,对前端的响应性能也大大提高。
大部分图片都尽量在缓存中命中,如果缓存中无法命中,则会在本地服务器上查找是否存有原图,并根据原图生成缩 略图,如果都没有命中,则会考虑去后台TFS集群文件存储系统上调取。因此,最终反馈到TFS集群文件存储系统上的流量已经被大大优化了。
淘宝网将图片处理与缓存编写成基于Nginx的模块,使用GraphicsMagick进行图片处理, 采用了面向小对象的缓存文件系统,前端有LVS+HA Proxy将原图和其所有的缩略图请求都调度到同一台Image Server。
在文件定位上,内存用Hash算法做索引,最多一次读盘。另外会有很多相同的图片重复上传上来,去除重复文件也是采用 Hash算法来做的。写盘方式则采用Append方式写,并采用了淘汰策略FIFO,主要考虑降低硬盘的写操作,没有必要进一步提高 Cache命中率,因为ImageServer和TFS位于同一个数据中心,读盘 效率还是非常高的。

TFS已经开源:code.taobao.org。

问题:分布式缓存问题。
办法:淘宝KV缓存系统:Tair
Tair包括缓存和持久化两种存储功能。Tair作为一个分布式系统,由一个中 心控制节点和一系列的服务节点组成,我们称中心控制节点为 Config Server,服务节点是Data Server。Config Server 负责管理所有的Data Server,维护Data Server的状态信息。Data Server 对外提供各种数据服务,并以心跳的形式将自身的状况汇报给Config Server。Config Server是控制点,而且是单点,目前采用一主一备 的形式来保证其可靠性。所有的Data Server 地位都是等价的。

Tair 的架构图: 



 Tair 部署架构图:




淘宝网架构图5.0:


12月,日均PV2.5亿。

6. 2008年 


问题:如何解决商品分类的问题?
办法:品牌、款式、材质等都可以叫做“属性”,属性是类 似Tag(标签)的一个概念,与类目相比更加离散、灵活,这样也缩减了类目的深度。
这个思想的提出一举解决了分类的难题!  并且最终成为了一个单独的服务模块:类目属性的服务(Category Server)。

问题:系统越来越臃 肿,业务的耦合性越来越高,开发的效率越来越低。
淘宝前台系统的业务量和代码量还是呈爆炸式的增长。业务方总在后面催,开发人员不够就继续招人,招来的人根本看不懂原来的业务,只好摸 索着在“合适的地方”加一些“合适的代码”,看看运行起来像那么回事后,就发布上线。
在这样的恶性循环中,系统出错的概率也逐步增长,常常是你改了商品相关的某些代码,发现交易出问题了,甚至你改了论坛上的某些代码,旺旺出问 题了。这让开发人员苦不堪言,而业务方还认为开发人员办事不力。
办法:肢解和重构系统,把复用性最高的一个模块:用户信息模块拆分出来,叫IC(User Information Center)。UIC只处理最基础的用户信息操作,例如getUserById、getUserByName等。

问题:主站系统容量已经到了瓶颈。
此时,商品数在1亿个以 上,PV在2.5亿个以上,会员数超过了5000万个。这时Oracle的连接池数量都不够用了,数据库的容量到了极限,即使上层系统加机器也无法继续扩容。
办法:只有把底层的基础服务继续拆分,从底层开始扩容,上层才能扩展,才能容纳以后三五年的增长。

把交易的底层业务拆分出来,叫交易中心(Trade Center,TC),TC只处理如创建订单、减库存、修改订单状态等原子型的操作。
把交易的上层业务拆分出来,叫交易管理(Trade Manager,TM),拍下一件普通商品要对订单、库存、物流进行操作,拍下虚拟商品不需要对物流 进行操作,这些在TM中完成。

这些拆分出来的基础服务为日后的淘宝商城的快速构建提供了良好的基础。 
最终拆分完成的架构图如下:

问题:拆分出来的各个系统之间如何交互?
办法:实时调用的中间件:高性能服务框架(HSF) + 异步消息通知的中间件:(Notify)

HSF功能
介绍
HSF旨在为淘宝的应用提供一个分布式的服务框架,HSF从分布式应用层面以及统一的发布/调用方式层面为大家提供支持,从而可以很容易地开发分布式的应用以及提供或使用公用功能模块,而不用考虑 分布式领域中的各种细节技术,例如,远程通讯、性能损耗、调 用的透明化、同步/异步调用方式的实现等问题。


HSF设计思想: 


服务调用者启动的时候向ConfigServer注册对哪些服务感兴趣(接口、版本),当服务提供者的信息变化时,ConfigServer向相应的感兴趣的服务调用者推送新的服务信息列表。
调用者在调用时则根据服务信息的列表直接访问相应的服务提供者,而无须经过 ConfigServer。
ConfigServer并不会把服务提供者的IP地址推送给服务的调用者,HSF框架会根据负载状况来选择具体的服务器,返回结果给调用者,这不仅统一了服务调用的方式,也实现了“软负载均衡”。
平时ConfigServer通过和服务提供者的心跳来感应服务提供者的存活状态。
服务集群对调用者来说是“统一”的,服务之间是“隔离”的,这保证了服务的扩展性和应用的统一性。 再加上HSF本身能提供的“软负载均衡”,服务层对应用层来说就是一片“私有云”了。
HSF框架以SAR包的方式部署到Jboss、Jetty或Tomcat下,在应用启动的时候,HSF服务随之启动。


Notify设计思想:

NotifyServer在ConfigServer上面注册消息服务,消息的客户端通过ConfigServer订阅消息服务。
某个客户端调用NotifyServer发 送一条消息,NotifyServer负责把消息发送到所有订阅这个消息的客户端。
为什么不使用现成的消息中间件?是因为发现,当消息数量上来之后,会有消息拥堵、顺序有错、消息丢失。
为了保证消息一定能发出,且对方也一定能收到,消息数据本身就需要记录下来,这些信息存放在数据库中。把要发出的通知存放到数据库中,如果 实时发送失败,再用一个时间程序来周期性地发送这些通知,系统记录下消息的中间状态和时间戳,这样保证消息一定能发出,也一定能通知到,且通知带有时间顺序,这些通知甚至可以实现事务性的操作。由于消息具有中间状态(已发送、未发送等),应用系统通过Notify可以实现分布式事务——BASE(基本可用,Basically Available、 软状态 Soft State、最终一致,Eventually Consistent)。 NotifyServer可以水平扩展,NotifyClient也可以水平扩展,数据 库也可以水平扩展,因此从理论上讲,这个消息系统的吞吐量是没有上限的,现在Notify系统每天承载了淘宝10亿次以上的消息 通知。

问题:制约系统规模的最重要的因素,数据库如何拆分?
办法:分布式数据访问层:TDDL。
原来的分库分表 + DBRoute 方案只是满足了当时的数据扩展需求,并且没有考虑其它需求。

TDDL所处的位置:

可以看出,是在JDBC之下,也就是说JDBC Driver会调用TDDL,TDDL负责与数据源交互。

TDDL实现了下面三个主要的特性:
  • 数据访问路由——将针对数据的读写请求发送到最合适的地方。
  • 数据的多向非对称复制——一次写入,多点读取。
  • 数据存储的自由扩展——不再受限于单台机器的容量瓶颈与速度瓶颈,平滑迁移。
问题:TDDL如何实现分布式Join(连接)?
在跨节点以后,简单的 Join会变成M×N台机器的合并,这个代价比原来的基于数据库的单机Join大太多了。

问题:TDDL如何实现高速多维度查询?
就像SNS中的消息系统,A发 给B一个消息,那么A要看到的是我发给所有人的消息,而B要看到的是所有人发给我的消息。这种多维度查询,如何能够做到高效快捷呢?

问题:TDDL如何实现分布式事务?
原始单机数据库中存在着大量的事务操作,在分布式以后,分布式事务的代价远远大于单机事务,那么这个矛盾也变得非常明显。

问题:用户登录系统A,然后进入系统B,登录信息如何保存?
办法:使用Session框架。

Session框架架构图:

Session框架实现了如下特性:
  • Session的客户端存储,将Session信息存储到客户端浏览器 Cookie中。
  • 实现服务端存储,减少Cookie使用,增强用户信息的安全 性,避免浏览器对Cookie数量和大小的限制。
  • Session配置统一管理起来,集中管理服务端Session和客户端 Cookie的使用情况,对Cookie的使用做有效的监管。
  • 支持动态更新,Session的配置动态更新。
问题:为什么这里还要提到用Cookie这种比较“落伍”的方式呢?
Cookie是放在客户端的,每一次HTTP请求都要提交到服务端,在访问量比较小的时候,采用Cookie避免了Session复制、硬件负载等高成本的情况。
但随着用户访问规模的提高,我们可以折算一下,一个Cookie大概是2KB的数据,也就是说,一次请求要提交到服务器的数据是网页请求数据,再加上2KB的Cookie,当有上亿个请求的时候,Cookie所带来的流量已经非常可观了, 而且网络流量的成本也越来越高。
以后采用了集中式的缓存区的Session方式。

没有评论: