横向扩展(Facebook)

原文:Scaling Out

作者:Jason Sobel (notes)

翻译:ShiningRay

我于2007年四月加入了Facebook,在结束了几周的课程之后,我的经理Robert Johnson来找我。我们谈了很久,不过内容可以归结为:

Bobby: “那么,Jason,我们要在2008年之前在弗吉尼亚开一个新的数据中心。你能去帮点忙吗?”
Me: “呃…. 可以?”
Bobby: “很好!”

我在Facebook的第一个项目上投入的要比我预期的多一点点,但是我认为这是为何我们拥有如此一个非常强大的工程组织的原因;我们还有很多难题有待解决,这里每个人都迫不及待要立刻去解决他们。我开始了解为何我们需要建造一个新的数据中心以及我们需要解决什么问题才能让他正常工作。

有何必要?

在东海岸建造一个新的数据中心的主要原因就是“延迟”。在一个高速连接上发送一个包横穿大陆需要大概70微秒的时间,而对于普通的互联网用户而言,可能会需要的时间就长得多。通过将服务器放在弗吉尼亚,我们可以大大减少给东海岸和欧洲的用户传送网页的时间。

第二个关注点是空间、能源和灾难恢复。在我们位于加利福尼亚的主数据中心中已经没有多少物理空间了,而弗吉尼亚的点可以给我们充分的空间添加东西。我们还有一个类似问题就是要给予充足的电能驱动所有的服务器。最后,如果把我们限制在某个单独的地方,意味着如果出现灾难事件(断电、地震、怪兽),可能会导致Facebook长时间无法访问。

开始构建!

在我们能处理应用级的问题之前,我们的小组在弗吉尼亚投入了大量的心血构建服务器和物理空间。他们还完成了数据中心之间的网络和低延迟光线通道连接。这些工作是非常巨大的工程,然而我们顶尖的团队使之看上去像是小菜一碟。

网络和硬件都到位后,我们搭建了标准的3层架构:Web服务器,memcache服务器和MySQL数据库。在弗吉尼亚的MySQL数据库作为西海岸的数据库的从数据库(Slave)运行,所以我们花了几周的时间复制所有的数据,然后建立同步复制流(replication stream)。

现在硬件、网络和基础的设备都已经建立好,那现在就要面对两个主要的应用级的挑战:缓存一致性(Cache Consistency)和流量路径选择(traffic routing)。

缓存一致性

先说一下我们的缓存模型:当一个用户修改了数据对象后,我们的底层设施会向数据库写入新的值,并且从memcache中删除旧的值(如果存在)。下一次用户请求该用户对象的时候,我们从数据库中取出新的结果并写入memcache。后续的请求就会直接从从memcache中取出数据直到缓存过期或者被另外一次更新删除。

这种设置在只有一套数据库的时候运行得很好,因为我们只有当数据库完成了新值的写操作之后才删除memcache中的值。这种方式保证了我们能够从数据库中获得新值并且放入memcache中。然而,当在东海岸有一个从数据库后,情况就有些棘手了。

当我们在西海岸的主数据库中更新了一些数据之后,在东海岸的从数据库能正确反映这些新数值之前,中间有一个同步复制的延迟。通常这个延迟小于一秒钟,但是在高峰时期,它可能会延长到20秒。

现在我们假设在更新了加利福尼亚的主数据库的同时,我们从弗吉尼亚的memcache层中删除了旧值。然后有一个对弗吉尼亚的从数据库的读操作可能由于复制延迟还是看到的旧数值。然后弗吉尼亚的memcache可能会更新为旧的(不正确)的数值,然后它可能被“困住”直到被删除。如你所见,最差的情况是弗吉尼亚的memcache层可能总是同一个版本而非正确的数据。

考虑下面的例子:

  1. 我将我的名字从“Jason”改成了“Monkey”
  2. 我们把“Monkey”写入了加利福尼亚的主数据库并且从加利福尼亚和弗吉尼亚的memcache中删除了原来的名字
  3. 有个人在弗吉尼亚访问我的信息
  4. 在memcache中没有找到我的姓名信息,所哟我们从弗吉尼亚的从数据库中读取,由于复制的延迟获得了“Jason”
  5. 我们将姓名“Jason”存入弗吉尼亚的memcache
  6. 同步复制上来了,我们将名字信息在从数据库中更新为“Monkey”
  7. 另一个人在弗吉尼亚访问我的信息
  8. 我们在memcache中找到了名字并返回“Jason”。

在我再更新我的名字或者数据过期需要再访问数据库之前,我的名字在弗吉尼亚会一直显示为“Jason”,在加利福尼亚显示为“Monkey”。混乱吧?确实。欢迎来到分布式系统的世界,在这里一致性确实是一个难题。

幸好,解决方案要比问题容易解释。我们对MySQL做了一个小小的改动,让MySQL能在同步复制流中附加一个额外的信息。我们利用这个功能将要变更的所有数据对象追加到给定查询上,然后当从数据库“看到”这些对象后,要负责在进行了数据库更新后将这些值从缓存中删除。

我们是怎么做到的呢?MySQL是用了一个词法解析器和yacc语法来定义查询的结构然后对其进行解析。为了解释方便,我对其进行了简化,这个语法最顶层差不多如下:

很直观吧?一个query(查询)是一个能分解成某种我们熟知的MySQL表达式的statement(语句)。我们将这个语法修改为允许在任意查询后追加memcache键,如下:

查询现在可以有一个额外的组件;在语句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同步复制流。

新的工作流变成了(更改的内容为粗体):

  1. 我将我的名字从“Jason”改为“Monkey”。
  2. 我将“Monkey”写入加利福尼亚的主数据库并从加利福尼亚的memcache中删除我的名字,但不包括弗吉尼亚的memcache
  3. 某个人在弗吉尼亚访问了我信息。
  4. 在memcache中找到了我的名字,并返回“Jason”。
  5. 同步复制到了之后,将从数据库中我的名字更新为“Monkey”。还需要从弗吉尼亚的memcache中删除我的名字因为缓存对象出现在同步复制流中了。
  6. 另一个人在弗吉尼亚访问了我的信息
  7. 没有在memcache中找到我的名字,所以从从数据库读出名字,得到了“Monkey”。

页面路径选择

我们还需要解决的另一个主要的问题是只有在加利福尼亚州的主数据库才可能接受写操作。这个情况就是说我们需要避免在弗吉尼亚服务那些需要进行数据库写操作的页面,因为他们都需要穿越整个大陆访问我们在加利福尼亚的主数据库。幸好,我们最频繁访问的页面(首页、档案、照片页面)在正常情况下都不会进行写操作。这样这个问题就归结于,当一个用户请求某个页面时,我们怎么判断它是否可以被“安全”地送到弗吉尼亚,或者它必须被引导到加利福尼亚?

这个问题最后有一个比较直观的答案。某个用户请求Facebook时,命中了第一批服务器其中的一个,这个服务器称之为负载均衡器;该机器的主要职责是选择一个Web服务器来处理该请求,不过它也进行一些其他目的的服务:防御拒绝服务攻击,多路复用用户连接等。这个负载均衡器拥有可以在第7层模式运行的能力,这样他可以检查用户请求的URI并根据这个信息进行路由选择决定。这个特性意味着,我们可以很容易地告诉负载均衡器哪些是“安全”页面,然后可以根据页面的名字和用户的位置决定是否要将请求发送到弗吉尼亚或者是加利福尼亚。

不过,这里还有一点问题。假设你访问editprofile.php来更改家乡信息。该页面没有被标记为安全所以他被引导到了加利福尼亚,并且进行了更改。然后你访问你的档案页面,同时由于这个页面是安全页面,所以被引导到了弗吉尼亚。然而因为前面提到的同步复制延迟,你可能不能立刻看到你刚刚做过的改动!这种体验会令用户感到非常混乱,同时会导致双重提交。我们通过在浏览器中设置一个包含(有过写入数据库操作的)当前时间cookie来绕开这个问题。负载均衡器会查看该cookie,如果它注意到20秒内你写入了些东西,将无条件地传送到加利福尼亚。过了20秒之后,我们确保数据已经同步到弗吉尼亚,这时便允许你回来访问安全页面。

回顾

从我们第一个用户在弗吉尼亚数据中心访问页面后的九个月中我们一直在运行同样架构获得了很好的效果。当然,一路上还有挫折;在头一两个月中,缓存一致性的框架非常地不稳定,逼我们在诊断和修复错误的时候每隔一段时间就要把流量从弗吉尼亚转移出去。当然,过了一段时间,我们消灭了这个问题,现在这个数据中心在Facebook的流量中占了很大的比重。

这个架构中主要的伸缩方面的挑战很明显:所有的写操作必须在同一个地方发生。更进一步我们对开发新的可以让我们在任何位置进行写操作的技术非常感兴趣。我们也在思考如何将新的数据中心做成一个灾难恢复点,以防怪兽要进攻加利福尼亚!想来帮帮我们吗?www.facebook.com/jobs!

“横向扩展(Facebook)”的2个回复

  1. 之前在dbanotes上看过… :)
    看过《构建可扩展的 Web 站点》这本书后感觉更加强烈了….

发表评论

电子邮件地址不会被公开。 必填项已用*标注