Abstract
Memcached 是一个著名的简单的内存缓存解决方案。本文描述了 Facebook 如何利用 Memcached 作为构建模块,构建和扩展一个支持全球最大社交网络的分布式键值存储系统。我们的系统处理每秒数十亿次请求,并存储数万亿项数据,为全球超过十亿用户提供丰富的体验。
1. Introduction
热门且引人入胜的社交网络网站给基础设施带来了巨大挑战。每天有数亿人使用这些网络,并对计算、网络和 I/O 资源提出了令传统 Web 架构难以满足的要求。社交网络的基础设施需要满足以下要求:(1) 允许近实时通信,(2) 实时从多个来源聚合内容,(3) 能够访问和更新非常受欢迎的共享内容,以及(4) 扩展以处理每秒数百万个用户请求。
本文描述了如何改进 memcached 开源版本 [14] 并将其用作构建全球最大社交网络分布式键值存储系统的构建模块。我们讨论了从单个服务器集群扩展到多个地理分布式集群的过程。据我们所知,这个系统是全球最大的 memcached 安装,每秒处理超过 10 亿个请求,存储数万亿个项目。
本文是一系列关于分布式键值存储灵活性和实用性的研究中的最新成果 [1, 2, 5, 6, 12, 14, 34, 36]。本文关注基于 Memcached 的开源内存哈希表实现,因为它提供了低延迟、低成本的共享存储池访问。这些特性使我们能够构建数据密集型功能,否则这些功能可能是不切实际的。例如,每个页面请求发出数百次数据库查询的功能,可能永远无法离开原型阶段,因为它性能过慢且成本过高。然而,在我们的应用中,网页经常从 Memcached 服务器获取数千个键值对。
“离开原型阶段”是指一个项目或产品从初始开发阶段(原型阶段)转变为更高级或最终状态的过程。在原型阶段,开发人员创建项目的早期版本或模型,用于实验、测试和收集反馈。一旦项目经过改进,被认为足够成熟和稳定,就可以超越原型阶段,并进一步开发,最终达到生产或发布。在您提供的文本背景下,性能较差且成本较高的功能很难超越原型阶段,因为它可能不够实用或高效,无法在实际系统中实施。
我们的目标之一是介绍部署不同规模时所面临的重要主题。虽然性能、效率、容错和一致性等特性在所有规模上都很重要,但根据我们的经验,在某些规模下,实现某些特性实现起来比其他规模要费力得多。例如,在较小规模上,如果复制量较少,维护数据一致性可能比较大规模的情况更容易,因为后者通常需要复制。此外,随着服务器数量的增加和网络成为瓶颈,寻找最优通信调度的重要性也在增加。
本文主要包括四个方面的贡献:(1)描述 Facebook 基于 Memcached 架构的演变。(2)确定对 Memcached 的改进,提高性能并增加内存效率。(3)我们强调那些能够提高我们在运行大规模操作系统方面的能力的机制。(4)描述了我们系统所承受的实际工作负载量。
2. Overview
以下特性对我们的设计产生了很大影响。首先,用户消费的内容比他们创造的内容多一个数量级。这种行为导致工作负载主要集中在获取数据上,并表明缓存可以带来显著优势。其次,我们的读操作需要从各种来源获取数据,如 MySQL 数据库、HDFS 安装和后端服务。这种异构性要求采用灵活的缓存策略,以存储来自不同来源的数据。
Memcached 提供一组简单的操作(set、get 和 delete),使其成为大规模分布式系统中基本组件的理想选择。我们开始使用的开源版本提供了一个单机内存哈希表。在本文中,我们讨论了如何在这个基本构建模块的基础上进行优化,并使用它构建一个可以每秒处理数十亿请求的分布式键值存储。从此,我们使用`memcached`表示源代码或运行中的二进制文件,`memcache`表示分布式系统。
Query cache:我们依赖 memcache 来减轻数据库的读负载。特别是,我们将 memcache 作为基于需求填充(`demand-filled`)的旁路式(`look-aside`)缓存,如图 1 所示。

图 1:基于需求填充的旁路式缓存 Memcache。左半部分说明了在缓存未命中时 Web 服务器的读取路径。右半部分说明了写入路径。 图 1 描述了 Memcache 作为基于需求填充的旁路缓存的工作原理。
在未命中缓存情况下,左半部分展示了 Web 服务器读取数据的过程。首先,Web 服务器会尝试从 Memcache 获取所需数据。如果未在 Memcache 中找到数据(也就是未命中),Web 服务器会从数据库或其他后端服务中提取数据,并将其存储在 Memcache 中以备后续使用。
右半部分展示了写入数据的过程。当 Web 服务器需要写入数据时,它首先向数据库发出更新请求。之后,为了确保缓存数据与数据库保持一致,Web 服务器会向 Memcache 发送删除请求,以使缓存中的失效数据失效。这样一来,下一次读取请求再次到达 Memcache 时,缓存将会发生未命中,从而迫使 Web 服务器从数据库中提取最新数据。
当 Web 服务器需要数据时,它首先通过提供字符串键从 memcache 请求值。如果该以该键为标记的条目未被缓存,Web 服务器会从数据库或其他后端服务中检索数据,并用键值对填充缓存。对于写请求,Web 服务器向数据库发出 SQL 语句,然后向 memcache 发送删除请求,使任何过期数据无效。我们选择删除缓存的数据而不是更新它,因为删除是幂等的。Memcache 不是数据的权威来源,因此允许清除缓存的数据。
在Memcache中,我们选择删除而不是更新缓存数据,原因有以下几点:
- 幂等性:如前所述,删除操作是幂等的。这意味着即使多次执行删除操作,其结果也与执行一次相同。这有助于确保系统在面对可能需要重试的网络请求时的一致性和稳定性。相比之下,更新操作可能不是幂等的,多次执行可能导致不一致的结果。
- 简化系统:采用删除而非更新缓存数据的策略可以简化系统。只需从数据库获取更新后的数据,然后删除缓存中的过期数据即可。下一次请求数据时,缓存将自动加载最新数据。这样可以确保缓存数据始终与数据库一致,降低出错的风险。
- 资源和性能节约:通过删除缓存数据的方式,系统可以避免在每次写入时都执行更新。因为在许多场景下,并非所有的更新操作都需要立即体现在缓存中,这样可以节省计算和网络资源。在实际应用中,用户可能在短时间内读取相同的数据,而不会频繁地进行更新。因此,在删除缓存条目并在需要时重新加载数据的过程中,性能和资源可能会得到更好的利用。
尽管在某些情况下,更新缓存可能是更合适的选择,但在许多实际系统中,删除缓存条目是一种既简单又高效的方法,可以确保缓存与持久化数据存储保持一致。
虽然有几种方法可以解决 MySQL 数据库上过多的读取流量问题,但我们选择使用 memcache。考虑到有限的工程资源和时间,这是最佳选择。此外,将缓存层与持久性层分离使得在工作负载发生变化时,我们可以独立调整各个层。
Generic cache:我们还将 memcache 用作更通用的键值存储。例如,工程师们使用 memcache 存储从复杂的机器学习算法中预先计算出的结果,然后可以被其他各种应用程序使用。新服务使用现有 marcher 基础设施的代价很小,无需承担调优、优化、分配和维护大型服务器集群的负担。
就目前而言,memcached 不提供服务器之间的协调;它是运行在单个服务器上的内存哈希表。在本文的剩余部分,我们将描述如何基于 memcached 构建一个分布式键值存储,使其能够在 Facebook 的工作负载下运行。我们的系统提供了一整套配置、聚合和路由服务,将 memcached 实例组织成一个分布式系统。
我们根据在三种不同部署规模中出现的突出问题来组织本文的思路。在我们拥有一个服务器集群时,以读取为主的工作负载和广泛的扇出是我们关注的主要问题。
当需要扩展到多个前端集群时,我们处理这些集群之间的数据复制。
最后,我们描述了在我们将集群分布到世界各地的情况下,提供一致用户体验的机制。

在所有规模上,操作复杂性和容错性都很重要。我们提供支持我们设计决策的重要数据,并参考 Atikoglu 等人的工作 [8],对我们的工作负载进行更详细的分析。在高层次上,图2演示了这个最终的体系结构,在该体系结构中,我们将位于同一位置的集群组织到一个区域中,并指定一个主区域,该主区域提供数据流以保持非主区域的最新状态。
在发展我们的系统时,我们优先考虑两个主要设计目标。(1)任何变更必须影响用户面对的或操作上的问题。很少考虑范围有限的优化。(2) 我们将阅读短暂过时数据的概率作为一个可调参数,类似于响应性。我们愿意暴露略有过时的数据,以换取使后端存储服务免受过度负载的影响。
2.1 Client,WebServer,Memcached server(术语说明)
当然可以。Web服务器、客户端和Memcached服务器之间的关系可以用以下方式描述:
- 客户端:客户端通常是用户通过浏览器发送请求和接收响应的设备(如计算机、手机等)。客户端可以运行Web应用程序以实现特定功能。
- Web服务器:Web服务器作为中间组件处理客户端发出的请求。Web服务器根据请求执行相应操作(如数据库查询、执行计算、检索静态文件等),然后将响应发送回客户端(通常是HTML、CSS、JavaScript或JSON数据)。Web服务器通过其内部执行的代码来实现功能(如PHP、Python、Node.js等)。
- Memcached服务器:Memcached服务器是一个缓存服务器,用于存储频繁使用的数据,以提高Web服务器的性能。通过将部分数据存储在Memcached服务器中,Web服务器可以快速获取这些数据,从而减轻数据库的负担并加快响应速度。Memcached服务器并不直接与客户端进行通信。它只与Web服务器之间进行数据交换。
这里的关系是:
- 客户端发起请求并与Web服务器进行通信。
- Web服务器负责处理客户端的请求并生成响应。在处理请求时,Web服务器可能需要快速获取某些数据,而不是每次都去查询数据库。这时,Web服务器会与Memcached服务器进行通信,以便存储或检索缓存数据。
- Memcached服务器存储和提供缓存数据,用于提高Web服务器的性能。它通过Web服务器中的库或代理(如mcrouter)与Web服务器交互,但并不直接与客户端进行通信。
总之,客户端、Web服务器和Memcached服务器之间的主要区别在于它们的角色和功能。客户端主要负责用户界面和用户交互,Web服务器处理并响应客户端的请求,而Memcached服务器为Web服务器提供缓存数据服务以提高其性能。这三者共同合作,提供了用户所需的高效、高性能的Web应用程序。
3. In a Cluster:Latency and Load
考虑在一个集群内扩展到数千台服务器时带来的挑战,在这种规模下,我们的大部分工作集中在减少获取缓存数据的延迟或者降低由于缓存未命中带来的负载。
3.1 Reducing Latency
无论数据请求导致的是缓存命中还是未命中,memcache 响应的延迟都是用户请求响应时间的关键因素。单个用户 Web 请求通常会导致数百个独立的 memcache 获取请求。例如,加载我们热门页面之一平均需要从 memcache 获取 521 个不同的项目^1^。
在针对那个页面的抓取中,95th百分位数是1,740个项目。这意味着在所有抓取中,95%的抓取请求返回的项目数少于或等于1,740个,而另外5%的抓取请求返回的项目数可能超过1,740个。
我们在集群中配置了数百台 memcached 服务器,以减轻数据库和其他服务的负载。通过一致性哈希[22]在 memcached 服务器之间分配每个服务器要存储的记录条目。因此,Web 服务器必须经常与许多 memcached 服务器通信以满足用户请求。结果,这会导致所有 Web 服务器在短时间内与每个 memcached 服务器进行通信。这种all-to-all的通信模式可能导致 incast congestion(集中式拥塞)[30],或使单个服务器成为许多 Web 服务器的瓶颈。数据复制通常缓解单服务器瓶颈,但在常见情况下会导致显著的内存效率低下。
我们主要通过关注在每个 Web 服务器上运行的 memcache 客户端来降低延迟。这个客户端具有一系列功能,包括序列化、压缩、请求路由、错误处理和请求批处理。客户端维护一个所有可用服务器的map数据结构,该地图通过辅助配置系统进行更新。
Parallel requests and batching(并行化请求和批处理): 我们结构化我们的Web应用程序的代码,以尽量减少响应页面请求所需的网络往返次数。我们构建一个有向无环图(DAG),表示数据之间的依赖关系。Web服务器使用这个DAG来最大化并行抓取的记录条目的数量。平均而言,这些批处理的请求数每次包含24个键^2^。
95th百分位数是每次请求95个键。也就是说,在所有请求中,95%的请求包含的键数少于或等于95个,而另外5%的请求可能包含超过95个键。
Client-server communication: Memcached服务器之间不进行通信。在适当的情况下,我们将系统的复杂性嵌入到无状态客户端,而不是Memcached服务器中。这大大简化了Memcached,使我们能够专注于它(Memcached服务器)在更有限的用例中能获得高性能。保持客户端无状态便于软件快速迭代,并简化了部署过程。客户端逻辑分为两个部分提供:可嵌入到应用程序的库和名为mcrouter的独立代理。此代理提供一个Memcached服务器接口,并将请求/回复路由到其他服务器。
在这段话的背景下,“我们将系统的复杂性嵌入到一个无状态的客户端”表示,他们没有在Memcached服务器内建立和管理复杂功能,而是将这些复杂性移入客户端组件(如库或代理,比如mcrouter)。 这里提到的“系统”指的是Memcached缓存实现的整体设计和功能需求,如连接管理、缓存查找、路由和错误处理。 将复杂性嵌入到无状态客户端,并不一定意味着给客户端添加状态;相反,它表示将逻辑和处理负担转移到客户端。无状态客户端仍然可以处理复杂任务,但不会保留与过去请求或响应相关的任何信息。这种方法简化了Memcached服务器,使其能够专注于关键功能,例如以高性能存储和检索缓存数据。 客户端的无状态性质保持不变,因为客户端本身不存储关于服务器或其处理的数据的任何状态信息。其功能是内置的,并在每个请求中进行处理,而不依赖于过去请求的信息。
客户端使用UDP和TCP与Memcached服务器通信。我们依赖于用UDP处理get请求以减少延迟和开销。由于UDP是无连接的,Web服务器中的每个线程都可以直接与Memcached服务器通信,无需经过mcrouter,也无需建立和维护连接,从而降低开销。UDP实现检测丢弃的数据包或乱序接收的数据包(使用序列号),并将其作为客户端的错误处理。它不提供任何从这些错误中恢复的机制。在我们的基础设施中,我们发现这个决定是实用的。在高峰负载下,Memcache客户端观察到0.25%的get请求被丢弃。其中大约80%的丢弃是由于延迟或丢包造成的,剩下的是因为乱序交付。客户端将get错误视为缓存未命中,但在查询数据后,Web服务器会跳过将条目插入Memcached,以避免给可能过载的网络或服务器增加负担。
为了可靠性,客户端通过在与Web服务器相同的机器上运行的mcrouter实例使用TCP执行set和delete操作。对于我们需要确认状态更改(update or set和delete)的操作,TCP减轻了在UDP实现中添加重试机制的需求。
Web服务器依赖于高度的并行性和超额订阅来实现高吞吐量。如果不通过mcrouter进行某种形式的连接合并,打开TCP连接的高内存需求使得在每个Web线程和Memcached服务器之间建立开放连接变得代价高昂。合并这些连接可以减少高吞吐量TCP连接所需的网络、CPU和内存资源,从而提高web服务器的效率。

图3显示了生产环境中的Web服务器通过UDP获取键以及通过TCP通过mcrouter的平均、中位数和95th百分位延迟。在所有情况下,这些平均值的标准差均小于1%。正如数据显示的那样,依赖UDP可以使服务请求的延迟减少20%。
Incast congestion(集中式拥塞):Memcache客户端实现了流量控制机制,以限制集中式拥塞。当客户端请求大量键时,如果这些响应同时到达,则响应可能会使机架和集群交换机等组件不堪重负。因此,客户端使用滑动窗口机制[11]来控制未完成请求的数量。当客户端收到响应时,可以发送下一个请求。与TCP拥塞控制类似,成功请求后,滑动窗口大小会缓慢增长;当请求未得到回应时,窗口大小会缩小。此窗口适用于所有独立于目的地的memcache请求;而TCP窗口仅适用于单个流。

图4显示了窗口大小对用户请求处于可运行状态但正在等待Web服务器调度的时间的影响。数据来自一个前端集群中的多个机架。用户请求在每个Web服务器处呈泊松到达过程。根据Little定律[26],L = λW,服务器中排队请求的数量(L)与处理请求所需的平均时间(W)成正比,假设输入请求速率是恒定的(这对于我们的实验是成立的)。Web请求等待调度的时间是系统中Web请求数量的直接指示。对于较低的窗口大小,应用程序将必须按序分发更多组memcache请求,从而增加Web请求的持续时间。当窗口大小过大时,同时请求的memcache数量会导致集中式拥塞。结果将是memcache错误,并且应用程序会回头(退而求其次)去请求持久性存储中的数据,这将导致Web请求处理速度变慢。在这两个极端之间存在一个平衡点,可以避免不必要的延迟,并将集中式拥塞降至最低。
3.2 Reducing Load(降低昂贵查询路径的频率)
我们使用memcache来降低沿着更昂贵的路径(如数据库查询)获取数据的频率。当所需数据未被缓存时,Web服务器会回退(退而求其次)到这些路径。以下小节介绍了减轻负载的三种技术。
3.2.1 Leases(租约)
我们引入了一种称为租约(leases)的新机制来解决两个问题:过时的set操作(stale sets)和惊群现象(thundering herds)。过时的set操作发生在Web服务器在memcache中设置一个值,但该值不反映应该被缓存的最新值。当并发更新操作到了memcache后发生重排序时,可能会发生这种情况。惊群现象是指一个特定键承受着大量的读写活动。由于写活动反复使最近设置的值失效,许多读取会默认到更昂贵的路径。我们的租约机制解决了这两个问题。
直观地说,当客户端遇到高速缓存未命中时,memcached实例会向客户端提供租约,让客户端将数据重新设置到缓存中。租约是一个与客户端最初请求的特定键有关的64位令牌。客户端在将值设置到缓存中时提供租约令牌。通过租约令牌,memcached可以验证并确定是否应存储数据,从而仲裁并发写操作。如果由于收到对那个项目的删除请求而使租约令牌失效,验证可能会失败。租约以类似于负载链接/存储条件操作[20]的方式防止过时的设置。
对于租约策略,memcached可以仲裁数据是否过时,所以可以将租约(64位令牌)视为某个项目的版本号。此外,当该项目遇到删除操作时,memcache可以直接认为此项目无效。您的理解是正确的。通过使用租约机制,可以确保数据的一致性,避免向缓存写入陈旧数据。
对租约进行轻微修改还可以缓解惊群现象。每个memcached服务器会规定返回令牌的速率。默认情况下,我们将这些服务器配置为每10秒只为每个键返回一个令牌。在发出令牌后的10秒内请求某个键的值会导致一条特殊通知,告诉客户端等待一段短暂的时间。通常,具有租约的客户端在几毫秒内会成功设置数据。因此,在等待的客户端重试请求时,数据通常已经存在于缓存中。
为了说明这一点,我们收集了一个星期内一组特别容易受到惊群现象影响的键的所有缓存未命中数据。在没有租约的情况下,所有的缓存未命中都导致了达到17K/s的高峰数据库查询速率。使用租约后,高峰数据库查询速率为1.3K/s。由于我们基于高峰负载为数据库分配资源,因此我们的租约机制转化为显著的效率提升。
Stale values(过时的值): 使用租约,在某些用例中,我们可以将应用程序的等待时间最小化。通过识别返回略微过时的数据是可以接受的情况,我们可以进一步减少这个时间。当一个键被删除时,它的值会被转移到一个保存最近删除项目的数据结构中,在被清除前会存活一段时间。一个get请求可以返回一个租约令牌或者被标记为过时的数据。对于可以使用过时数据继续取得进展的应用程序,无需等待从数据库获取最新值。 根据我们的经验,由于缓存值往往是数据库单调递增快照,大多数应用程序可以在不做任何更改的情况下使用过时值。
注意在一开始我们就强调了,当update操作或set操作到来时,web服务器总是向memcache中的现有的item执行delete操作(因为delete操作是幂等的),所以delete与stale在某种程度上是一个道理,虽然他们可能在不同的场景下使用,但是他们的意思都是这个数据是过时的(stale)。
“一个get请求可以返回一个租约令牌或被标记为过时的数据。” 这在租约策略中的应用是,当客户端试图访问一个缓存项时,如果该项不存在或已被删除,memcache将返回一个租约令牌。客户端需要携带这个租约令牌,将最新的数据设置回到缓存中。租约策略可以帮助解决过时设置和惊群现象等问题,提高缓存性能并减轻数据库负担。
与此同时,get请求也可能返回过时的数据。过时的数据是指被标记为删除或已过期但尚未被完全清除的缓存条目。这对于那些能够容忍使用略微过时数据的应用程序非常有用,例如,那些在获取稍旧数据仍然可以进行正常操作的应用程序。这样可以避免不必要的数据库查询,从而提高系统性能。当过时数据被使用时,应用程序需要权衡数据准确性与性能的取舍。
这种配置数据需求的应用程序中,即使数据稍微过时,它们也能正常运行。因此,即使租约机制中的陈旧值可用,它们也能继续运行。这大大减轻了从数据库获取新数据的延迟。这种方法可以用来优化某些对陈旧数据容忍度较高的应用程序性能,同时保证数据仍然是近似最新的。
3.2.2 Memcache Pools
使用 memcache 作为通用缓存层需要不同工作负载共享基础架构,尽管它们可能有不同的访问模式、内存占用和服务质量要求。不同应用程序的工作负载可能会产生负面干扰,导致缓存命中率降低。
为了适应这些差异,我们将集群的 memcached 服务器划分为单独的池。我们将一个池(名为通配符wildcard)指定为默认池,并为在通配符中存在问题的键提供单独的池。例如,我们可能会为访问频繁但缓存未命中代价较低的键配置一个小池;我们还可能为访问不频繁但缓存未命中代价极高的键配置一个大池。

在这里我们将讨论两种不同类型的键族 - 高流失率键族 (high-churn) 和低流失率键族 (low-churn) 的每日和每周工作集。
- 高流失率键族 (high-churn): 这类键在短时间内访问频繁、更改或移除。它们的访问模式很活跃,可能在很短的时间范围内发生大量变化。
- 低流失率键族 (low-churn): 这类键的访问模式更为稳定,随着时间的推移,它们的访问、更改或移除的频率相对较低。
图5显示了两组不同项目的工作集,一组是低流失率,另一组是高流失率。工作集通过对每一百万个项目中的一个进行操作采样来近似。对于这些项目,我们收集最小、平均和最大项目大小。这些大小相加并乘以一百万以近似工作集。每日和每周工作集之间的差异表示流失量。具有不同流失特征的项以一种不幸的方式交互:在不再访问的高流失键之前,仍然有价值的低流失键被驱逐。
这句话的意思是:“尽管仍具有价值的低流失率键被驱逐出缓存,而不再被访问的高流失率键却依然占据着缓存空间。” 解释:在缓存系统(如 memcached)中,当缓存空间有限时,可能需要移除一部分键来为新的键腾出空间。这句话表示,即使低流失率(访问、更新或移除频率较低)的键对于系统仍具有价值,它们仍可能被提前移除,而不再被访问的高流失率键却仍然占用缓存空间,导致缓存空间的浪费和低效率。这种现象可能会影响缓存系统的性能并增加不必要的缓存未命中成本。
将这些键放在不同的池中可以防止这种负面干扰,并允许我们根据缓存丢失成本来调整高流失率池的大小。第7节提供进一步的分析。
3.2.3 Replication Within Pools(增加了处理效率)
在某些池中,我们使用复制来提高memcached服务器的延迟和效率。我们选择在池中复制一类键,当这类键符合以下的要求时:
(1) 应用程序常规性地同时获取许多键,(2) 整个数据集能适应一个或两个memcached服务器,且(3) 请求率远高于单个服务器所能处理的。
在这种情况下,我们更倾向于复制而不是进一步划分键空间。考虑一台拥有100个条目并能应对每秒500k个请求的memcached服务器。每个请求都会获取100个键。每个请求检索100个键与检索1个键相比,memcached开销的差异很小。要将这个系统扩展到处理每秒1M请求,假设我们增加了第二台服务器,并将键空间在两者之间平均划分。客户端现在需要将每个100个键的请求分为两个并行请求,每个请求大约50个键。因此,两个服务器仍然必须每秒处理1M个请求。但是,如果我们将所有100个键复制到多个服务器,则客户端对100个键的请求可以发送到任何副本。这将每台服务器的负载减少到每秒500k个请求。每个客户机根据自己的IP地址选择副本。这种方法要求将失效的所有副本都交付,以维持一致性。
3.3 Handling Failures
在无法从memcache获取数据的情况下,会对后端服务造成过多负载,可能导致进一步的级联故障。我们需要在两个层面解决故障:(1)网络或服务器故障导致少量主机无法访问;(2)范围较大的中断影响到集群内大部分服务器的正常运行。如果整个集群必须离线,我们将用户Web请求转向其他集群,从而有效地消除该集群内memcache的所有负载。
对于小型故障,我们依赖自动故障修复系统[3]。这些操作并非瞬时完成,可能需要几分钟的时间。这个时间足够长,可能导致前述的级联故障,因此我们引入一种机制来进一步隔离后端服务的故障。 我们为少数失败的服务器分配一小部分机器,名为Gutter(排水沟),承担它们的责任。Gutter占集群中memcached服务器的大约1%。
当memcached客户端未收到其获取请求的响应时,客户端会认为服务器已故障,并向特殊的Gutter池重新发出请求。如果第二次请求未命中,客户端将在查询数据库后将适当的键值对插入Gutter机器。Gutter中的条目会很快过期以避免无效的Gutter记录。Gutter以略微陈旧的数据为代价限制了后端服务的负载。
请注意,这种设计与客户端在剩余的memcached服务器之间重新散列键的方法不同。这样的方法有可能因非均匀的键访问频率导致级联故障。例如,单个键可能占用服务器请求的20%。负责这个热点键的服务器可能也会变得过载。通过将负载转移到空闲服务器,我们限制了这种风险。
gutter pooling(亦称作备用池)机制在 memcached 中不仅可以在一台服务器失效时接管其负载,同时还可以在某个 memcached 服务器负载过高时分担部分负载。通过将请求转发到空闲或负载较低的服务器上,gutter pooling 有助于维持整个分布式缓存系统的性能,防止单个服务器过载而导致的性能下降或故障。
在实际开发中,设计师需要关注负载均衡策略以防止热点数据导致的不稳定,尤其在分布式系统中。通过有效利用备用资源,可以提高系统的可用性和稳定性,确保分布式缓存能高效地支持大型应用的运行。
通常情况下,每个失败的请求都会导致备份存储器上的命中,可能使其过载。通过使用Gutter存储这些结果,大部分这些失败被转化为Gutter池中的命中,从而减轻了备份存储器的负载。实际上,这个系统将客户端可见的失败率降低了99%,并将每天10%-25%的失败转化为命中。如果memcached服务器完全失效,那么在4分钟内,Gutter池的命中率通常超过35%,而且往往接近50%。因此,当少数memcached服务器由于故障或轻微网络事件而无法使用时,Gutter保护了备份存储器免受突发流量的冲击。
4. In a Region:Replication
面对需求增长,人们可能会想购买更多的Web和memcached服务器来扩展集群。然而,简单地对系统进行扩展并不能消除所有问题。随着增加更多的Web服务器以应对增加的用户流量,请求量最高的条目会变得更受欢迎。随着memcached服务器数量的增加,集中式拥塞发生的次数也会加剧。因此,我们将Web和memcached服务器拆分到多个前端集群。
这些集群与包含数据库的存储集群一起定义了一个`Region`。这种区域架构还允许较小的故障域和易于管理的网络配置。这种区域架构还允许更小的故障域和易于处理的网络配置。我们用数据复制换取更独立的故障域、可处理的网络配置和减少集中式拥塞。
本节将分析多个共享相同存储集群的前端集群的影响。具体而言,我们将讨论允许跨这些集群进行数据复制的后果,以及不允许这种复制的潜在内存效率。
通过这种设计,我们可以在多个独立的前端集群之间实现故障隔离,确保单个集群的故障不会影响整个系统。同时,这种方法还可以提高内存效率,因为每个前端集群可以根据实际需求对数据进行局部复制,减少了人为复制导致的冗余数据。
禁止跨集群复制可以在一定程度上提高内存效率,通过将每个前端集群独立管理,避免不必要的数据复制。然而,这种方法可能会导致集群之间数据访问的延迟增加,因为每个集群需要独立从存储层获取数据。
从系统整体性能和稳定性的角度出发,可以根据实际需求对跨集群复制策略进行权衡和选择。
4.1 Regional Invalidations
尽管某个区域的存储集群保留了数据的权威副本,但用户需求可能会将该数据复制到前端集群中。存储集群负责使缓存数据失效,以保持前端集群与权威版本的一致性。作为一种优化,修改数据的Web服务器还会将失效信息发送到其自己的集群,以便为单个用户请求提供read-after-write语义,并减少其本地缓存中陈旧数据的存在时间。
SQL语句修改权威状态会被修改为在事务提交后包含需要失效的memcache键[7]。我们在每个数据库上部署失效守护程序(名为mcsqueal)。每个守护程序都会检查其数据库提交的SQL语句,提取任何删除操作,并将这些删除操作广播到该区域内每个前端集群的memcache部署中。
“SQL语句修改权威状态会被修改为在事务提交后包含需要失效的memcache键[7]” 这句话的意思是,当数据库中的数据被更新时,相应的SQL语句会被更改,以包含由于这些更新需要使哪些memcache键失效的信息。
举个例子,如果一个SQL语句更新了数据库中的某一行数据,memcache中该行的缓存版本可能会变得过时。因此,SQL语句会被修改,以携带与该行或数据相关的memcache键的信息。一旦事务提交,也就是说更新已成功应用于数据库,系统便知道哪些memcache键需要失效。使这些键失效可以确保在数据更新后,后续对该数据的请求不会使用高速缓存中的过时数据,而是从数据库中获取更新后的数据。
这种机制有助于维护存储在数据库和memcache中的数据之间的一致性,确保在权威(数据库)数据副本被更新后,不再使用过时数据。

失效管道显示需要通过守护程序(mcsqueal)删除的键。
在这个失效管道中,当有对数据库的更改发生时,如之前所述,SQL语句会包含需要失效的memcache键信息。这个过程可按以下步骤划分:
- Web服务器对数据库执行更新操作,SQL语句中包含需要失效的memcache键信息。
- 事务被提交并成功应用于数据库。
- 数据库上运行的守护程序(mcsqueal)监视提交的SQL语句。
- mcsqueal从SQL语句中提取需要失效的memcache键信息。
- 将这些失效信息批量打包到更少的数据包中并发送到每个前端集群中运行mcrouter实例的专用服务器。
- mcrouter从每个批次中解压单个删除操作,并将这些失效操作路由到位于前端集群内的相应memcached服务器。
- memcached服务器接收到失效指令,并使相关的缓存键失效(从缓存中删除)。
通过这个失效管道,当数据库有权威数据更新时,缓存中的过期数据能得到处理,保证系统中数据一致性。
图6说明了这种方法。我们意识到,大多数失效操作并未删除数据;实际上,只有4%的所有删除操作导致了缓存数据的实际失效。
通过这种方法,系统可以确保前端集群中的数据与存储层中权威数据保持一致,有效减少了陈旧数据对系统性能和用户体验的影响。同时,通过限制失效操作的广播范围,可以有效减少网络拥塞和资源占用,提高系统的整体性能。
减少数据包传输速率(Reducing packet rates):虽然mcsqueal可以直接与memcached服务器通信,但从后端集群传输到前端集群的数据包速率将会过高,无法接受。数据包速率问题是由于许多数据库和许多memcached服务器在集群边界之间进行通信。
“数据包速率问题”是指在系统各个组件之间管理大量数据包传输的挑战。在这种情况下,”这个数据包速率问题是由于许多数据库和许多 memcached 服务器在集群边界之间进行通信的结果”表明,由于大量数据库和 memcached 服务器在不同集群之间相互交互,数据包速率增加了。
当存在许多数据库和 memcached 服务器时,它们之间需要发送数据包进行通信和同步,跨集群边界发送的数据包数量大大增加。这可能导致数据包速率很高,从而压垮网络、消耗资源、并可能降低系统性能。 在这种情况下,挑战在于通过诸如批处理、使用专用路由器(如 mcrouter)或采用其他优化技术等方法有效地管理此数据包速率问题,以确保整个系统的稳定性和性能不受不利影响。
在这篇论文中,”cluster boundary” 指的是系统中不同集群之间的分隔线或界限。集群是指一组互联服务器或节点,它们共同运行并执行特定任务。在计算机网络和分布式系统中,集群可能负责各种任务,如数据存储、处理、负载均衡或其他应用程序功能。
“集群边界” 这个术语主要用于表示这些集群之间的逻辑分隔,强调它们相互之间的通信和协作行为。例如,在一个由许多数据库和 Memcached 服务器组成的系统中,集群之间的边界可能指的是一条将数据库分别与缓存服务器分组的逻辑线。通常情况下,不同集群的任务或功能各不相同,并且可能遇到跨集群边界的通信挑战。 因此,在论文描述的情境中,”集群边界” 这个术语通常用于强调系统中的集群之间如何互连以及如何在这些集群之间处理通信和信息传递。
因此我们采取如下的策略减少数据包的传输速率,如上图所示:失效守护程序将删除操作批量打包到更少的数据包中,并将它们发送到每个前端集群中运行mcrouter实例的专用服务器。然后,这些mcrouter从每个批次中解压单个删除操作,并将这些失效操作路由到位于前端集群内的正确memcached服务器。这种打包方法使每个数据包中删除操作的中位数数量提高了18倍。
通过Web服务器进行失效处理(Invalidation via web servers):Web服务器将失效广播到所有前端集群是一种更简单的方法。但是,这种方法有两个问题。首先,由于Web服务器在批量失效处理方面效果不如mcsqueal管道,因此会产生更多的数据包开销。其次,当出现系统性失效问题(例如由于配置错误导致删除操作的错误路由)时,这种方法提供的解决方案有限。过去,这通常需要对整个memcache基础设施进行滚动重启,这是我们希望避免的一种缓慢而中断性的过程。相比之下,将失效操作嵌入SQL语句中(数据库提交并存储在可靠的日志中)允许mcsqueal简单地重新播放可能已丢失或错误路由的失效操作。
4.2 Regional Pools
每个集群根据发送到该集群的用户请求的混合情况独立缓存数据。如果用户的请求随机路由到所有可用的前端集群,那么在所有前端集群之间的缓存数据会大致相同。这使我们能够在不降低缓存命中率的情况下将一个集群脱机进行维护。数据的过度复制可能会导致内存使用效率低下,尤其是对于大型且很少访问的项目。通过让多个前端集群共享相同的 Memcached 服务器集,我们可以减少副本数量。我们将这种方式称为区域池(Regional Pool)。
穿越集群边界会导致更高的延迟。此外,我们的网络在集群边界上的平均可用带宽比单个集群内的带宽数少40%。复制用更多的 Memcached 服务器换取较低的集群间带宽、较低的延迟和更好的容错能力。对于某些数据,放弃复制数据的优势并在每个区域只保留一份副本可能会更具成本效益。在一个区域内扩展 Memcache 的主要挑战之一是决定一个键是否需要在所有前端集群之间进行复制,还是每个区域只保留一个副本。当区域池中的服务器出现故障时,也会使用 Gutter池(一种处理服务器故障的工具)。

表1总结了我们应用程序中具有大值的两类项目。我们已经将一种项目(B)移动到区域池,而另一种项目(A)保持不变。请注意,客户端访问属于B类别的项目的次数比A类别的项目少一个数量级。B类的低访问率使其成为区域池的理想候选对象,因为它不会对集群间带宽产生不利影响。此外,B类别还将占用每个集群通配池的25%,因此区域化能够提供显著的存储效率。然而,A类别的项目尺寸是B类别项目的两倍,访问频率也高得多,这使得它们不适合纳入区域考虑范围。当前,根据访问率、数据集大小和访问特定项目的唯一用户数量等一系列手动启发式规则来决定是否将数据迁移到区域池。
4.3 Cold Cluster Warmup
当我们将新集群投入使用、现有集群出现故障或执行计划内维护时,缓存的命中率会非常低,降低了对后端服务的保护能力。一种名为”冷集群预热”(Cold Cluster Warmup)的系统通过允许”冷集群”(Cold Cluster)(即缓存为空的前端集群)从”热集群”(Warmup)(即具有正常命中率的缓存集群)而不是持久性存储中检索数据来缓解这种情况(利用了Region或者是Regional Pool的特性)。这利用了前面提到的跨前端集群的数据复制。借助此系统,冷集群可以在几小时内恢复到满负荷运行,而不是几天。
需要注意避免由并发冲突(竞争条件)导致的不一致性。例如,如果冷集群中的客户端执行数据库更新操作,而另一个客户端在热集群接收到失效通知之前从热集群中检索到旧值,那么冷集群中的该条目将无限期的存在不一致性。
在没有设置延迟时间(hold-off time)情况下,确实会出现上述的问题,以下是这个问题的详细分析:
- 冷集群的客户端执行数据库更新操作。
- 由于没有设置hold-off时间,冷集群还没来得及通知热集群数据失效。
- 另一个客户端(属于冷集群)从热集群中访问到旧数据。
- 该客户端将从热集群获取的旧数据存储在冷集群中,导致冷集群中的数据已被标记为删除的旧值重新生效。
在这种情况下,由于冷集群和热集群的缓存数据都是有效的,客户端不需要访问数据库。因此,冷集群中的数据将永久性地保持不一致。
这种情况下的问题是这样的:在冷热集群预热系统中,当冷集群中的客户端对数据库进行更新操作后,热集群尚未接收到数据失效通知。而此时,如果另一个客户端(属于冷集群)从热集群中检索到旧值,冷集群中的缓存将不会及时更新,导致数据不一致。
这种情况下,冷集群中的项目将一直保持不一致状态,因为它需要在热集群中接收到失效通知,然后在冷集群中将相应的缓存条目更新为新值。然而,在接收到失效通知之前,另一个客户端已经从热集群中检索了旧值并将其缓存在冷集群中。 为了减轻这种情况的发生,可以使用非零延迟时间(hold-off time)来控制删除操作。这种做法让客户端有时间处理失效通知。在上述例子中,我们使用了默认的2秒延迟时间。虽然这种方法不能完全避免数据不一致问题,但它可以显著降低数据不一致的风险。由于使用了冷热集群预热的操作优势大于极少数缓存一致性问题的成本,这种权衡在实际应用中是可接受的。
Memcached的删除操作支持非零延迟时间(hold-off time),在该延迟时间内会拒绝执行add操作,这意味着在执行删除操作之后的2秒内,集群会拒绝执行添加操作。默认情况下,对冷集群的所有删除操作都有2秒的延迟。
因此,当在冷集群中检测到未命中时,客户端会从热集群重新请求键并将其adds到冷集群中。在这个例子中,由于这里我们加入了hold-off time,因此当另一个客户端向热集群客户端请求完相同的数据后,向冷集群客户端的add操作将会失败,这表明数据库上有较新的数据,因此客户端将从数据库中重新获取值。虽然理论上仍有删除操作可能延迟超过2秒的可能性,但在绝大多数情况下并不会发生这种情况。冷集群预热的运行优势远大于缓存一致性问题的成本。一旦冷集群的命中率稳定,优势减弱时,我们会关闭这个功能。
5. Across Regions:Consistency
数据中心地理位置分布较广有几个优势。首先,将网络服务器放在离终端用户更近的地方可以显著减少延迟。其次,地理多样性可以减轻自然灾害或大规模停电等事件的影响。第三,新地点可能提供更便宜的能源和其他经济激励。我们通过在多个区域部署来获得这些优势。每个区域包括一个存储集群和几个前端集群。我们指定一个区域来存放主数据库,其他区域包含只读副本;我们依赖MySQL的复制机制来保持副本数据库与主数据库同步更新。在这种设计中,Web服务器访问本地memcached服务器或本地数据库副本时延迟很低。在跨多个区域扩展时,保持memcache数据和持久存储间一致性成为主要的技术挑战。这些挑战源于一个问题:副本数据库可能落后于主数据库。
我们的系统只代表了广泛的一致性和性能权衡中的一点。与系统的其他部分一样,一致性模型也经历了多年的演变,以适应网站的规模。它混合了可以实际构建的东西,而不会牺牲我们的高性能要求。系统管理的大量数据意味着任何增加网络或存储的需求的微小变化都有非常重要的关联成本。大多数提供更严格语义的想法很少离开设计阶段,因为它们变得过于昂贵。与许多为现有用例量身定制的系统不同,memcache和Facebook是共同发展的。这允许应用程序和系统工程师一起工作,以找到一个模型,该模型对应用程序工程师来说足够容易理解,但性能良好,并且足够简单,并能够可靠地处理大规模的工作。我们提供尽最大努力的最终一致性,但重点关注性能和可用性。因此,该系统在实践中非常适合我们,我们认为我们已经找到了一个可接受的权衡。
在主区域中执行写操作(Writes from a master region):我们早期的决策要求存储集群通过守护进程来使数据失效,这在多区域架构中具有重要意义。具体而言,它避免了这样的一个竞争条件:失效通知在数据从主区域复制之前到达。考虑一个位于主区域的web服务器,完成了对数据库的修改并试图使现在过期的数据失效。在主区域内发送失效通知是安全的。但是,让web服务器使副本区域中的数据无效可能为时过早,因为更改可能尚未传播到副本数据库。对复制区域数据的后续查询将与复制流(来自主区域的正确值)竞争,从而增加将过期数据设置到memcache中的可能性。从历史上看,我们在扩展到多个区域后,实现了mcsqueal。
原文描述了一个潜在的竞争条件,其中缓存失效发生在已更新数据被复制到副本区域之前。由于缓存失效,副本区域中的缓存数据被标记为陈旧数据。同时,尚未将实际更新的数据从主区域复制到副本区域的数据库中。
现在考虑一个从副本区域发出的针对同一数据的后续查询。有两种可能:
- 查询发生在已更新数据被复制到副本区域数据库之前。在这种情况下,查询将从副本区域的数据库中获取旧/陈旧数据,并将其作为有效数据存储在缓存中。这将导致主副本区域之间的不一致。
- 查询发生在已更新数据被复制到副本区域数据库之后。在这种情况下,查询将获取正确的、已更新数据。
此处的竞争条件是来自副本区域的后续查询和数据复制过程之间的竞争。结果取决于哪一个先发生,要么导致主副本区域之间的不一致,要么保持正确的、已更新数据。
在非主区域执行写操作(Writes from a non-master region):现在考虑一位用户,在复制延迟过大时从非主区域更新其数据。如果用户最近的更改丢失,他的下一个请求可能导致混乱。只有在复制流(从主区域的数据库中获取到正确数据)赶上后,才允许从副本数据库重新填充缓存。没有这个机制,后续请求可能导致获取和缓存副本区域的旧数据。
我们采用远程标记机制(remote marker)来尽量减少读取过时数据的可能性。标记的存在表示本地副本数据库中的数据可能已过时,查询应重定向到主区域。当web服务器希望更新影响键k的数据时,该服务器(1)在当前区域中设置远程标记rk,(2)执行将k和rk写入主数据库,在SQL语句中使其失效,以及(3)在本地集群中删除k.
当随后对k发出请求时(在本地region中),Web服务器将无法找到缓存数据,检查是否存在rk,并根据rk的存在与否将其查询指向主区域或本地区域。在这种情况下,我们明确将缓存未命中时的额外延迟换为减少读取过时数据的可能性。
在这个过程中,远程标记
rk是设置在该区域的 memcached 服务器上的,而不是后端数据库。这是因为远程标记主要用于标识本地副本数据库中的数据可能已过期,从而决定查询应该定向到主区域还是本地区域。 将远程标记存储在 memcached 服务器上,可将缓存层与后端数据库分离,确保查询性能更高且减轻对后端数据库的压力。此外,将远程标记与 memcached 关联,使得检查过期数据和缓存策略更加简洁和直接。当Web服务器无法找到缓存数据时,可以直接检查远程标记rk是否存在,实现快速有效地定向。
我们通过使用regional pool来实现远程标记。请注意,此机制可能会在对同一键的并发修改期间显示过时的信息,因为一个操作可能会删除应该为另一个正在进行的操作保留的远程标记。值得强调的是,我们使用memcache来存储远程标记(remote markers)与用它来缓存结果在某种程度上有微妙的区别。作为一个缓存,删除或驱逐一个键总是一个安全的操作;它可能会增加数据库的负载,但不会损害一致性。相反,远程标记的存在有助于区分非主数据库是否保存过时数据。在实践中,我们发现远程标记的移除和并发修改的情况都很少见。
虽然memcache可以同时用于远程标记与缓存数据结果,但实际使用方式和目的有所不同。
对于数据缓存:
- 缓存的目的是减轻数据库的负担,通过缓存热点数据避免重复查询。
- 从缓存中删除或逐出数据是安全的操作。它可能会给数据库带来更多负担,但不会损害数据一致性。
而对于远程标记:
- 远程标记的目的是帮助确定非主数据库中数据是否过时。
- 如果在同一个键上进行并发修改,这种机制可能会返回过时的数据。因为一个操作可能在另一个在途操作的过程中删除应该保持存在的远程标记。
所以,作者在提到使用memcache存储远程标记时,试图强调这种机制和普通的数据缓存之间的微妙区别。虽然两者都使用了memcache,但它们的使用方式和目的不完全相同。
远程标记(remote marker)可能会在某些情况下变得陈旧,但这并不直接影响数据本身的正确性和一致性。 远程标记的作用是指示本地副本数据库中的数据可能已过时,并在某些情况下将查询重定向到主区域。在正常情况下,如果远程标记有效地表示数据的状态(已过时或最新),那么整个系统将确保数据正确且一致。 然而,在同一个键上进行并发修改时,一个操作可能会在另一个在途操作的过程中删除应该保持存在的远程标记。这就使得远程标记可能变得陈旧,难以准确反映数据的状态。但是,请注意,这种情况在实际应用中是相对罕见的,即使发生了,也只会影响远程标记的准确性,而不会直接影响到数据本身。 总之,仅在特定情况下,远程标记可能变得陈旧。但这并不直接影响数据。对于数据正确性和一致性,主要是通过数据库的复制和一系列其他机制来维护。
操作考虑因素(Operational considerations):跨区域通信的成本较高,因为数据需要穿越较大的地理距离(如穿越美国大陆)。通过将delete stream的通信渠道与数据库复制共享,我们可以在带宽较低的连接上提高网络效率。
在第 4.1 节中提到的管理删除操作的系统也部署在副本数据库上,以便将删除操作广播到副本区域的 memcached 服务器。当下游组件无响应时,数据库和 mcrouters 会缓存删除操作。任何组件出现故障或延迟都会增加读取陈旧数据的可能性。一旦这些下游组件再次可用,这些缓存的删除操作将被重新执行。替代方案包括在检测到问题时将集群离线或在前端集群中过度失效数据。鉴于我们的工作负载,这些方法带来的中断要多于带来的好处。
在这里,”downstream components”(下游组件)指的是在一个分布式系统中,位于数据库和mcrouters之后、负责处理数据的其他组件。这些组件接收来自数据库和mcrouters的数据和操作,例如删除操作。这些下游组件可能包括数据缓存层(如memcached服务器)和应用服务器,它们依赖数据库和mcrouters提供的数据。
在这个上下文中,当下游组件变得无响应(例如由于瞬时故障或网络问题)时,数据库和mcrouters将把删除操作缓存起来。一旦下游组件恢复正常,缓存的删除操作会被重新执行,以确保数据一致性。
这段文字主要讨论了跨区域通信的成本以及当下游组件出现问题时如何处理删除操作的一些操作考虑因素。通过共享通信渠道、部署特定的系统来广播删除操作,以及在需要时缓存和回放删除操作,可以在保持工作负载稳定的前提下降低跨区域通信的成本并在一定程度上降低读取陈旧数据的可能性。同时,避免采用可能导致更多中断的替代方法。
6. Single Server Improvements
全互联通信模式意味着单个服务器可能成为集群的瓶颈。本节描述了在 memcached 中的性能优化和内存效率提升,从而实现集群内更好的扩展性。提高单个服务器缓存性能是一个活跃的研究领域[9, 10, 28, 25].
6.1 Performance Optimizations
我们从单线程的 memcached 开始,该 memcached 使用固定大小的哈希表。第一波主要的优化包括:(1)允许哈希表自动扩展,以避免查找时间漂移到 O(n),(2)使服务器变为多线程,使用全局锁来保护多个数据结构,并(3)为每个线程分配自己的 UDP 端口,以减少发送回复时的争用以及后续分散中断处理开销。前两项优化已贡献给开源社区。本节其余部分将探讨尚未应用在开源版本中的其他优化方法。
我们的实验主机配备了运行频率为 2.67GHz 的 Intel Xeon CPU(X5650,12个核心和12个超线程)、Intel 82574L 千兆以太网控制器和12GB内存。生产服务器有更多的内存。更多细节已在之前的发表文章中给出[4]。性能测试设置包括 15 个客户端向具有24个线程的单个 memcached 服务器生成 memcache 流量。客户端和服务器位于同一机架上,通过千兆以太网连接。这些测试在持续负载的两分钟内测量 memcached 响应的延迟。

Get Performance:我们首先研究将我们原来的多线程单锁实现替换为细粒度锁定的效果。我们通过在发出 memcached 请求(每个请求包含10个键)之前使用32字节的值预填充缓存来测量命中数。图 7 展示了在不同版本的 memcached 下,可以在亚毫秒级平均响应时间内持续的最大请求率。第一组柱状图是在细粒度锁定之前的 memcached,第二组是我们当前的 memcached,最后一组是开源版本 1.4.10,它独立实现了我们锁策略的粗粒度版本。
采用细粒度锁定,命中的峰值获取速率从每秒 60 万个项目增加到了 180 万个。未命中项的性能也从每秒 270 万个增加到了450 万个。命中时更加昂贵(所以命中时整体峰值低于未命中),因为需要构造并传输返回值,而未命中项则只需为整个 multiget 指示所有键都未命中的单个静态响应(END)。

我们还研究了使用 UDP 代替 TCP 的性能影响。图 8 显示了我们在单个获取和10键 multigets 情况下,平均延迟小于1毫秒时所能承受的峰值请求速率。我们发现,对于单个获取和10键 multigets,我们的 UDP 实现分别在性能上优于我们的 TCP 实现13%和8%。
因为 multigets 在每个请求中打包的数据比单个 get 更多,所以它们使用更少的数据包来完成相同的工作。如图8所示,10键 multigets 相对于单个 get 的性能提高了大约四倍。
总结起来,通过引入细粒度锁定、使用 UDP 代替 TCP 以及利用 multigets,memcached 性能得到了显著提升。命中和未命中项的峰值速率都有所增加,从而更好地满足了高并发场景下的需求。
6.2 Adaptive Slab Allocator
Memcached 使用 slab 分配器来管理内存。分配器将内存组织成 slab 类,每个类包含预分配的、大小一致的内存块。Memcached将items存储在尽可能小的slab类中,slab中的每个item空间可以容纳项的元数据、键和值。Slab 类从 64 字节开始,按 1.07 的因子指数倍增,最大为 1MB,对齐在 4 字节边界^4^。每个slab类都维护一个可用块的空闲列表,当空闲列表为空时,以1MB slab 的形式请求更多内存。一旦 memcached 服务器无法再分配空闲内存,就会通过在该 slab 类中逐出最近最少使用(LRU)的项目,从而完成新的item的存储。当工作负载发生变化时,最初分配给每个 slab 类的内存可能不再足够,导致命中率较低。
这个缩放因子确保我们同时拥有64字节和128字节的项目,这对硬件缓存行来说更容易处理。缓存行的典型大小是64字节或128字节,因此在分配内存块时,使用这些大小可以提高缓存使用效率。 当内存块大小与硬件缓存行大小相匹配时,可以减少缓存冲突和无效操作,从而提高内存访问效率。
对于memcached来说,这意味着更高的命中率和更好的性能。采用适当的缩放因子以满足硬件缓存需求有助于整体性能的优化。这也解释了为什么memcached的slab类从64字节开始,并在128字节处呈指数增长。调整这些指标可以确保内存分配与硬件缓存线的管理更具优势,从而对系统性能产生积极影响。
我们实现了一种自适应分配器,它会周期性地重新平衡 slab 当前被分配的内存以适应当前工作负载。如果当前正在驱逐items的 slab 类需要更多内存,并且下一个要被驱逐的项的使用时间至少比其他 slab 类中最近最少使用的项目的平均使用时间至少多20%,则将 slab 类标识为需要更多内存。如果找到了这样的类,那么包含最近最少使用项的slab将被释放并将该目标项转移到需要的类。
当 memcached 在某个 slab 类(类别)中没有足够的内存来存储新项目时,它会根据需要对内存空间进行重新平衡。首先在其他 slab 类(排除当前 slab 类)中找到最近最少使用的项目(这个item一定是全局的LRU项目,当然不包含当前的slab类)以及该项目所在的 slab 类。然后驱逐该项目,接着将释放出来的内存分配给当前没有足够内存的 slab 类。这样,内存资源在不同 slab 类之间得到了更有效的平衡,从而提高整个 memcached 系统的性能。
请注意,开源社区已经独立实现了一个类似的分配器,用于在slab类之间平衡驱逐率,而我们的算法关注于在类之间平衡最老项目的年龄。平衡年龄为整个服务器提供了更接近单个全局最近最少使用(LRU)驱逐策略的近似值,而不是调整可能受到访问模式严重影响的驱逐率。
通过使用自适应分配器,可以动态地重新分配 slab 类的内存,以适应不断变化的工作负载。这种方法有助于提高 memcached 的内存使用效率和命中率,从而提高应用程序的整体性能。
6.3 The Transient Item Cache
虽然 memcached 支持过期时间,但条目在过期后可能会在内存中继续存在。Memcached在为该项提供get请求或到达LRU的末端时,通过检查过期时间来惰性地驱逐此类条目。尽管这对于常见情况来说是有效的,但这种方案允许只在短暂活动期间占用内存的短暂键,直到它们到达 LRU 末尾。
这句话的意思是,虽然懒惰驱逐对于处理通常情况(常见情况)是高效的,但这种策略却允许短暂存活的键在内存中消耗资源,直到它们达到 LRU 队列末端。
简单来说,这些键在激活一次后就不再活跃,但仍然会占用内存空间。
在这个上下文中,懒惰驱逐(Lazy Eviction)是 memcached 用于处理过期数据的策略。这种策略在请求到达该项目时或当它们到达 LRU 末尾时检查并删除过期的项目。然而,这会导致仅在短暂活动期间占用内存的短暂键在系统中占满内存,这个现象持续到它们的位置移动到 LRU 末尾才会结束。 总之,懒惰驱逐策略在通常情况下有效,但对于某些短暂存活并仅获得一次活动的键,它们会在内存中浪费资源,直到它们到达 LRU 队列的末尾。这有时可能导致系统的内存效率降低。
因此,我们引入了一种混合方案,该方案对大多数键依赖惰性驱逐,并在过期时主动驱逐短寿命键。我们根据项目的过期时间将短寿命项目放入一个圆形缓冲区(由秒索引到到期)的链表数据结构——称为瞬态项目缓存(The Transient Item Cache)。每秒钟,缓冲区头部的所有项目都会被驱逐,头部向前推进一个位置。当我们为一组在短时间内具有较短实际寿命的密集使用的键添加较短的到期时间时,这个键族占用的 memcache 池的比例从6%减少到0.3%,而且不会影响命中率。
6.4 Software Upgrades
频繁的软件更改可能需要进行升级、修复错误、临时诊断或性能测试。一个 memcached 服务器在几个小时内就可以达到其峰值命中率的 90%。因此,升级一组 memcached 服务器可能需要超过 12 小时,因为需要仔细管理所产生的数据库负载。我们修改了 memcached ,使其将缓存的值和主要数据结构存储在 System V 共享内存区域中,以便数据可以在软件升级过程中保持活动状态,从而尽量减少中断。
7. Memcache Workload
这块不看,单纯的实验部分,只是为了验证而已。
8. Related Work
许多其他大型网站也认识到了键值存储的实用性。DeCandia 等人[12]提出了一种高可用的键值存储,该存储被 Amazon.com 的各种应用服务所使用。虽然他们的系统针对写入密集型工作负载进行了优化,但我们的目标是读取优势的工作负载。类似地,LinkedIn 使用了受 Dynamo 启发的 Voldemort 系统[5]。其他主要部署键值缓存解决方案的公司包括 Redis [6](Github、Digg 和 Blizzard 使用)以及 Twitter [33]和 Zynga(使用 memcached 进行缓存)。Lakshman 等人[1]开发了schema-based的分布式键值存储 Cassandra。我们更倾向于部署和扩展 memcached,因为它的设计更简单。
我们在扩展 memcache 方面的工作是基于分布式数据结构的大量研究。Gribble 等人[19]提出了一个早期版本的键值存储系统,适用于互联网的大规模服务。Ousterhout等人[29]也提出了一个大规模内存键值存储系统的理论。与这两种解决方案不同,memcache 并不保证持久性。我们依赖其他系统来处理持久数据存储。
Ports 等人[31]提供了一个库,用于管理事务数据库查询的缓存结果。我们的需求需要更灵活的缓存策略。我们对租约[18]和过期读取[23]的使用利用了之前对高性能系统中缓存一致性和读取操作的研究。Ghandeharizadeh和Yap[15]的工作也提出了一种算法,该算法基于时间戳而不是显式的版本号来解决过期数据集的问题。
虽然软件路由器更容易定制和编程,但它们的性能通常不如硬件路由器。Dobrescu 等人[13]通过利用通用服务器上的多核、多内存控制器、多队列网络接口和批处理来解决这些问题。将这些技术应用到 mcrouter 的实现中仍然是未来的工作。Twitter 也独立开发了一个类似于 mcrouter 的 memcache 代理[32]。
在 Coda [35]中,Satyanarayanan 等人展示了如何将由于断开操作而发散的数据集重新同步。Glendenning等人[17]利用Paxos[24]和quorum[16]来构建Scatter,这是一个具有线性化语义的分布式哈希表[21],可以适应数据的流失。Lloyd等人[27]研究了广域存储系统COPS的因果一致性。
TAO[37]是另一个依赖于缓存来处理大量低延迟查询的 Facebook 系统。TAO 与 memcache 在两个基本方面有所不同:(1)TAO 实现了一种图数据模型(Graph Data Model),在该模型中,节点由固定长度的永久标识符(64位整数)标识。 (2)TAO对其图模型到持久存储的特定映射进行编码,并负责持久化。许多组件(如我们的客户端库和 mcrouter)都被这两个系统使用。
9. Conclusion
在本文中,我们展示了如何扩展基于 memcached 的架构以满足 Facebook 不断增长的需求。讨论的许多权衡并不是根本性的,而是植根于平衡工程资源的现实,同时在持续的产品开发中发展一个实时系统。在构建、维护和发展我们的系统过程中,我们学到了以下几个教训。
- 将缓存和持久存储系统分离,可以使我们独立地进行扩展。
- 提高监控、调试和操作效率的功能与性能同样重要。
- 管理有状态组件在操作上比无状态组件更复杂。因此,将逻辑保留在无状态客户端有助于迭代功能并最大程度地减少中断。
- 系统必须支持新功能的逐步推出和回滚,即使这会导致功能集的暂时异构。
- 简单性至关重要。
将逻辑保存在无状态客户端意味着将某些关键部分的处理逻辑放在客户端应用程序中,而非 Memcache 服务器或其他服务端组件上。
无状态客户端可以处理来自用户的请求,与存储在 Memcache 服务器上的数据互动以及与后端数据库交互。然而,这并不意味着所有逻辑都在客户端应用程序中。 实际上,将某些处理逻辑放在客户端意味着应用逻辑被分布在客户端和服务器端(包括 Memcache 和数据库)。
这使得多个组件可以独立地进行开发、维护和升级,从而提高整个系统的灵活性和可扩展性。 当我说客户端应用程序的升级和维护与客户端本身、Memcache 服务器和后端数据库的升级和维护是完全独立的时,我的意思是这些组件可以单独进行修改和升级,从而降低故障风险、减少对整个系统的影响。
然而,这并不意味着客户端应用程序的故障或维护不会对整个系统产生影响。实际上,客户端应用程序、客户端本身、Memcache 服务器和后端数据库在功能和性能上是相互依赖的。通常,分层的架构在某种程度上提高了系统的可扩展性和稳定性。
将逻辑保存在无状态客户端有助于实现低耦合,这意味着系统各个组件之间的依赖关系较小,各组件可以相对独立地进行开发、维护和升级。低耦合有助于提高系统的灵活性、可扩展性和可维护性。此外,它还可以加速新功能的迭代和降低中断风险。
back.