1. The Revolution of Website architect
memcache这篇论文并没有什么新的概念、技术,但是有很多可以借鉴的经验。
1.1 The architecture of big website
在讨论FaceBook之前,我们首先需要了解当部署网站时,随着规模的变大,网站的部署方式的演变。

第一种架构,网站初创时期,访问的用户很少,所以一般选择将前端服务器(Web服务器),后端数据库(FB采用的是MySQL)放在同一台服务器上。我们使用的php编写的脚本用于web服务器与数据库交互,当用户越来越多时,为了获取更高的性能,我们需要把前端服务、交互脚本、后端数据库以某种方式分离。
第二种架构是在第一种的基础上的优化,因为我们拥有了更多的用户量,这里我们把服务分为前端服务集群和后端服务集群两大部分,前端服务集群上运行着web服务器和交互脚本;后端服务集群(目前只有一个服务器)运行着MySQL数据库。更多的前端服务器意味着我们可以同时处理更多的用户请求,但是目前的瓶颈是后端数据库服务器只有一个,迟早会达到处理能力的上限。

第三种架构:最大的进化是后端数据库集群现在有多个MySQL数据库服务器了,每台服务器都运行着MySQL实例,同时也对数据进行了分片,以此来分担前端集群带来的读写负载,但是我们需要以某种方式让交互脚本知道他们应该访问哪个后端服务器。我们的网站工作负载主要是读,但是一旦涉及到分布式事务,这会让写操作变得很慢。
这种架构有几个问题:
- 虽然我们对数据进行了分片,但是一旦出现了热门key数据,那么保存这些数据的服务器大概率会发生过载。
- 这种架构下,随着用户量增多,我们不得不继续添加MySQL服务器,这需要大量的钱。
所以我们开始考虑能否在后端服务器上添加一些运行速度更快的东西,比如:memcache。当然如果你很有钱,可以直接添加更多的专用于Memcache的缓存层的服务器集群。
1.2 Add a memcache Layer
这就演变出了下一种架构,也是与论文中描述的架构有点类似的架构,也是当前很多网站在使用的架构。

这种架构的进化点在于:在前端集群与后端集群之间放入了一个缓存层,Facebook使用的是memcache方案。memcache的读取处理速度要比后端的MySQL服务器快10倍以上。
前端服务器在处理读请求时,他首先会尝试向其中一个Memcache服务器发送get(key)请求,memcache是用一个巨大的内存型hash table实现的,他会根据key去查找是否有这个数据,如果命中memcache,那么memcache直接返回数据给前端服务器,前端服务器返回给客户,生成内容,这次访问就结束了。
若没有命中memcache,前端服务器需要重新将请求发送给后端数据库服务器,web server收到数据后,会对原始数据进行相关处理,之后将结果数据放入到某个memcache服务器中(用put操作),以供下次查询使用。
但是写操作依然要发送给后端数据库,这些是我们必须持久化到稳定存储上的内容。
Memcache是一个完全独立的软件,由于它的实现十分简单,它的工作只是根据key去缓存对应的内容。他对数据库的处理逻辑、内容代表什么意思一无所知,所以我们不能让他直接与后端服务器交互,他不具备处理与转发后端服务器回复的能力。
更重要的一点是,我们不会把每次访问的记录都放到memcache中去,所以没必要让Memcache担当起中转请求的职责,这是一种资源浪费,也需要额外的工作,我们只需要让memcache负责缓存由前端服务器处理完后的最终结果即可。此外,Memcache缓存的也不一定非得是某次的get结果,也可能前端服务器对多次get结果进行处理生成的一个通用的数据。
处理与交互逻辑都放在了php代码编写的交互脚本中,不需要给memcache再添加额外的工作,交给前端服务器更好。虽然这会让缓存一致性变得更好,但是这也会加重memcache的负担。FaceBook的memcache策略使用的是look-aside缓存策略。
1.3 look-aside cache && look-through cache
查找旁路缓存(look-aside cache)和查找穿透缓存(look-through cache)是两种不同的缓存访问策略,它们在请求缓存数据时的行为有所不同。
-
查找旁路缓存(look-aside cache):
在这种策略中,CPU直接访问缓存。如果数据在缓存中(缓存命中),CPU直接读取缓存数据。如果数据不在缓存中(缓存未命中),CPU需要访问主存储器以获取数据,并将其加载到缓存中以供将来使用。
查找旁路缓存的优点是它节省了访问缓存的时间,因为它从主存储器获取数据。但缺点是由于CPU需要直接操作缓存,所以引入了对缓存和主存储器的并发访问的管理复杂性。
-
查找穿透缓存(look-through cache):
在这种策略中,CPU访问缓存,然后缓存访问主存储器。无论数据是否在缓存中(缓存命中或未命中),CPU都将其请求发送到缓存。如果缓存未命中,缓存从主存储器中获取数据,并将其返回给CPU。此时数据也会被加载到缓存中供将来使用。
查找穿透缓存的优点是它简化了缓存和主存储器之间的访问管理,因为CPU始终与缓存进行通信。缺点是,即使发生缓存命中,CPU也需要等待缓存请求完成,可能导致访问数据所需时间的增加。
在Facebook中,Memcache是一种用于分布式缓存的技术。它用于减轻数据库的负载并降低请求响应时间。Memcache不同于查找旁路缓存(look-aside cache)和查找穿透缓存(look-through cache),这两者主要关注CPU与内存(缓存)之间的访问策略。
Facebook通过使用Memcache实现了类似于查找旁路缓存策略(look-aside cache)的效果。在访问数据时,Facebook的服务首先检查Memcache中是否有缓存数据。如果数据在Memcache中(缓存命中),服务直接读取缓存数据。否则(缓存未命中),服务需要查询数据库以获取数据,并将其存储在Memcache中供将来使用。
尽管Facebook的Memcache策略与查找旁路缓存策略类似,但它们的实现和应用场景有所不同。查找旁路缓存和查找穿透缓存提供了针对CPU和内存之间访问策略的解决方案,而Memcache主要关注Web服务通过网络缓存减轻数据库负担的问题。
1.4 Challenges
Facebook在使用Memcache时面临的挑战包括:
- 数据一致性:确保缓存数据与数据库中的数据保持一致。
- 数据失效策略:确定以何种方式和时间将过时数据从缓存中移除,以便有效管理缓存空间。
- 缓存扩展性:由于Facebook的规模迅速增长,需要设计可扩展的缓存架构以应对大量访问请求。
此外,我们使用memcache后,系统的处理数据量将会是后端数据库本身负载的很多倍,一旦有memcache服务器发生故障都会增加数据库那边的读请求数量。为了防止缓存层故障从而导致后端数据库服务器的压力激增,从而导致数据库高负荷瘫痪,我们要采取措施确保数据库层不会承担大部分的请求负载(即不会完全失去缓存层)。
一般来说Memcache的命中率会在90%以上,所以一旦memcache服务器故障,并且大部分流量都流向后端数据库后,这很有可能导致后端数据库服务器直接崩溃。
2. The Memcache of Facebook
2.1 Architecture of Facebook
Facebook有多个数据中心,每个数据中心被称为region:

上图展示的是两个数据中心,西海岸的是主数据中心,东海岸的是副本数据中心。
每一个数据中心都有一份完整的数据,同样是后端服务器集群+memcache集群+前端服务器集群(web服务器)的架构配置。主数据中心中的MySQL数据库中的数据是authority status,论文中提到的权威数据。
任何写操作都要发到主数据中心的对应数据库中进行处理。MySQL本身提供了异步日志复制机制,待主数据中心处理完当前写操作后,就可以将这次写操作发送到副本数据中心对应的后端数据库中。这种机制确保在几秒的延迟时间内,副本数据中心中的MySQL服务器就可以与主数据中心中的数据库完成数据同步。
读操作是在本地的数据中心中进行的。前端服务器会先与memcache服务器通信,若命中最好,未命中则需要去后端数据库服务器中读取数据。此外,前端服务器也会将处理好的结果数据缓存到memcache中。memcache中保存数据的方式可能与MySQL数据库有所不同。
2.2 Look-aside cache(Memcache)

2.2.1 Read operation
对于读请求,web服务器会调用交互脚本中的get(key)函数(携带一个目标key,用于从目标memcache获取数据),从而生成一个RPC call,发送给目标memcache服务器,memcache根据key查找是否缓存了对应的数据,如果命中,直接返回;若未命中,返回Nil。
在memcache未命中的前提下,前端web服务器向后端数据库发送获取该数据的SQL查询,之后将获取到的数据放入到memcache中,以备下一次查询使用。
2.2.2 Write Operation
当后端数据库完成修改后,Mcsqueal会去读取日志,直到它监视到这个SQL语句完成了,之后将目标失效键信息(用于memcache)从SQL中提取出来,发送给Mcrouter,之后由Mcrouter发送给指定的Memcache,使相关的stale data失效。论文是这么说的,但是这些操作的完成需要一定的时间,所以还是有一定的概率会读取到陈旧的值。具体细节可以看论文。
同样,当主MySQL集群将更新同步给备份MySQL集群时,备份MySQL集群同样也需要做Memcache失效操作。
这一策略主要是为了确保用户自己做的更新操作能及时的被自己看到,如更新个人信息等等。
2.3 Performance
存储系统一般可以通过分区(Partition)与复制(Replication)来获得更高的性能:

3. Partition and Replication of Facebook
3.1 Partition-Pros and Confs
- 如果所有key的使用频率都差不多,那么分区可以很好的工作。
- 出现热门key时,分区并不能很好的起作用,因为无论如何分区,一个key对应的数据只会被放到一台服务器上,还是会给该服务器造成很大的负担。
- 当前端服务器需要使用大量的不同key时,它可能需要与大量的分区进行通信,通信数量越多,开销越明显。
单纯的分区策略下,服务器保存的数据内容间互不相交。
所以Facebook将复制与分区结合起来使用:每个region都保存了一份完整的数据库数据(复制);
每个region内部,memcache服务器根据自己服务的前端服务器的实际需求去动态的调整不同memcache缓存的数据项,更加灵活(分区),同时不同的memcache服务器保存的数据也多少可能会重合,这也是replication。
3.2 Replication:Across Regions(DCs)
FaceBook将这两种策略结合起来使用,在不同的region间采取replication策略,在单个region内部采取分区策略。
不同的region位于不同的区域,如一个在东海岸,一个在西海岸,方便用户可以实现本地访问,延迟更低。但是由于每次写操作都必须发给primary region中的副本,所以写操作的成本相应变高。
此外,单个region内的不同memcache服务器保存的数据可能会重合(由服务的前端服务器访问数据的特性决定),所以这里也有一点replication的意思。
我们知道在Facebook的应用场景下读操作的频率远远地高于写操作。
3.3 Why Multi-clusters within a region
在一个region内,我们将前端服务器与memcache服务器拆分到不同的集群中,有如下的几个原因:
我们知道memcache中保存的大部分数据可能只会被小部分用户使用,热门数据只是占少数,所以我们更希望热门数据可以被复制(缓存)到多个memcache服务器上。使用多个集群可能会减少冷门数据的占用空间,因为只有当前集群内的memcache才会缓存当前客户的独用数据。
而由于热门数据会被大部分的用户访问,所以不管是否使用多集群,热门数据总会被复制(缓存)到所有的memcache服务器中去,所以无论用哪种集群方案,这对热门数据是没有什么提升的,
多个集群可以有效的减少集群内的连接数。前端服务器与memcache服务器通信采用的是all-to-all的方式,通常来说,前端服务器可能需要从每个memcache服务器上获取数据,这意味着连接数量将会是O(N^2^)级别个TCP连接,并且要维护每个连接的状态,这会产生大量的性能开销;
此外,前端服务器在这种情况下可能会在同一时间内收到所有的memcache服务器的回应,若集群过大,且当前前端服务器非常繁忙,可能会导致丢包。
在每个region(数据中心)中存在着一个巨大的网络,设计者想要在集群中构建一个很快的网络环境,集群规模越大难度越大,耗费资源越多,但是不需要构建一个大统一的集群。所以拆分为几个集群可以更好地减少构建快速可靠的网络环境的成本。
综上所述,多集群方案能够带来很多收益。
拆分后每个集群之间是互相独立的,他们只需要负责自己的那部分前端服务器(对应的客户)即可。
3.4 Partitions:Within a Region

在一个区域(数据中心)内,只有一份后端数据库集群,并且将不同数据做了分片,但是注意可能数据库本身的数据并没有进行复制,可能只在不同的region间做了这个操作。
Facebook在一个Region内又划分了多个集群,每个集群由若干前端服务器和集群若干memcache服务器集群组成,每个集群内的前端服务器只会与集群内的memcache服务器进行通信(不考虑cold start情况)。所以不同的集群是完全独立的,他们之间并没有什么信息需要同步,也不需要进行通信。上图划分了两个大的集群。
非热门数据一般不会被放到集群内的memcache服务器中,每个集群内的memcache服务器们只会存储(复制)自己负责的前端服务器访问的相对频繁的数据,热门数据甚至可能在所有的memcache服务器中都有备份,这取决于该数据被访问的频率。
因此,我们又设计了一个regional pool,同样存放着一些memcache服务器,它被该region内的所有集群共享,前端服务器会将一些使用频率不高的数据放到这个regional pool中,只放一个数据副本即可,因为使用的频率并不高,可能偶尔会被访问几次。
3.5 Add a new cluster
当我们加入一个新的集群,或者重启了一个集群后(称其处于cold start状态),为了让该集群快速的融入到生产中,且不会给后端数据库服务器造成过大的额外负担,我们需要采取一些措施,如果什么都不做,那么在初期新集群要承担的读流量将会完全的流向后端数据库服务器,因为刚开始memcache服务器中啥都没有。
因此论文中提到了Cold Cluster Warmup的思路:新集群在本地的memcache未命中时会首先从已经运行了很久的集群中的memcache服务器(称为warm memcache)中获取数据,如果是热门数据,那么一般可以获取到。因此,对于cold start而言,只有当从本地memcache和warm memcache中都没有拿到数据时,才会从后端数据库服务器中获取数据。
3.6 Thundering Herd(惊群效应)

惊群现象:一个特定键承受着大量的读写活动。由于写活动反复使最近设置的值失效,许多读取会默认到更昂贵的路径。比如说,一个Front End Server(简称为FE)修改了某个热门key对应的数据,使memcache失效,紧接着很多FE都要请求这个key对应的数据,这时他们得到的将都是memcache未命中,所以之后他们会同时对DB发起请求,而DB会在这个时间段内承受大量的负载。
3.6.1 lease机制
采用LEASE(租约)机制可以缓解惊群效应。在背景中,有很多个FE都要访问某个在memcache中失效的热门key时,memcache会给第一个FE授予一个租约(TOKEN);后续再有FE访问该key时,Memcache会发现自己已经把这个key对应的令牌授予了某个FE,所以后续的FE需要等待一段短暂的时间,等待具有租约的FE从DB拿到新值并放入到memcache中,随后FE们就可以直接从memcache中访问了。
每个memcached服务器会规定返回令牌的速率。默认情况下,我们将这些服务器配置为每10秒只为每个键返回一个令牌。在发出令牌后的10秒内请求某个键的值会导致一条特殊通知,告诉客户端等待一段短暂的时间。通常,具有租约的客户端在几毫秒内会成功设置数据。因此,在等待的客户端重试请求时,数据通常已经存在于缓存中。
4. Handling Failures of Memcache

在无法从memcache获取数据的情况下,会对后端服务造成过多负载,可能导致进一步的级联故障。需要从两个层面解决故障:(1)网络或服务器故障导致少量主机无法访问;(2)范围较大的中断影响到集群内大部分服务器的正常运行。如果整个集群必须离线,我们将用户Web请求转向其他集群,从而有效地消除该集群内memcache的所有负载。
我们主要讨论如何处理第一种小型故障。
4.1 Little Scale Failure
网络或服务器故障导致少量FE无法访问memcache。上图中是一个小型故障,某个memcache服务器出现了问题,导致一些FE的请求会直接落到DB上,会对后端服务造成过多负载,可能导致进一步的级联故障。
对于小型故障,我们依赖自动故障修复系统[3]。这些操作并非瞬时完成,可能需要几分钟的时间。这个时间足够长,可能导致前述的级联故障,因此我们引入一种机制来进一步隔离后端服务的故障。 我们为少数失败的服务器分配一小部分机器,名为Gutter(排水沟),承担它们的责任。Gutter占集群中memcached服务器的大约1%。
当memcached客户端未收到其获取请求的响应时,客户端会认为目标的memcache了,服务器已故障,并向特殊的Gutter池重新发出请求。如果第二次请求未命中,客户端将在查询数据库后将适当的键值对插入Gutter机器。Gutter中的条目会很快过期以避免无效的Gutter记录。Gutter以略微陈旧的数据为代价限制了后端服务的负载。
请注意,这种设计与客户端在剩余的memcached服务器之间重新散列键的方法不同。这样的方法有可能因非均匀的键访问频率导致级联故障。例如,单个键可能占用服务器请求的20%。负责这个热点键的服务器可能也会变得过载。通过将负载转移到空闲服务器,我们限制了这种风险。
gutter pooling(亦称作备用池)机制中不仅可以在一台memcache服务器失效时接管其负载,同时还可以在某个 memcached 服务器负载过高时分担部分负载。
通常情况下,每个失败的请求都会导致备份存储器上的命中,可能使其过载。通过使用Gutter存储这些结果,大部分这些失败被转化为Gutter池中的命中,从而减轻了备份存储器的负载。实际上,这个系统将客户端可见的失败率降低了99%,并将每天10%-25%的失败转化为命中。如果memcached服务器完全失效,那么在4分钟内,Gutter池的命中率通常超过35%,而且往往接近50%。因此,当少数memcached服务器由于故障或轻微网络事件而无法使用时,Gutter保护了备份存储器免受突发流量的冲击。
5. The consistency of Facebook

对于任何给定数据来说,它在很多不同的地方存在着相应的副本,因此当写操作到来时,我们需要能让所有保存该数据的服务器都能得到及时的更新(或使旧数据失效)。
问题描述:如上所述,C1与C2两个客户端,C1要获取某个值,C2要对这个值进行更新。C1会拿到一个过时的值,因为C1在从DB拿到值后,C2随之对DB进行了更新,之后C2和DB都会对memcache发送一个delete信号,但是memcache收到后,随后C1又发来了一个set操作,而这个set会把memcache的值设置为一个过时的状态。
如果我们不做任何处理,那么memcache提供的数据在下一次更新到来之前,将始终是过时的。
5.1 lease机制
通过对lease机制扩展,可以有效的解决这个问题,具体规则如下:
- 当C1访问memcache得到一个miss时,memcahce会为这个目标数据生成一个lease,memcache内部会记录
<key,lease>的pair,规则是只有持有这个lease的FE才有资格去更新这个key对应的数据。 - 当memcache收到一个针对该key的delete操作时,它不仅会使这个key对应的数据失效,同时也会删除颁发过得lease。
在这里,C1虽然拿到了lease,但随后C2更新数据后向memcache发送了delete(key)请求,当memcache收到C2的删除请求时,无论memcache中key对应的数据是否是有效的(即C1与C2的delete操作无论谁先到),我们总可以保证后续的client请求拿到的总是最新的数据,理由如下:
- memcache中的数据无效:意味着C2的delete操作先到,此时key对应的数据已经是无效了;之后C1的set操作到了,但是由于C1持有的lease已经被C2的delete操作删除,所以memcache会无视C1的set操作,从而下一次请求该key对应的数据的操作会从DB中获取数据。
- memcache中数据有效:意味着C1的set先到,那么后续的C2的delete操作会将C1的lease与set操作设置的数据一并删除,同样下一次请求该key对应数据的操作会从DB中获取数据。这种情况下过时数据会在memcache中保存较长的时间,但总会得到最新的数据。
6. Conclusion
该系统存在着大量的复杂性,因为他们是由彼此并不了解的部分拼接起来的:这个系统采用的三层架构(前端服务器集群、memcache服务器集群、后端数据库集群),这三部分互相是不了解的。
Yahoo的PNUTS存储系统可以与Facebook的memcache进行一定的比较。
Facebook的memcache系统提示我们:必须去思考caching、partition、replication之间的权衡,看哪部分是我们重点关注的,可以投入更多的精力。
back.