本文介绍了Instagram从AWS迁移到Facebook基础架构的过程中面临的由多数据中心带来的挑战和解决方法。
在2013年,大约是我们加入Facebook一周年后,每个月有2亿人使用Instagram而且我们存储了200亿照片。毫不犹豫的,我们开始了“Instagration”——从AWS服务器移动到Facebook的基础架构。
两年后,Instagram已经成长为月活4亿有400亿照片和视频的社区,每秒服务超过100万请求。为了保持对这种增长的支持和确保社区在Instagram上有一个可靠的体验,我们决定在地理上扩展我们的基础架构。
本文将讨论为什么我们要将基础架构从一个数据中心扩展到三个,和在扩展中遇到的一些技术挑战。
动机
Mike Krieger, Instagram的联合创始人和CTO,近期写了一篇文章,文章中提到了一个故事,大约在2012年的时候,弗吉尼亚州的一场飓风瘫痪了将近一半的(服务器)实例。
在接下来的36小时里,这个小团队重建了几乎我们全部的基础架构,这种体验是他们永远不想重复的。
像这样的自然灾害有可能对数据中心造成临时的和永久的伤害——我们需要保证在用户体验上有最小的损失。
其他的在地理上扩容的动机包括:
区域故障的恢复: 比自然灾害更加常见的是网络短线、电力问题,等等。例如在我们扩展我们的服务到俄勒冈州不久,我们的一个基础构件,包括memecache和异步层服务器,被关机了,导致了用户请求的大规模一场。
在我们的新架构下,我们能够将流量从该区域转移走,以减轻我们在从电力故障中恢复时的问题。
弹性容量扩展: Facebook有不少数据中心。当我们的基础架构准备好扩展到一个区域甚至当网络上有不小的延迟时,可以非常容易的将Instagram的容量扩展到所有可用的容量中。这帮助我们快速决定为用户准备好新的功能而不用Scramble for基础架构资源来支持他们。
从一到二
所以我们怎么开始这件事情的? 首先让我们来看一下Instagram的整体基础架构栈。
扩展到多数据中心的关键是区分全局数据和局部数据。全局数据需要在不同的数据中心间复制,而局部数据在每个区域可能不同(例如web服务器创建的异步任务应该只在所在的区域被看到)。
下一个要考虑的是硬件资源。这个可以粗略的氛围三中:存储,计算和缓存。
存储
Instagram主要是用两种后端数据库系统:PostgreSQL和Cassandra。他们都有成熟的复制框架来很好的作为全局的一致数据存储。
全局数据整齐地映射到这些服务器上存储的数据。目标是在不同的数据中心间保持这些数据的最终一致性,每一个区域有一个读复制,来避免web服务器的跨数据中心读。
但是,对PostgreSQL的写入仍然夸数据中心,因为他们总是要写到主服务集群上。
CPU处理
Web服务器,异步服务器都是无状态的容易分布的计算资源,并且只需要访问本地数据。Web服务器可以创建异步工作,这些异步工作被异步消息代理加入队列,然后被异步服务器消费,全都在一个区域。
缓存
缓存层是web服务器最常访问的层,并且它们需要在同一个数据中心中来避免用户请求的延迟。这意味着对一个数据中心缓存的更新不会反映到另一个数据中心中,因此对迁移到多数据中心创建了一个挑战。
想象一个用户在你的最新发表的照片上评论。在一个数据中心的情况下,服务这个请求的web服务器可以仅仅在缓存中更新这个新评论。一个关注者会从同一个缓存中看到这个新评论。
然而在多数据中心的情景下,如果评论者和关注者被不同的区域服务,关注者的区域缓存将不会被更新,这个用户就不能看到评论。
我们的解决方法是使用PgQ, 增强它使得插入缓存失效事件到被修改的数据库中。
在主节点:
Web服务器插入一条评论到PostgreSQL数据库中 Web服务器在同一个数据库中插入一个缓存失效条目
在从节点:
复制主数据库,包括新插入的评论和缓存失效条目 缓存失效处理读取缓存失效条目并且使区域缓存失效 Django集群从数据库中读到新插入的评论并且重新填充缓存
这解决了缓存一致性问题。另一方面,相对于单区域的例子,django服务器直接更新缓存而不重新读区数据库,多区域时会增加数据库的读负载。
为了减轻这个问题,我们使用了两种办法:1) 通过冗余计数器减少每一个读需要的计算资源;2) 通过缓存租约减少读的数量。
冗余计数器
最常见的缓存键是计数器。例如,我们使用一个计数器来确定喜欢Justin Bieber的一个具体的帖子的人数。
当只有一个区域时,我们可以从web服务器增加memcache的计数器,所以避免一个“select count(*)”的数据库调用,这回节省几百毫秒。
但是在有两个区域和PgQ失效时,每一个新的喜欢对计数器创建了一个缓存失效事件。这会创建大量的“select count(*)”,尤其是在热点对象上。
为了减少这些操作每一个需要的资源,我们对这个帖子的喜欢数量的计数器进行冗余(译注:即在post的字段中加上likes的计数器,虽然是反范式的但带来了性能提升)。当一个新的喜欢来到时,这个计数在数据库中增加,因此,每个对这个计数的读会变成一个更有效的简单的select。
另一个在存储喜欢这个帖子的人的同一个数据库中进行冗余计数的好处是,更新可以被包含在一个事务中,似的这个更新总是原子的和一致的。虽然在改变前,缓存的计数器可能和数据库中存储的不一致,因为超时或重试等等原因。
Memcache租约
在上面来自Justin Bieber的新的帖子的例子中,在这个帖子的最初的几分钟,浏览和点赞的都会达到峰值。对每一个赞,计数器都从缓存中删去。非常常见的情况是web服务器都尝试从缓存中获取同一个
计数器,但是会有“缓存未命中”发生。如果所有的这些web服务器都去数据库服务器来获取数据,将会导致惊群问题。
我们使用memcache租约机制来解决这个问题。它像这样工作:
·Web服务器发起一个“租约get”请求,不是通常的“get”请求到memcache服务器。
· Memcache服务器在命中时返回命中的缓存值。在这种情况下和一个通常的“get”请求没有区别。
· 如果memcache服务器找不到对应的key,它在n秒内返回一个“首次未命中”给这段时间内请求的一个web服务器;这段时间内任何其他的“租约get”请求会得到一个“热未命中”。
·在“热未命中”的情况下,这个key最近从cache中删除,它会返回过期的值。
·如果这个缓存的key在n秒内没有被挺冲,它再次对一个“租约get”请求返回“首次未命中”。
· 当一个web server收到“首次未命中”时,它进到数据库中获取数据并且填充缓存。
·当一个web server收到“热未命中”和一个过期的值时,它可以使用这个值。
·如果它收到一个没有值的“热未命中”,它可以选择等待缓存被“首次未命中”的web server填充。
总之,在以上的实现中,我们可以通过减少访问数据库的次数和每次访问的资源来减少数据库的负载。
这也提高了我们后端在一些热计数器调出缓存时的可靠性,这种情形在Instagram的早期并非不常见。每次这种情形发生都会使得工程师赶忙手动修复缓存。在这样的改变下,这些事故成为了老工程师的回忆。
从10ms延迟到60ms
目前为止,我们主要关注了当缓存变得有区域性之后的缓存一致性。数据中心之间的网络延迟是另一个影响很多设计的挑战。数据中心之间,一个60ms的网络延迟可以导致数据库复制的问题和web server更新数据库的问题。我们需要解决以下问题来支持无缝扩展:
PostgreSQL 读复制落后
当一个Postgres的主节点写的时候,它生成增量日至。写请求来的越快,这些日志生成的越频繁。主节点们为从节点偶尔的需求存储最近的日志文件,但是它们归档所有的日志到存储中,来保证日志被保存
并且可以被任何需要更早的主节点保留的数据的从节点的访问。这样,主节点不会耗尽硬盘空间。
当我们创建一个新的读复制时,读复制开始读主节点的一个快照。一旦完成,它需要应用从这个快照之后发生的日志。当所有的日志都应用之后,它会是最新的并且可以持续的同步主节点和服务web服务器的读请求。
然而,当一个大数据库的写比率相当高时,在从节点和存储设备中会有较多的网络延迟,有可能日志被读取的速率比日志创建的速率要慢,这样从节点将会被落的越来越远而且永远都追不上。
我们的解决方案是在读复制开始从主节点上传输基础快照时就开启第二个流来传输日志并存储到本地磁盘上,当一个快照结束传输时,读复制可以在本地读区日志,使得恢复进程更加块。
这不仅解决了我们在全美的数据库复制问题,也使建造新的复制的时间减半。现在即使主节点和从节点在同一个区域,操作效率也很大程度的提高了。
总结
Instagram现在在全美运行了多个数据中心,给我们了更弹性的容量规划和获取,更高的可靠性,更好的为2012年发生的那样的自然灾害的准备。事实上,我们最近在一个计划的“灾难”中存活。Facebook
规律性地测试它的数据中心,通过在访问高峰的时候关闭它们。大约一个月前,我们刚刚完成迁移我们的数据到一个新的数据中心,Facebook就运行了一个测试并且关停了这个数据中心。这是一个高风险
的模拟,但是幸运的是我们不被用户注意到的度过了容量损失。Instagram迁移第二部分成功了!
翻译:陈光