Flickr.com 是网上最受欢迎的照片共享网站之一,还记得那位给Windows Vista拍摄壁纸的Hamad Darwish吗?他就是将照片上传到Flickr,后而被微软看中成为Vista壁纸御用摄影师。
Flickr.com 是最初由位于温哥华的Ludicorp公司开发设计并于2004年2月正式发布的,由于大量应用了WEB 2.0技术,注重用户体验,使得其迅速获得了大量的用户,2007年11月,Flickr迎来了第20亿张照片,一年后,这个数字就达到了30亿,并且还 在以加速度增长。 2005年3月,雅虎公司以3千500万美元收购了Ludicorp公司和Flickr.com。虽然Flickr并不是最大的照片共享网站 (Facebook以超过100亿张照片排名第一),但这笔收购仍然被认为是WEB 2.0浪潮中最精明的收购,因为仅仅一年后,Google就以16亿美元的高价收购了YouTube,而2007年10月,微软斥资2.4亿美元收购 Facebook 1.6%股份,此举使Facebook估值高达150亿美元。估计Ludicorp公司的创始人Stewart Butterfield和Caterina Fake夫妇现在还在后悔吧。
在2005年温哥华PHP协会的简报以及随后的一系列会议上,Flickr的架构师Cal Henderson公开了大部分Flickr所使用的后台技术,使得我们能有机会来分享和研究其在构建可扩展Web站点的经验。本文大部分资料来自互联网和自己的一点点心得,欢迎大家参与讨论,要是能够起到抛砖引玉的作用,本人将不胜荣幸。
Flickr整体框架图
在讨论Flickr 网站架构之前,让我们先来看一组统计数据(数据来源:April 2007 MySQL Conf and Expo和Flickr网站)
- 每天多达40亿次的查询请求
- squid总计约有3500万张照片(硬盘+内存)
- squid内存中约有200万张照片
- 总计有大约4亿7000万张照片,每张图片又生成不同尺寸大小的4-5份图片
- 每秒38,000次Memcached请求 (Memcached总共存储了1200万对象)
- 超过2 PB 存储,其中数据库12TB
- 每天新增图片超过 40万(周日峰值超过200万,约1.5TB)
- 超过8百50万注册用户
- 超过1千万的唯一标签(tags)
- 响应4万个照片访问请求
- 处理10万个缓存操作
- 运行13万个数据库查询
这张是Flickr的网站架构图,我们这里只作一些简要的描述,具体的分析请静待后续文章。
- Pair of ServerIron's做负载均衡方案
- Squid Caches 代理,用于缓存静态的HTML和照片
- Net App公司的Filer, NAS存储设备,用于存储照片
- PHP App Servers
- 运行REDHAT LINUX,Apache上的PHP应用,Flickr网站的主体是大约6万行PHP代码
- 没有使用PHP session, 应用是stateless,便于扩展,并避免PHP Server故障所带来的Session失效。
- 每个页面有大约27~35个查询(不知道为什么这么设计,个人觉得没有必要)
- 另有专门的Apache Web Farm 服务于静态文件(HTML和照片)的访问 - Storage Manager 运行私有的,适用于海量文件存储的Flickr File System
- Dual Tree Central Database
- MySQL 数据库,存放用户表,记录的信息是用户主键以及此用户对以的数据库Shard区,从中心用户表中查出用户数据所在位置,然后直接从目标Shard中取出数据。
- “Dual Tree"架构是”Master-Master"和“Master-Slave"的有效结合,双Master 避免了“单点故障”,Master-Slave又提高了读取速度,因为用户表的操作90%以上是读。 - Master-master shards
- MySQL 数据库,存储实际的用户数据和照片的元数据(Meta Data),每个Shard 大约40万个用户,120GB 数据。每个用户的所有数据存放在同一个shard中。
- Shard中的每一个server的负载只是其可最大负载的50%,这样在需要的时候可以Online停掉一半的server进行升级或维护而不影响系统性能。
- 为了避免跨Shard查询所带来的性能影响,一些数据有在不同的Shard有两份拷贝,比如用户对照片的评论,通过事务来保证其同步。 - Memcached Cluster 中间层缓存服务器,用于缓存数据库的SQL查询结果等。
- Big Search Engine
- 复制部分Shard数据(Owner’s single tag)到Search Engine Farm以响应实时的全文检索。
- 其他全文检索请求利用Yahoo的搜索引擎处理(Flickr是Yahoo旗下的公司) - 服务器的硬件配置:
- Intel或AMD 64位CPU,16GB RAM
- 6-disk 15K RPM RAID-10.
- 2U boxes. - 服务器数量:(数据来源:April 2008 MySQL Conference & Expo)
166 DB servers, 244 web servers(不知道是否包括 squid server?), 14 Memcached servers
数据库最初的扩展-Replication
也许有人不相信,不过Flickr确实是从一台服务器起步的,即Apache/PHP和MySQL是运行在同一台服务器上的,很快MySQL服务器就独立 了出来,成了双服务器架构。随着用户和访问量的快速增长,MySQL数据库开始承受越来越大的压力,成为应用瓶颈,导致网站应用响应速度变慢,MySQL 的扩展问题就摆在了Flickr的技术团队面前。
不幸的是,在当时,他们的选择并不多。一般来说,数据库的扩展无外是两条路,Scale-Up和Scale-Out,所谓Scale-Up,简单的说就是 在同一台机器内增加CPU,内存等硬件来增加数据库系统的处理能力,一般不需要修改应用程序;而Scale-Out,就是我们通常所说的数据库集群方式, 即通过增加运行数据库服务器的数量来提高系统整体的能力,而应用程序则一般需要进行相应的修改。在常见的商业数据库中,Oracle具有很强的 Scale-Up的能力,很早就能够支持几十个甚至数百个CPU,运行大型关键业务应用;而微软的SQL SERVER,早期受Wintel架构所限,以Scale-Out著称,但自从几年前突破了Wintel体系架构8路CPU的的限制,Scale-Up的 能力一路突飞猛进,最近更是发布了SQL 2008在Windows 2008 R2版运行256个CPU核心(core)的测试结果,开始挑战Oracle的高端市场。而MySQL,直到今年4月,在最终采纳了GOOGLE公司贡献 的SMP性能增强的代码后,发布了MySQL5.4后,才开始支持16路CPU的X86系统和64路CPU的CMT系统(基于Sun UltraSPARC 的系统)。
从另一方面来说,Scale-Up受软硬件体系的限制,不可能无限增加CPU和内存,相反Scale-Out却是可以"几乎"无限的扩展,以Google 为例,2006年Google一共有超过45万台服务器(谁能告诉我现在他们有多少?!);而且大型SMP服务器的价格远远超过普通的双路服务器,对于很 多刚刚起步或是业务增长很难预测的网站来说,不可能也没必要一次性投资购买大型的硬件设备,因而虽然Scale-Out会随着服务器数量的增多而带来管 理,部署和维护的成本急剧上升,但确是大多数大型网站当然也包括Flickr的唯一选择。
经过统计,Flickr的技术人员发现,查询即SELECT语句的数量要远远大于添加,更新和
删除的数量,比例达到了大约13:1甚至更多,所以他们采用了“Master-Slave”的复制模式,即所有的“写”操作都在发生在“Master", 然后”异步“复制到一台或多台“Slave"上,而所有的”读“操作都转到”Slave"上运行,这样随着“读”交易量的增加,只需增加Slave服务器 就可以了。
让我们来看一下应用系统应该如何修改来适应这样的架构,除了”读/写“分离外,对于”读“操作最基本的要求是:1)应用程序能够在多个”Slave“上进 行负载均分;2)当一个或多个”slave"出现故障时,应用程序能自动尝试下一个“slave”,如果全部“Slave"失效,则返回错误。 Flickr曾经考虑过的方案是在Web应用和”Slave“群之间加入一个硬件或软件的”Load Balancer“,如下图
这样的好处是应用所需的改动最小,因为对于应用来说,所有的读操作都是通过一个虚拟的Slave来进行,添加和删除“Slave"服务器对应用透 明,Load Balancer 实现对各个Slave服务器状态的监控并将出现故障的Slave从可用节点列表里删除,并可以实现一些复杂的负载分担策略,比如新买的服务器处理能力要高 过Slave群中其他的老机器,那么我们可以给这个机器多分配一些负载以最有效的利用资源。一个简单的利用Apache proxy_balancer_module的例子如下:
1
2
3
4
5
6
7
8
9
10
11
|
<strong>。。。。。。。。。。。。。。 LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_balancer_module modules/mod_proxy_balancer.so LoadModule proxy_http_module modules/mod_proxy_http.so 。。。。。。。。。。。。。。。。。。。。 <Proxy balancer://mycluster> BalancerMember "http://slave1:8008/App" loadfactor=4 BalancerMember "http://slave2:8008/App" loadfactor=3 BalancerMember "http://slave3:8008/App" loadfactor=3 .................... ///slave load ratio 4:3:3. </strong> |
最终,Flickr采用了一种非常“轻量”但有效的“简易”PHP实现,基本的代码只有10几行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function db_connect( $hosts , $user , $pass ){ shuffle( $hosts ); //shuffle()是PHP函数,作用是将数组中每个元素的顺序随机打乱。 foreach ( $hosts as $host ){ debug( "Trying to connect to $host..." ); $dbh = @mysql_connect( $host , $user , $pass , 1); if ( $dbh ){ debug( "Connected to $host!" ); return $dbh ; } debug( "Failed to connect to $host!" ); } debug( "Failed to connect to all hosts in list - giving up!" ); return 0; } |
在上述代码中,如果需要对特定的Slave赋予更高的负载,只要在$hosts中多出现一次或多次就可以了。这段代码只要稍稍改进,就可以实现更复杂的功能,如当connect失败时自动将host从hosts列表中去除等。
“Master”-"Slave"模式的缺点是它并没有对于“写'操作提供扩展能力,而且存在单点故障,即一旦Master故障,整个网站将丧失“更新” 的能力。解决的办法采用“Master"-"Master"模式,即两台服务器互为”Master“-"Slave",这样不仅”读/写“能力扩展了一 倍,而且有效避免了”单点故障“,结合已有的“Master"-"Slave",整个数据库的架构就变成了下面的”双树“结构,
“双树”架构并没有支撑太久的时间,大概6个月后,随着用户的激增,系统再一次达到了极限,不仅”写”操作成为了瓶颈,而且“异步复制"也由于 ”Slave“服务器过于繁忙而出现了严重的滞后而造成读数据的不一致。那么,能不能在现有架构加以解决,比如说增加新的”Master“服务器和考虑采 用”同步复制“呢?答案是否定的,在Master超过两台的设置中,只能采用”闭环链“的方式进行复制,在大数据量的生产环境中,很容易造成在任意时刻没 有一个Master或Slave节点是具有全部最新数据的(有点类似于”人一次也不能踏进同一条河“?),这样很难保障数据的一致性,而且一旦其中一个Master出现故障,将中断整个复制链;而对于”同步复制“,当然这是消除”复制滞后“的最好办法,不过在当时MySQL的同步复制还远没有成熟到可以运用在投产环境中。
Flickr网站的架构,需要一次大的变化来解决长期持续扩展的问题。
Shard - 大型网站数据库扩展的终极武器?
2005年7月,另一位大牛(MySQL 2005、2006年度 "Application of the Year Award"获得者)Dathan Pattishall加入了Flickr团队。一个星期之内,Dathan解决了Flickr数据库40%的问题,更重要的是,他为Flickr引进了 Shard架构,从而使Flickr网站具备了真正“线性”Scale-Out的增长能力,并一直沿用至今,取得了巨大的成功。
Shard主要是为了解决传统数据库Master/Slave模式下单一Master数据库的“写”瓶颈而出现的,简单的说Shard就是将一个大表分 割成多个小表,每个小表存储在不同机器的数据库上,从而将负载分散到多个机器并行处理而极大的提高整个系统的“写”扩展能力。相比传统方式,由于每个数据 库都相对较小,不仅读写操作更快,甚至可以将整个小数据库缓存到内存中,而且每个小数据库的备份,恢复也变得相对容易,同时由于分散了风险,单个小数据库 的故障不会影响其他的数据库,使整个系统的可靠性也得到了显著的提高。
对于大多数网站来说,以用户为单位进行Shard分割是最合适不过的,常见的分割方法有按地域(比如邮编),按Key值(比如Hash用户ID),这些 方法可以简单的通过应用配置文件或算法来实现,一般不需要另外的数据库,缺点是一旦业务增加,需要再次分割Shard时要修改现有的应用算法和重新计算所 有的Shard KEY值;而最为灵活的做法是以“目录”服务为基础的分割,即在Shard之前加一个中央数据库(Global Lookup Cluster),应用要先根据用户主键值查询中央数据库,获得用户数据所在的Shard,随后的操作再转向Shard所在数据库,例如下图:
而应用的主要修改在于要添加一个Lookup访问层,例如将以下的代码:
1
2
3
|
string connectionString = @"Driver={MySQL};SERVER=dbserver;DATABASE=CustomerDB;" ; OdbcConnection conn = new OdbcConnection(connectionString); conn.Open(); |
变为:
1
2
3
|
string connectionString = GetDatabaseFor(customerId); OdbcConnection conn = new OdbcConnection(connectionString); conn.Open(); |
GetDatabaseFor()函数完成根据用户ID获取Shard connectionString的作用。
对应我们前面所提到过的Flickr架构
Dual Tree Central Database就是中央数据库,存放用户表,记录的信息是用户主键以及此用户对以的数据库Shard区;而Master-Master Shards就是一个个的Shard用户数据库,存储实际的用户数据和照片的元数据(Meta Data)。
Flickr Shard的设计我们在Flickr 网站架构研究(1)中已经总结过了,在此不再赘述。我们在此谈一下Shard架构的主要问题和Flickr的解决办法:1)Shard只适用于不需要 join操作的表,因为跨Shard join操作的开销太大,解决的办法是将一个用户的所有数据全部存放在同一个Shard里,对于一些传统方式下需要 跨Shard查询的数据,只能采取冗余的方法,比如Shard1的用户A对Shard2的用户B的照片进行了评论,那么这条评论将同时存放在Shard1 和Shard2中。这样就存在一个数据一致性的问题,常规的做法是用数据库事务(Transaction)、”两阶段提交“(2 phase commit)来解决,但做过两阶段提交(2PC)应用的都知道,2PC的效率相对较差,而且实际上也不能100%保证数据的完整性和一致性;另外,一旦 由于其中一个Shard故障而提交失败回滚,用户只能放弃或再试一遍,用户体验较差。Flickr对于数据一致性的解决方案是Queue(Flickr用 PHP开发了一个强大的Queue系统,将所有可以异步的任务都用Queue来实现,每天处理高达1千万以上的任务。),事实上当用户A对用户B的照片进 行评论时,他并不关心这条评论什么时候出现在用户B的界面上,即将这条评论添加到用户B的交易是可以异步的,允许一定的迟延,通过Queue处理,既保证 了数据的一致性,又缩短了用户端的相应时间,提高了系统性能。2)Shard的另一个主要问题Rebalancing,既当现有Shard的负载达到一定 的阀值,如何将现有数据再次分割,Flickr目前的方式依然是手工的,既人工来确定哪些用户需要迁移,然后运行一个后台程序进行数据迁移,迁移的过程用 户账户将被锁住。(据说Google做到了完全自动的Rebalancing,本着”萨大“坑里不再挖坑的原则,如果有机会的话,留到下一个系列再研究 吧)
Memcached的应用和争论
大家应该已经注意到,Flickr为中央数据库配置了Memcached作为数据库缓存,接下来的问题是,为什么用Memcached?为什么 Shard不需要Memcached?Memcached和Master,Slave的关系怎样?笔者将试图回答这些问题供大家参考,网上的相关争论很 多,有些问题尚未有定论。
Memecached是一个高性能的,分布式的,开源的内存对象缓存系统,顾名思义,它的主要目的是将经常读取的对象放入内存以提高整个系统,尤其是数 据库的扩展能力。Memcached的主要结构是两个Hash Table,Server端的HashTable以key-value pair的方式存放对象值,而Client端的HashTable的则决定某一对象存放在哪一个Memcached Server.举个例子说,后台有3个Memecached Server,A、B、C,Client1需要将一个对象名为”userid123456“,值为“鲁丁"的存入,经过Client1的Hash计 算,"userid123456"的值应该放入Memcached ServerB, 而这之后,Client2需要读取"userid123456"的值,经过同样的Hash计算,得出"userid123456"的值如果存在的话应该在 Memcached ServerB,并从中取出。最妙的是Server之间彼此是完全独立的,完全不知道对方的存在,没有一个类似与Master或Admin Server的存在,增加和减少Server只需在Client端"注册"并重新Hash就可以了。
Memcached作为数据库缓存的作用主要在于减轻甚至消除高负载数据库情况下频繁读取所带来的Disk I/O瓶颈,相对于数据库自身的缓存来说,具有以下优点:1)Memecached的缓存是分布式的,而数据库的缓存只限于本机;2)Memcached 缓存的是对象,可以是经过复杂运算和查询的最终结果,并且不限于数据,可以是任何小于1MB的对象,比如html文件等;而数据库缓存是以"row"为单 位的,一旦"row"中的任何数据更新,整个“row"将进行可能是对应用来说不必要的更新;3)Memcached的存取是轻量的,而数据库的则相对较 重,在低负载的情况下,一对一的比较,Memcached的性能未必能超过数据库,而在高负载的情况下则优势明显。
Memcached并不适用于更新频繁的数据,因为频繁更新的数据导致大量的Memcached更新和较低的缓冲命中率,这可能也是为什么Shard没 有集成它的原因;Memcached更多的是扩展了数据库的”读“操作,这一点上它和Slave的作用有重叠,以至于有人争论说应该 让"Relication"回到它最初的目的”Online Backup"数据库上,而通过Memcached来提供数据库的“读”扩展。(当然也有人说,考虑到Memcached的对应用带来的复杂性,还是慎 用。)
然而,在体系架构中增加Memecached并不是没有代价的,现有的应用要做适当的修改来同步Memcached和数据库中的数据,同时Memcached不提供任何冗余和“failover”功能,这些复杂的控制都需要应用来实现。基本的应用逻辑如下:
对于读操作:
1
2
3
4
5
|
$data = memcached_fetch( $id ); return $data if $data $data = db_fetch( $id ); memcached_store( $id , $data ); return $data ; |
对于写操作:
1
2
|
db_store( $id , $data ); memcached_store( $id , $data ); |
我们看到在每一次数据更新都需要更新Memcached,而且数据库或Memcached任何一点写错误应用就可能取得“过期”的数据而得到错误的结果,如何保证数据库和Memcached的同步呢?
复制滞后和同步问题的解决
我们知道复制滞后的主要原因是数据库负载过大而造成异步复制的延迟,Shard架构有效的分散了系统负载,从而大大减轻了这一现象,但是并不能从根本上消除,解决这一问题还是要靠良好的应用设计。
当用户访问并更新Shard数据时,Flickr采用了将用户“粘”到某一机器的做法,
$id = intval(substr($user_id, -10));
$id % $count_of_hosts_in_shard
即同一用户每次登录的所有操作其实都是在Shard中的一个Master上运行的,这样即使复制到Slave,也就是另一台Master的时候有延时,也不会对用户有影响,除非是用户刚刚更新,尚未复制而这台Master就出现故障了,不过这种几率应该很小吧。
对于Central Database的复制滞后和同步问题,Flickr采用了一种复杂的“Write Through Cache"的机制来处理:
"Write Through Cache"就是将所有的数据库”写“操作都先写入”Cache",然后由Cache统一去更新数据库的各个Node,“Write Through Cache"维护每一个Node的更新状态,当有读请求时,即将请求转向状态为”已同步“的Node,这样即避免了复制滞后和Memcached的同步问 题,但缺点是其实现极为复杂,“Write Throug Cache"层的代码需要考虑和实现所有”journal","Transaction“,“failover”,和“recovery"这些数据库已经 实现的功能,另外还要考虑自身的"failover"问题。我没有找到有关具体实现的说明,只能猜测这一部分的处理可能也是直接利用或是实现了类似于 Flickr的Queue系统吧。