我在Facebook的第一个项目上投入的要比我预期的多一点点,但是我认为这是为何我们拥有如此一个非常强大的工程组织的原因;我们还有很多难题有待解决,这里每个人都迫不及待要立刻去解决他们。我开始了解为何我们需要建造一个新的数据中心以及我们需要解决什么问题才能让他正常工作。
有何必要?在东海岸建造一个新的数据中心的主要原因就是“延迟”。在一个高速连接上发送一个包横穿大陆需要大概70微秒的时间,而对于普通的互联网用户而言,可能会需要的时间就长得多。通过将服务器放在弗吉尼亚,我们可以大大减少给东海岸和欧洲的用户传送网页的时间。
第二个关注点是空间、能源和灾难恢复。在我们位于加利福尼亚的主数据中心中已经没有多少物理空间了,而弗吉尼亚的点可以给我们充分的空间添加东西。我们还有一个类似问题就是要给予充足的电能驱动所有的服务器。最后,如果把我们限制在某个单独的地方,意味着如果出现灾难事件(断电、地震、怪兽),可能会导致Facebook长时间无法访问。
开始构建!在我们能处理应用级的问题之前,我们的小组在弗吉尼亚投入了大量的心血构建服务器和物理空间。他们还完成了数据中心之间的网络和低延迟光线通道连接。这些工作是非常巨大的工程,然而我们顶尖的团队使之看上去像是小菜一碟。
网络和硬件都到位后,我们搭建了标准的3层架构:Web服务器,memcache服务器和MySQL数据库。在弗吉尼亚的MySQL数据库作为西海岸的数据库的从数据库(Slave)运行,所以我们花了几周的时间复制所有的数据,然后建立同步复制流(replication stream)。
现在硬件、网络和基础的设备都已经建立好,那现在就要面对两个主要的应用级的挑战:缓存一致性(Cache Consistency)和流量路径选择(traffic routing)。
缓存一致性先说一下我们的缓存模型:当一个用户修改了数据对象后,我们的底层设施会向数据库写入新的值,并且从memcache中删除旧的值(如果存在)。下一次用户请求该用户对象的时候,我们从数据库中取出新的结果并写入memcache。后续的请求就会直接从从memcache中取出数据直到缓存过期或者被另外一次更新删除。
这种设置在只有一套数据库的时候运行得很好,因为我们只有当数据库完成了新值的写操作之后才删除memcache中的值。这种方式保证了我们能够从数据库中获得新值并且放入memcache中。然而,当在东海岸有一个从数据库后,情况就有些棘手了。
当我们在西海岸的主数据库中更新了一些数据之后,在东海岸的从数据库能正确反映这些新数值之前,中间有一个同步复制的延迟。通常这个延迟小于一秒钟,但是在高峰时期,它可能会延长到20秒。
现在我们假设在更新了加利福尼亚的主数据库的同时,我们从弗吉尼亚的memcache层中删除了旧值。然后有一个对弗吉尼亚的从数据库的读操作可能由于复制延迟还是看到的旧数值。然后弗吉尼亚的memcache可能会更新为旧的(不正确)的数值,然后它可能被“困住”直到被删除。如你所见,最差的情况是弗吉尼亚的memcache层可能总是同一个版本而非正确的数据。
考虑下面的例子:
在我再更新我的名字或者数据过期需要再访问数据库之前,我的名字在弗吉尼亚会一直显示为“Jason”,在加利福尼亚显示为“Monkey”。混乱吧?确实。欢迎来到分布式系统的世界,在这里一致性确实是一个难题。
幸好,解决方案要比问题容易解释。我们对MySQL做了一个小小的改动,让MySQL能在同步复制流中附加一个额外的信息。我们利用这个功能将要变更的所有数据对象追加到给定查询上,然后当从数据库“看到”这些对象后,要负责在进行了数据库更新后将这些值从缓存中删除。
我们是怎么做到的呢?MySQL是用了一个词法解析器和yacc语法来定义查询的结构然后对其进行解析。为了解释方便,我对其进行了简化,这个语法最顶层差不多如下:
1
2
3
4
5
6
7
8
9
等.)
很直观吧?一个query(查询)是一个能分解成某种我们熟知的MySQL表达式的statement(语句)。我们将这个语法修改为允许在任意查询后追加memcache键,如下:
1
2
3
4
5
6
7
8
9
10
11
查询现在可以有一个额外的组件;在语句statement之后有mc_dirty可以为空或者为一个关键词MEMCACHE_DIRTY后面跟着一个mc_key_list。一个mc_key_list只是一个逗号隔开的字符串列表,该规则会告诉解析器将所有字符串一个接一个存入某个叫做mc_key_list向量中,这个向量将被存入每查询解析器对象中。
看个例子,某个老式的查询看上去像:
REPLACE INTO profile (first_name) VALUES ('Monkey') WHERE user_id='jsobel'
在新语法下会变成:a
REPLACE INTO profile (first_name) VALUES ('Monkey') WHERE user_id='jsobel' MEMCACHE_DIRTY 'jsobel:first_name'
新的查询会告诉MySQL,除了要将我的名字更改为Monkey外,它还需要将一个对应的memcache键设脏。这很容易实现。由于每对象解析器对象现在储存了所有的memcache键,我们在mysql_execute_command最后添加了一小段代码——如果查询成功了,就设脏这些键。看看,我们成功地按照我们的目的——缓存一致性——劫持了MySQL同步复制流。
新的工作流变成了(更改的内容为粗体):
页面路径选择我们还需要解决的另一个主要的问题是只有在加利福尼亚州的主数据库才可能接受写操作。这个情况就是说我们需要避免在弗吉尼亚服务那些需要进行数据库写操作的页面,因为他们都需要穿越整个大陆访问我们在加利福尼亚的主数据库。幸好,我们最频繁访问的页面(首页、档案、照片页面)在正常情况下都不会进行写操作。这样这个问题就归结于,当一个用户请求某个页面时,我们怎么判断它是否可以被“安全”地送到弗吉尼亚,或者它必须被引导到加利福尼亚?
热门源码