facebook海量图片存储系统与淘宝TFS系统比较

经典论文翻译导读之《Finding a needle in Haystack: Facebook’s photo storage》

【译者预读】面对海量小文件的存储和检索,Google发表了GFS,淘宝开源了TFS,而Facebook又是如何应对千亿级别的图片存储、每秒百万级别的图片查询?Facebook与一样提供了海量图片服务的淘宝,解决方案有何异同?本篇文章,为您揭晓。html

本篇论文的原文可谓通俗易懂、行云流水、结构清晰、图文并茂……正如做者所说的——"替换Facebook的图片存储系统就像高速公路上给汽车换轮子,咱们没法去追求完美的设计……咱们花费了不少的注意力来保持它的简单",本篇论文也是同样,没有牵扯空洞的庞大架构、也没有晦涩零散的陈述,有的是对痛点的反思,对目标的分解,条理清晰,循序渐进。既描述了宏观的总体流程,又推导了细节难点的技术突破过程。以致于译者都不须要在文中插入过多备注和解读了^_^。不过在文章末尾,译者以淘宝的解决方案做为对比,阐述了文章中的一些精髓的突破点,以供读者参考。node

摘要

本篇论文描述了Haystack,一个为Facebook的照片应用而专门优化定制的对象存储系统。Facebook当前存储了超过260 billion的图片,至关于20PB的数据。用户每一个星期还会上传1 billion的新照片(60TB),Facebook在峰值时需提供每秒查询超过1 million图片的能力。相比咱们之前的方案(基于NAS和NFS),Haystack提供了一个成本更低的、性能更高的解决方案。咱们观察到一个很是关键的问题:传统的设计由于元数据查询而致使了过多的磁盘操做。咱们不遗余力的减小每一个图片的元数据,让Haystack能在内存中执行全部的元数据查询。这个突破让系统腾出了更多的性能来读取真实的数据,增长了总体的吞吐量。web

 

1 介绍

分享照片是Facebook最受欢迎的功能之一。迄今为止,用户已经上传了超过65 billion的图片,使得Facebook成为世界上最大的图片分享网站。对每一个上传的照片,Facebook生成和存储4种不一样大小的图片(好比在某些场景下只需展现缩略图),这就产生了超过260 billion张图片、超过20PB的数据。用户每一个星期还在上传1 billion的新照片(60TB),Facebook峰值时须要提供每秒查询1 million张图片的能力。这些数字将来还会不断增加,图片存储对Facebook的基础设施提出了一个巨大的挑战。sql

这篇论文介绍了Haystack的设计和实现,它已做为Facebook的图片存储系统投入生产环境24个月了。Haystack是一个为Facebook上分享照片而设计的对象存储技术,在这个应用场景中,每一个数据只会写入一次、读操做频繁、从不修改、不多删除。在Facebook遭遇的负荷下,传统的文件系统性能不好,优化定制出Haystack是大势所趋。数据库

根据咱们的经验,传统基于POSIX的文件系统的缺点主要是目录和每一个文件的元数据。对于图片应用,不少元数据(好比文件权限),是无用的并且浪费了不少存储容量。并且更大的性能消耗在于文件的元数据必须从磁盘读到内存来定位文件。文件规模较小时这些花费可有可无,然而面对几百billion的图片和PB级别的数据,访问元数据就是吞吐量瓶颈所在。这是咱们从以前(NAS+NFS)方案中总结的血的教训。一般状况下,咱们读取单个照片就须要好几个磁盘操做:一个(有时候更多)转换文件名为inode number,另外一个从磁盘上读取inode,最后一个读取文件自己。简单来讲,为了查询元数据使用磁盘I/O是限制吞吐量的重要因素。在实际生产环境中,咱们必须依赖内容分发网络(CDN,好比Akamai)来支撑主要的读取流量,即便如此,文件元数据的大小和I/O一样对总体系统有很大影响。浏览器

了解传统途径的缺点后,咱们设计了Haystack来达到4个主要目标:缓存

  • 高吞吐量和低延迟。咱们的图片存储系统必须跟得上海量用户查询请求。超过处理容量上限的请求,要么被忽略(对用户体验是不可接受的),要么被CDN处理(成本昂贵并且可能遭遇一个性价比转折点)。想要用户体验好,图片查询必须快速。Haystack但愿每一个读操做至多须要一个磁盘操做,基于此才能达到高吞吐量和低延迟。为了实现这个目标,咱们不遗余力的减小每一个图片的必需元数据,而后将全部的元数据保存在内存中。安全

  •  容错。在大规模系统中,故障天天都会发生。尽管服务器崩溃和硬盘故障是不可避免的,也毫不能够给用户返回一个error,哪怕整个数据中心都停电,哪怕一个跨国网络断开。因此,Haystack复制每张图片到地理隔离的多个地点,一台机器倒下了,多台机器会替补上来。服务器

  •  高性价比。Haystack比咱们以前(NAS+NFS)方案性能更好,并且更省钱。咱们按两个维度来衡量:每TB可用存储的花费、每TB可用存储的读取速度。相对NAS设备,Haystack每一个可用TB省了28%的成本,每秒支撑了超过4倍的读请求。cookie

  •  简单。替换Facebook的图片存储系统就像高速公路上给汽车换轮子,咱们没法去追求完美的设计,这会致使实现和维护都很是耗时耗力。Haystack是一个新系统,缺少多年的生产环境级别的测试。咱们花费了不少的注意力来保持它的简单,因此构建和部署一个可工做的Haystack只花了几个月而不是好几年。

本篇文章3个主要的贡献是:

  • Haystack,一个为高效存储和检索billion级别图片而优化定制的对象存储系统。

  • 构建和扩展一个低成本、高可靠、高可用图片存储系统中的经验教训。

  • 访问Facebook照片分享应用的请求的特征描述

文章剩余部分结构以下。章节2阐述了背景、突出了以前架构遇到的挑战。章节3描述了Haystack的设计和实现。章节4描述了各类图片读写场景下的系统负载特征,经过实验数据证实Haystack达到了设计目标。章节5是对比和相关工做,以及章节6的总结。

 

2 背景 & 个人前任

在本章节,咱们将描述Haystack以前的架构,突出其主要的经验教训。因为文章大小限制,一些细节就不细述了。

 

2.1 背景

咱们先来看一个概览图,它描述了一般的设计方案,web服务器、CDN和存储系统如何交互协做,来实现一个热门站点的图片服务。图1描述了从用户访问包含某个图片的页面开始,直到她最终从磁盘的特定位置下载此图片结束的全过程。访问一个页面时,用户的浏览器首先发送HTTP请求到一个web服务器,它负责生成markup以供浏览器渲染。对每张图片,web服务器为其构造一个URL,引导浏览器在此位置下载图片数据。对于热门站点,这个URL一般指向一个CDN。若是CDN缓存了此图片,那么它会马上将数据回复给浏览器。不然,CDN检查URL,URL中须要嵌入足够的信息以供CDN从本站点的存储系统中检索图片。拿到图片后,CDN更新它的缓存数据、将图片发送回用户的浏览器。

 

2.2 基于NFS的设计

在咱们最初的设计中,咱们使用了一个基于NFS的方案。咱们吸收的主要教训是,对于一个热门的社交网络站点,只有CDN不足觉得图片服务提供一个实用的解决方案。对于热门图片,CDN确实很高效——好比我的信息图片和最近上传的照片——可是一个像Facebook的社交网络站点,会产生大量的对不热门(较老)内容的请求,咱们称之为long tail(长尾理论中的名词)。long tail的请求也占据了很大流量,它们都须要访问更下游的图片存储主机,由于这些请求在CDN缓存里基本上都会命中失败。缓存全部的图片是能够解决此问题,但这么作代价太大,须要极大容量的缓存。

基于NFS的设计中,图片文件存储在一组商用NAS设备上,NAS设备的卷被mount到Photo Store Server的NFS上。图2展现了这个架构。Photo Store Server解析URL得出卷和完整的文件路径,在NFS上读取数据,而后返回结果到CDN。

咱们最初在NFS卷的每一个目录下存储几千个文件,致使读取文件时产生了过多的磁盘操做,哪怕只是读单个图片。因为NAS设备管理目录元数据的机制,放置几千个文件在一个目录是极其低效的,由于目录的blockmap太大不能被设备有效的缓存。所以检索单个图片均可能须要超过10个磁盘操做。在减小到每一个目录下几百个图片后,系统仍然大概须要3个磁盘操做来获取一个图片:一个读取目录元数据到内存、第二个装载inode到内存、最后读取文件内容。

为了继续减小磁盘操做,咱们让图片存储服务器明确的缓存NAS设备返回的文件"句柄"。第一次读取一个文件时,图片存储服务器正常打开一个文件,将文件名与文件"句柄"的映射缓存到memcache中。同时,咱们在os内核中添加了一个经过句柄打开文件的接口,当查询被缓存的文件时,图片存储服务器直接用此接口和"句柄"参数打开文件。遗憾的是,文件"句柄"缓存改进不大,由于越冷门的图片越难被缓存到(没有解决long tail问题)。值得讨论的是能够将全部文件"句柄"缓存到memcache,不过这也须要NAS设备能缓存全部的inode信息,这么作是很是昂贵的。总结一下,咱们从NAS方案吸收的主要教训是,仅针对缓存——无论是NAS设备缓存仍是额外的像memcache缓存——对减小磁盘操做的改进是有限的。存储系统终究是要处理long tail请求(不热门图片)。

 

2.3 讨论

咱们很难提出一个指导方针关于什么时候应该构建一个自定义的存储系统。下面是咱们在最终决定搭建Haystack以前的一些思考,但愿能给你们提供参考。

面对基于NFS设计的瓶颈,咱们探讨了是否能够构建一个相似GFS的系统。而咱们大部分用户数据都存储在Mysql数据库,文件存储主要用于开发工做、日志数据以及图片。NAS设备其实为这些场景提供了性价比很好的方案。此外,咱们补充了hadoop以供海量日志数据处理。面对图片服务的long tail问题,Mysql、NAS、Hadoop都不太合适。

咱们面临的困境可简称为"已存在存储系统缺少合适的RAM-to-disk比率"。然而,没有什么比率是绝对正确的。系统须要足够的内存才能缓存全部的文件系统元数据。在咱们基于NAS的方案中,一个图片对应到一个文件,每一个文件须要至少一个inode,这已经占了几百byte。提供足够的内存太昂贵。因此咱们决定构建一个定制存储系统,减小每一个图片的元数据总量,以便能有足够的内存。相对购买更多的NAS设备,这是更加可行的、性价比更好的方案。

 

3 设计和实现

Facebook使用CDN来支撑热门图片查询,结合Haystack则解决了它的long tail问题。若是web站点在查询静态内容时遇到I/O瓶颈,传统方案就是使用CDN,它为下游的存储系统挡住了绝大部分的查询请求。在Facebook,为了传统的、廉价的的底层存储不受I/O摆布,CDN每每须要缓存难以置信的海量静态内容。

上面已经论述过,在不久的未来,CDN也不能彻底的解决咱们的问题,因此咱们设计了Haystack来解决这个严重瓶颈:磁盘操做。咱们接受long tail请求必然致使磁盘操做的现实,可是会尽可能减小除了访问真实图片数据以外的其余操做。Haystack有效的减小了文件系统元数据的空间,并在内存中保存全部元数据。

每一个图片存储为一个文件将会致使元数据太多,难以被所有缓存。Haystack的对策是:将多个图片存储在单个文件中,控制文件个数,维护大型文件,咱们将论述此方案是很是有效的。另外,咱们强调了它设计的简洁性,以促进快速的实现和部署。咱们将以此核心技术展开,结合它周边的全部架构组件,描述Haystack是如何实现了一个高可靠、高可用的存储系统。在下面对Haystack的介绍中,须要区分两种元数据,不要混淆。一种是应用元数据,它是用来为浏览器构造检索图片所需的URL;另外一种是文件系统元数据,用于在磁盘上检索文件。

 

3.1 概览

Haystack架构包含3个核心组件:Haytack Store、Haystack Directory和Haystack Cache(简单起见咱们下面就不带Haystack前缀了)。Store是持久化存储系统,并负责管理图片的文件系统元数据。Store将数据存储在物理的卷上。好比,在一台机器上提供100个物理卷,每一个提供100GB的存储容量,整台机器则能够支撑10TB的存储。更进一步,不一样机器上的多个物理卷将对应一个逻辑卷。Haystack将一个图片存储到一个逻辑卷时,图片被写入到全部对应的物理卷。这个冗余可避免因为硬盘故障,磁盘控制器bug等致使的数据丢失。Directory维护了逻辑到物理卷的映射以及其余应用元数据,好比某个图片寄存在哪一个逻辑卷、某个逻辑卷的空闲空间等。Cache的功能相似咱们系统内部的CDN,它帮Store挡住热门图片的请求(能够缓存的就毫不交给下游的持久化存储)。在独立设计Haystack时,咱们要设想它处于一个没有CDN的大环境中,即便有CDN也要预防其节点故障致使大量请求直接进入存储系统,因此Cache是十分必要的。

图3说明了Store、Directory、Cache是如何协做的,以及如何与外部的浏览器、web服务器、CDN和存储系统交互。在Haystack架构中,浏览器会被引导至CDN或者Cache上。须要注意的是Cache本质上也是一个CDN,为了不困惑,咱们使用"CDN"表示外部的系统、使用"Cache"表示咱们内部的系统。有一个内部的缓存设施能减小对外部CDN的依赖。

当用户访问一个页面,web服务器使用Directory为每一个图片来构建一个URL(Directory中有足够的应用元数据来构造URL)。URL包含几块信息,每一块内容能够对应到从浏览器访问CDN(或者Cache)直至最终在一台Store机器上检索到图片的各个步骤。一个典型的URL以下:

http://<cdn>/<cache>/<machine id="">/<logical volume,="" photo="">

第一个部分<cdn>指明了从哪一个CDN查询此图片。到CDN后它使用最后部分的URL(逻辑卷和图片ID)便可查找缓存的图片。若是CDN未命中缓存,它从URL中删除<cdn>相关信息,而后访问Cache。Cache的查找过程与之相似,若是还没命中,则去掉<cache>相关信息,请求被发至指定的Store机器(<machine id="">)。若是请求不通过CDN直接发至Cache,其过程与上述相似,只是少了CDN这个环节。

图4说明了在Haystack中的上传流程。用户上传一个图片时,她首先发送数据到web服务器。web服务器随后从Directory中请求一个可写逻辑卷。最后,web服务器为图片分配一个惟一的ID,而后将其上传至逻辑卷对应的每一个物理卷。

 

3.2 Haystack Directory

Directory提供4个主要功能。首先,它提供一个从逻辑卷到物理卷的映射。web服务器上传图片和构建图片URL时都须要使用这个映射。第二,Directory在分配写请求到逻辑卷、分配读请求到物理卷时需保证负载均衡。第三,Directory决定一个图片请求应该被发至CDN仍是Cache,这个功能可让咱们动态调整是否依赖CDN。第四,Directory指明那些逻辑卷是只读的(只读限制多是源自运维缘由、或者达到存储容量上限;为了运维方便,咱们以机器粒度来标记卷的只读)。

当咱们增长新机器以增大Store的容量时,那些新机器是可写的;仅仅可写的机器会收到upload请求。随时间流逝这些机器的可用容量会不断减小。当一个机器达到容量上限,咱们标记它为只读,在下一个子章节咱们将讨论如何这个特性如何影响Cache和Store。

Directory将应用元数据存储在一个冗余复制的数据库,经过一个PHP接口访问,也能够换成memcache以减小延迟。当一个Store机器故障、数据丢失时,Directory在应用元数据中删除对应的项,新Store机器上线后则接替此项。

 【译者YY】3.2章节是整篇文章中惟一一处译者认为没有解释清楚的环节。结合3.1章节中的URL结构解析部分,读者能够发现Directory须要拿到图片的"原始URL"(页面html中link的URL),再结合应用元数据,就能够构造出"引导URL"以供下游使用。从3.2中咱们知道Directory必然保存了逻辑卷到物理卷的映射,仅用此映射+原始URL足够发掘其余应用元数据吗?原始URL中到底包含了什么信息(论文中没看到介绍)?咱们能够作个假设,假如原始URL中仅仅包含图片ID,那Directory如何得知它对应哪一个逻辑卷(必须先完成这一步映射,才能继续挖掘更多应用元数据)?Directory是否在upload阶段将图片ID与逻辑卷的映射也保存了?若是是,那这个映射的数据量不能忽略不计,论文也不应一笔带过。

从原文一些细枝末节的描述中,译者认为Directory确实保存了不少与图片ID相关的元数据(存储在哪一个逻辑卷、cookie等)。但整篇论文译者也没找到对策,总感受这样性价比过低,不符合Haystack的做风。对于这个疑惑,文章末尾扩展阅读部分将尝试解答。读者先认为其具有此能力吧。

3.3 Haystack Cache

Cache会从CDN或者直接从用户浏览器接收到图片查询请求。Cache的实现可理解为一个分布式Hash Table,使用图片ID做为key来定位缓存的数据。若是Cache未命中,Cache则根据URL从指定Store机器上获取图片,视状况回复给CDN或者用户浏览器。

咱们如今强调一下Cache的一个重要行为概念。只有当符合两种条件之一时它才会缓存图片:(a)请求直接来自用户浏览器而不是CDN;(b)图片获取自一个可写的Store机器。第一个条件的理由是一个请求若是在CDN中没命中(非热门图片),那在咱们内部缓存也不太须要命中(即便此图片开始逐渐活跃,那也能在CDN中命中缓存,这里无需画蛇添足;直接的浏览器请求说明是不通过CDN的,那就须要Cache代为CDN,为其缓存)。第二个条件的理由是间接的,有点经验论,主要是为了保护可写Store机器;缘由挺有意思,大部分图片在上传以后很快会被频繁访问(好比某个美女新上传了一张自拍),并且文件系统在只有读或者只有写的状况下执行的更好,不太喜欢同时并发读写(章节4.1)。若是没有Cache,可写Store机器每每会遭遇频繁的读请求。所以,咱们甚至会主动的推送最近上传的图片到Cache。

 

3.4 Haystack Store

Store机器的接口设计的很简约。读操做只需提供一些很明确的元数据信息,包括图片ID、哪一个逻辑卷、哪台物理Store机器等。机器若是找到图片则返回其真实数据,不然返回错误信息。

每一个Store机器管理多个物理卷。每一个物理卷存有百万张图片。读者能够将一个物理卷想象为一个很是大的文件(100GB),保存为'/hay/haystack<logical volume="" id="">'。Store机器仅须要逻辑卷ID和文件offset就能很是快的访问一个图片。这是Haystack设计的主旨:不须要磁盘操做就能够检索文件名、偏移量、文件大小等元数据。Store机器会将其下全部物理卷的文件描述符(open的文件"句柄",卷的数量很少,数据量不大)缓存在内存中。同时,图片ID到文件系统元数据(文件、偏移量、大小等)的映射(后文简称为"内存中映射")是检索图片的重要条件,也会所有缓存在内存中。

如今咱们描述一下物理卷和内存中映射的结构。一个物理卷能够理解为一个大型文件,其中包含一系列的needle。每一个needle就是一张图片。图5说明了卷文件和每一个needle的格式。Table1描述了needle中的字段。

为了快速的检索needle,Store机器须要为每一个卷维护一个内存中的key-value映射。映射的Key就是(needle.key+needle.alternate_key)的组合,映射的Value就是needle的flag、size、卷offset(都以byte为单位)。若是Store机器崩溃、重启,它能够直接分析卷文件来从新构建这个映射(构建完成以前不处理请求)。下面咱们介绍Store机器如何响应读写和删除请求(Store仅支持这些操做)。

【译者注】从Table1咱们看到needle.key就是图片ID,为什么仅用图片ID作内存中映射的Key还不够,还须要一个alternate_key?这是由于一张照片会有4份副本,它们的图片ID相同,只是类型不一样(好比大图、小图、缩略图等),因而将图片ID做为needle.key,将类型做为needle.alternate_key。根据译者的理解,内存中映射不是一个简单的HashMap结构,而是相似一个两层的嵌套HashMap,Map<long *needle.key*="" ,map<int="" *alternate_key*="" ,object="">>。这样作可让4个副本共用同一个needle.key,避免为重复的内容浪费内存空间。

 

3.4.1 读取图片

Cache机器向Store机器请求一个图片时,它须要提供逻辑卷id、key、alternate key,和cookie。cookie是个数字,嵌在URL中。当一张新图片被上传,Directory为其随机分配一个cookie值,并做为应用元数据之一存储在Directory。它就至关于一张图片的"私人密码",此密码能够保证全部发往Cache或CDN的请求都是通过Directory"批准"的(Cache和Store都持有图片的cookie,若用户本身在浏览器中伪造、猜想URL或发起攻击,则会由于cookie不匹配而失败,从而保证Cache、Store能放心处理合法的图片请求)。

当Store机器接收到Cache机器发来的图片查询请求,它会利用内存中映射快速的查找相关的元数据。若是图片没有被删除,Store则在卷文件中seek到相应的offset,从磁盘上读取整个needle(needle的size能够提早计算出来),而后检查cookie和数据完整性,若所有合法则将图片数据返回到Cache机器。

 

3.4.2 写入图片

上传一个图片到Haystack时,web服务器向Directory咨询获得一个可写逻辑卷及其对应的多台Store机器,随后直接访问这些Store机器,向其提供逻辑卷id、key、alternate key、cookie和真实数据。每一个Store机器为图片建立一个新needle,append到相应的物理卷文件,更新内存中映射。过程很简单,可是append-only策略不能很好的支持修改性的操做,好比旋转(图片顺时针旋转90度之类的)。Haystack并不容许覆盖needle,因此图片的修改只能经过添加一个新needle,其拥有相同的key和alternate key。若是新needle被写入到与老needle不一样的逻辑卷,则只须要Directory更新它的应用元数据,将来的请求都路由到新逻辑卷,不会获取老版本的数据。若是新needle写入到相同的逻辑卷,Store机器也只是将其append到相同的物理卷中。Haystack利用一个十分简单的手段来区分重复的needle,那就是判断它们的offset(新版本的needle确定是offset最高的那个),在构造或更新内存中映射时若是遇到相同的needle,则用offset高的覆盖低的。

 

3.4.3 图片删除

在删除图片时,Store机器将内存中映射和卷文件中相应的flag同步的设置为已删除(软删除机制,此刻不会删除needle的磁盘数据)。当接收到已删除图片的查询请求,Store会检查内存中flag并返回错误信息。值得注意的是,已删除needle依然占用的空间是个问题,咱们稍后将讨论如何经过压缩技术来回收已删除needle的空间。

 

3.4.4 索引文件

Store机器使用一个重要的优化——索引文件——来帮助重启初始化。尽管理论上一个机器能经过读取全部的物理卷来从新构建它的内存中映射,但大量数据(TB级别)须要从磁盘读取,很是耗时。索引文件容许Store机器快速的构建内存中映射,减小重启时间。

Store机器为每一个卷维护一个索引文件。索引文件能够想象为内存中映射的一个"存档"。索引文件的布局和卷文件相似,一个超级块包含了一系列索引记录,每一个记录对应到各个needle。索引文件中的记录与卷文件中对应的needle必须保证相同的存储顺序。图6描述了索引文件的布局,Table2解释了记录中的不一样的字段。

使用索引帮助重启稍微增长了系统复杂度,由于索引文件都是异步更新的,这意味着当前索引文件中的"存档"可能不是最新的。当咱们写入一个新图片时,Store机器同步append一个needle到卷文件末尾,并异步append一个记录到索引文件。当咱们删除图片时,Store机器在对应needle上同步设置flag,而不会更新索引文件。这些设计决策是为了让写和删除操做更快返回,避免附加的同步磁盘写。可是也致使了两方面的影响:一个needle可能没有对应的索引记录、索引记录中没法得知图片已删除。

咱们将对应不到任何索引记录的needle称为"孤儿"。在重启时,Store机器顺序的检查每一个孤儿,从新建立匹配的索引记录,append到索引文件。咱们能快速的识别孤儿是由于索引文件中最后的记录能对应到卷文件中最后的非孤儿needle。处理完孤儿问题,Store机器则开始使用索引文件初始化它的内存中映射。

因为索引记录中没法得知图片已删除,Store机器可能去检索一个实际上已经被删除的图片。为了解决这个问题,能够在Store机器读取整个needle后检查其flag,若标记为已删除,则更新内存中映射的flag,并回复Cache此对象未找到。

 

3.4.5 文件系统

Haystack能够理解为基于通用的类Unix文件系统搭建的对象存储,可是某些特殊文件系统能更好的适应Haystack。好比,Store机器的文件系统应该不须要太多内存就可以在一个大型文件上快速的执行随机seek。当前咱们全部的Store机器都在使用的文件系统是XFS,一个基于"范围(extent)"的文件系统。XFS有两个优点:首先,XFS中邻近的大型文件的"blockmap"很小,可放心使用内存存储;第二,XFS提供高效文件预分配,减轻磁盘碎片等问题。

使用XFS,Haystack能够在读取一张图片时彻底避免检索文件系统元数据致使的磁盘操做。可是这并不意味着Haystack能保证读取单张图片绝对只须要一个磁盘操做。在一些极端状况下会发生额外的磁盘操做,好比当图片数据跨越XFS的"范围(extent)"或者RAID边界时。不过Haystack会预分配1GB的"范围(extent)"、设置RAID stripe大小为256KB,因此实际上咱们不多遭遇这些极端场景。

 

3.5 故障恢复

对于运行在普通硬件上的大规模系统,容忍各类类型的故障是必须的,包括硬盘驱动故障、RAID控制器错误、主板错误等,Haystack也不例外。咱们的对策由两个部分组成——一个为侦测、一个为修复。

为了主动找到有问题的Store机器,咱们维护了一个后台任务,称之为pitchfork,它周期性的检查每一个Store机器的健康度。pitchfork远程的测试到每台Store机器的链接,检查其每一个卷文件的可用性,并尝试读取数据。若是pitchfork肯定某台Store机器没经过这些健康检查,它会自动标记此台机器涉及的全部逻辑卷为只读。咱们的工程师将在线下人工的检查根本故障缘由。

一旦确诊,咱们就能快速的解决问题。不过在少数状况下,须要执行一个更加严厉的bulk同步操做,此操做须要使用复制品中的卷文件重置某个Store机器的全部数据。Bulk同步发生的概率很小(每月几回),并且过程比较简单,只是执行很慢。主要的瓶颈在于bulk同步的数据量常常会远远超过单台Store机器NIC速度,致使好几个小时才能恢复。咱们正积极解决这个问题。

3.6 优化

Haystack的成功还归功于几个很是重要的细节优化。

3.6.1 压缩

压缩操做是直接在线执行的,它能回收已删除的、重复的needle所占据的空间。Store机器压缩卷文件的方式是,逐个复制needle到一个新的卷文件,并跳过任何重复项、已删除项。在压缩时若是接收到删除操做,两个卷文件都需处理。一旦复制过程执行到卷文件末尾,全部对此卷的修改操做将被阻塞,新卷文件和新内存中映射将对前任执行原子替换,随后恢复正常工做。

 

3.6.2 节省更多内存

上面描述过,Store机器会在内存中映射中维护一个flag,可是目前它只会用来标记一个needle是否已删除,有点浪费。因此咱们经过设置偏移量为0来表示图片已删除,物理上消除了这个flag。另外,映射Value中不包含cookie,当needle从磁盘读出以后Store才会进行cookie检查。经过这两个技术减小了20%的内存占用。

当前,Haystack平均为每一个图片使用10byte的内存。每一个上传的图片对应4张副本,它们共用同一个key(占64bits),alternate keys不一样(占32bits),size不一样(占16bits),目前占用(64+(32+16)*4)/8=32个bytes。另外,对于每一个副本,Haystack在用hash table等结构时须要消耗额外的2个bytes,最终总量为一张图片的4份副本共占用40bytes。做为对比,一个xfs_inode_t结构在Linux中需占用536bytes。

 

3.6.3 批量上传

磁盘在执行大型的、连续的写时性能要优于大量小型的随机写,因此咱们尽可能将相关写操做捆绑批量执行。幸运的是,不少用户都会上传整个相册到Facebook,而不是频繁上传单个图片。所以只需作一些巧妙的安排就能够捆绑批量upload,实现大型、连续的写操做。

章节四、五、6是实验和总结等内容,这里再也不赘述了。

 

【扩展阅读】

提到CDN和分布式文件存储就不得不提到淘宝,它的商品图片不会少于Facebook的我的照片。其著名的CDN+TFS的解决方案因为为公司节省了巨额的预算开支而得到创新大奖,团队成员也获得不菲的奖金(羡慕嫉妒恨)。淘宝的CDN技术作了很是多的技术创新和突破,不过并不是本文范畴,接下来的讨论主要是针对Haystack与TFS在存储、检索环节的对比,并尝试提取出此类场景常见的技术难点。(译者对TFS的理解仅限于介绍文档,如有错误望读者矫正)

淘宝CDN、TFS的介绍请移步

<http: www.infoq.com="" cn="" presentations="" zws-taobao-image-store-cdn=""> 

http://tfs.taobao.org/index.html 

注意:下文中不少术语(好比应用元数据、Store、文件系统元数据等,都是基于本篇论文的上下文,请勿混淆)

上图是整个CDN+TFS解决方案的全貌,对应本文就是图3。CDN在前三层上实现了各类创新和技术突破,不过并不是本文焦点,这里主要针对第四层Storage(淘宝的分布式文件系统TFS),对比Haystack,看其是否也解决了long tail问题。下面是TFS的架构概览:

从粗粒度的宏观视角来看,TFS与Haystack的最大区别就是: TFS只care存储层面,它没有Haystack Cache组件;Haystack指望提供的是从浏览器、到CDN、到最终存储的一整套解决方案,架构定位稍有不一样,Haystack也是专门为这种场景下的图片服务所定制的,作了不少精细的优化;TFS的目标是通用分布式文件存储,除了CDN还会支持其余各类场景。

究竟是定制一整套优化的解决方案,仍是使用通用分布式文件存储平台强强联手?Facebook的工程师也曾纠结过(章节2.3),这个没有标准答案,各有所长,视状况去选择最合适的方案吧。

下面咱们以本文中关注的一些难点来对比一下双方的实现:

1 存储机器上的文件结构、文件系统元数据对策

Haystack的机器上维护了少许的大型物理卷文件,其中包含一系列needle来存储小文件,同时needle的文件系统元数据被全量缓存、持久化"存档"。

在TFS中(后文为清晰起见,引用TFS文献的内容都用淘宝最爱的橙色展现):

"……在TFS中,将大量的小文件(实际用户文件)合并成为一个大文件,这个大文件称为块(Block)。TFS以Block的方式组织文件的存储……"

"……!DataServer进程会给Block中的每一个文件分配一个ID(File ID,该ID在每一个Block中惟一),并将每一个文件在Block中的信息存放在和Block对应的Index文件中。这个Index文件通常都会所有load在内存……"

看来面对可怜的操做系统,你们都不忍心把海量的小文件直接放到它的文件系统上,合并成super block,维护super block中各entry的元数据和索引信息(并全量缓存),才是王道。这里TFS的Block应该对应到Haystack中的一个物理卷。

 

2 分布式协调调度、应用元数据策略

Haystack在接收到读写请求时,依靠Directory分析应用元数据,再结合必定策略(如负载均衡、容量、运维、只读、可写等),决定请求被发送到哪台Store机器,并向Store提供足够的存储或检索信息。Directory负责了总体分布式环境的协调调度、应用元数据管理职能,并基于此帮助实现了系统的可扩展性、容错性。

在TFS中:

"……!NameServer主要功能是: 管理维护Block和!DataServer相关信息,包括!DataServer加入,退出, 心跳信息, block和!DataServer的对应关系创建,解除。正常状况下,一个块会在!DataServer上存在, 主!NameServer负责Block的建立,删除,复制,均衡,整理……"

"……每个Block在整个集群内拥有惟一的编号,这个编号是由NameServer进行分配的,而DataServer上实际存储了该Block。在!NameServer节点中存储了全部的Block的信息……"

TFS中与Directory对应的就是NameServer了,职责大同小异,就是分布式协调调度和应用元数据分配管理,并基于此实现系统的平滑扩容、故障容忍。下面专门讨论一下这两个重要特性。

3 扩展性

Haystack和TFS都基于(分布式协调调度+元数据分配管理)实现了很是优雅的可扩展方案。咱们先回顾一下传统扩展性方案中的那些简单粗暴的方法。

最简单最粗暴的场景:

如今有海量的数据,好比data [key : value],有100台机器,经过一种策略让这些数据能负载均衡的发给各台机器。策略能够是这样,int index=Math.abs(key.hashCode)%100,这就获得了一个惟一的、肯定的、[0,99]的序号,按此序号发给对应的某台机器,最终能达到负载均衡的效果。此方案的粗暴显而易见,当咱们新增机器后(好比100变成130),大部分老数据的key执行此策略后获得的index会发生变化,这也就意味着对它们的检索都会发往错误的机器,找不到数据。

稍微改进的场景是:

如今有海量的数据,好比data [key : value],我假想本身是高富帅,有一万台机器,一样按照上述的策略进行路由。可是我只有100台机器,这一万台是假想的,怎么办?先给它们一个称号,叫虚拟节点(简称vnode,vnode的序号简称为vnodeId),而后想办法将vnode与真实机器创建多对一映射关系(每一个真实机器上100个vnode),这个办法能够是某种策略,好比故技重施对vnodeId%100获得[0,99]的机器序号,或者在数据库中建几张表维护一下这个多对一的映射关系。在路由时,先按老办法获得vnodeId,再执行一次映射,找到真实机器。这个方案还须要一个架构假设:个人系统规模在5年内都不须要上涨到一万台机器(5年差很少了,像我等码农估计一生也玩不了一万台机器的集群吧),所以10000这个数字"永远"不会变,这就保证了一个key永远对应某个vnodeId,不会发生改变。而后在扩容时,咱们改变的是vnode与真实机器的映射关系,可是此映射关系一改,也会不可避免的致使数据命中失败,由于必然会产生这样的现象:某个vnodeId(v1)原先是对应机器A的,如今变成了机器B。可是相比以前的方案,如今已经好不少了,咱们能够经过运维手段先阻塞住对v1的读写请求,而后执行数据迁移(以已知的vnode为粒度,而不是千千万万个未知的data,这种迁移操做仍是能够接受的),迁移完毕后新机器开始接收请求。作的更好一点,能够不阻塞请求,想办法作点容错处理和写同步之类的,能够在线无痛的完成迁移。

上面两个老方案还能够加上一致性Hash等策略来尽可能避免数据命中失败和数据迁移。可是始终逃避不了这样一个公式:

int machine_id=function(data.key , x)

machine_id指最终路由到哪台机器,function表明咱们的路由策略函数,data.key就是数据的key(数据ID之类的),x在第一个方案里就是机器数量100,在第二个方案里就是vnode数量+(vnode与机器的映射关系)。在这个公式里,永远存在了x这个未知数,一旦它风吹草动,function的执行结果就可能改变,因此它逃避不了命中失败。

只有当公式变成下面这个,才能绝对避免:

 Map<data.key,final machine_id=""> map = xxx; 

int machine_id=map.get(data.key);

注意map只是个理论上的结构,这里只是简单的伪代码,并不强制它是个简单的<key-value>结构,它的结构可能会更复杂,可是不管怎么复杂,此map都真实的、明确的存在,其效果都是——用data.key就能映射到machine_id,找到目标机器,无论是直接,仍是间接,反正不是用一个function去动态计算获得。map里的final不符合语法,加在这里是想强调,此map一旦为某个data.key设置了machine_id,就永不改变(起码不会由于平常扩容而改变)。当增长机器时,此map的已有值也不会受到影响。这样一个没有未知数x的公式,才能保证新老数据来了都能根据key拿到一个永远不变的machine_id,永远命中成功。

所以咱们得出这样一个结论,只要拥有这样一个map,系统就能拥有很是优雅平滑的可扩展潜力。当系统扩容时,老的数据不会命中失败,在分布式协调调度的保证下,新的增量数据会更倾向于写入新机器,整个集群的负载会逐渐均衡。

很显然Haystack和TFS都作到了,下面忽略其余细节问题,着重讨论一下它们是如何装备上这个map的。

读者回顾一下3.2章节留下的那个疑惑——原始URL中到底包含什么信息,是否是只有图片ID?Directory到底需不须要维护图片ID到逻辑卷的映射?

这个"图片ID到逻辑卷的映射",就是咱们须要的map,用图片ID(data.key)能get到逻辑卷ID(此值是upload时就明确分配的,不会改变),再间接从"逻辑卷到物理卷映射"中就能get到目标Store机器;不管是新增逻辑卷仍是新增物理卷,"图片ID到逻辑卷的映射"中的已有值均可以不受影响。这些都符合map的行为定义。

Haystack也所以,具有了十分优雅平滑的可扩展能力。可是译者提到的疑惑并无解答——"这个映射(图片ID到逻辑卷的映射)的数据量不能忽略不计,论文也不应一笔带过"

做者提到过memcache,也许这就是相关的解决方案,此数据虽然不小,可是也没大到望而生畏的地步。不过咱们依然能够发散一下,假如Haystack没保存这个映射呢?

这就意味着原始URL不仅包含图片ID,还包含逻辑卷ID等必要信息。这样也是遵循map的行为定义的,即便map的信息没有集中存储在系统内,可是却分散在各个原始URL中,依然存在。不可避免的,这些信息就要在upload阶段返回给业务系统(好比Facebook的照片分享应用系统),业务系统须要理解、存储和处理它们(随后再利用它们组装为原始URL去查询图片)。这样至关于把map的维护工做分担给了各个用户系统,这也是让人十分痛苦的,致使了不可接受的耦合。

咱们能够看看TFS的解决方案:

"……TFS的文件名由块号和文件号经过某种对应关系组成,最大长度为18字节。文件名固定以T开始,第二字节为该集群的编号(能够在配置项中指定,取值范围 1~9)。余下的字节由Block ID和File ID经过必定的编码方式获得。文件名由客户端程序进行编码和解码,它映射方式以下图……"

"……根据TFS文件名解析出Block ID和block中的File ID.……dataserver会根据本地记录的信息来获得File ID所在block的偏移量,从而读取到正确的文件内容……"

 一切,迎刃而解了…… 这个方案能够称之为"结构化ID"、"聚合ID",或者是"命名规则大于配置"。当咱们纠结于仅仅有图片ID不够时,能够给ID简单的动动手脚,好比ID是long类型,8个byte,左边给点byte用于存储逻辑卷ID,剩下的用于存储真实的图片ID(某些场景下还能够多截几段给更多的元数据),因而既避免了保存大量的映射数据,又避免了增长系统间的耦合,鱼和熊掌兼得。不过这个方案对图片ID有所约束,也不支持自定义的图片名称,针对这个问题,TFS在新版本中:

 "……metaserver是咱们在2.0版本引进的一个服务. 用来存储一些元数据信息, 这样本来不支持自定义文件名的 TFS 就能够在 metaserver 的帮助下, 支持自定义文件名了.……"

此metaserver的做用无疑就和Directory中部分应用元数据相关的职责相似了。我的认为能够二者结合左右开弓,毕竟自定义文件名这种需求应该不是主流。

值得商榷的是,全量保存这部分应用元数据其实仍是有不少好处的,最典型的就是顺带保存的cookie,有效的帮助Haystack不受伪造URL攻击的困扰,这个问题不知道TFS是如何解决的(大量的文件检索异常势必会影响系统性能)。若是Haystack的做者能和TFS的同窗们作个交流,说不定你们都能少走点弯路吧(这都是后话了~)

小结一下,针对第三个可扩展性痛点,译者描述了传统方案的缺陷,以及Haystack和TFS是如何弥补了这些缺陷,实现了平滑优雅的可扩展能力。此小节的最后再补充一个TFS的特性:

"……同时,在集群负载比较轻的时候,!NameServer会对!DataServer上的Block进行均衡,使全部!DataServer的容量尽早达到均衡。进行均衡计划时,首先计算每台机器应拥有的blocks平均数量,而后将机器划分为两堆,一堆是超过平均数量的,做为移动源;一类是低于平均数量的,做为移动目的……"

均衡计划的职责是在负载较低的时候(深夜),按计划执行Block数据的迁移,促进总体负载更加均衡。根据译者的理解,此计划会改变公式中的map,由于根据文件名拿到的BlockId对应的机器可能发生变化,这也是它为什么要在深夜负载较低时按计划缜密执行的缘由。其效果是避免了由于运维操做等缘由致使的数据分布不均。

 

4 容错性

Haystack的容错是依靠:一个逻辑卷对应多个物理卷(不一样机器上);"客户端"向一个逻辑卷的写操做会翻译为对多个物理卷的写,达到冗余备份;机器故障时Directory优雅的修改应用元数据(在牵涉到的逻辑卷映射中删除此机器的物理卷项)、或者标记只读,继而指导路由过程(分布式协调调度)将请求发送到后备的节点,避免请求错误;经过bulk复制重置来安全的恢复数据。等等。

在TFS中:

"……TFS能够配置主辅集群,通常主辅集群会存放在两个不一样的机房。主集群提供全部功能,辅集群只提供读。主集群会把全部操做重放到辅集群。这样既提供了负载均衡,又能够在主集群机房出现异常的状况不会中断服务或者丢失数据。……"

"……每个Block会在TFS中存在多份,通常为3份,而且分布在不一样网段的不一样!DataServer上……"

"……客户端向master dataserver开始数据写入操做。master server将数据传输为其余的dataserver节点,只有当全部dataserver节点写入均成功时,master server才会向nameserver和客户端返回操做成功的信息。……"

能够看出冗余备份+协调调度是解决这类问题的惯用范式,在大概思路上二者差很少,可是有几个技术方案却差异很大:

第一,冗余写机制。Haystack Store是将冗余写的责任交给"客户端"(发起写操做的客户端,就是图3中的web server),"客户端"须要发起屡次写操做到不一样的Store机器上;而TFS是依靠自身的master-slave机制,由master向slave复制。

第二,机房容错机制。TFS依然是遵循master-slave机制,集群也分主辅,主辅集群分布在不一样机房,主集群负责重放数据操做到辅集群。而Haystack在这方面没有详细介绍,只是略微提到"……Haystack复制每张图片到地理隔离的多个地点……"

针对上面两点,按译者的理解,Haystack可能更偏向于对等结构的设计,也就是说没有master、slave之分,各个Store是对等的节点,没有谁负责给谁复制数据,"客户端"向各个Store写入数据,一视同仁。

不考虑webserver、Directory等角色,只考虑Store,来分析一下它的容错机制:若是单台Store挂了,Directory在应用元数据的相关逻辑卷映射中删除此台机器的物理卷(此过程简称为"调整逻辑物理映射"),其余"对等"的物理卷能继续服务,没有问题;一整个机房挂了,Directory处理过程和单台故障相同,只是会对此机房中每台机器都执行一遍"调整逻辑物理映射",因为逻辑卷到物理卷的映射是在Directory中明确维护的,因此只要在维护和管理过程当中确保一个逻辑卷下不一样的物理卷分布在不一样的机房,哪怕在映射中删除一整个机房全部机器对应的物理卷,各个逻辑卷下依然持有到其余机房可用物理卷的映射,依然有对等Store的物理卷作后备,没有问题。

主从结构和对等结构各有所长,视状况选择。对等结构看似简洁美好,也有不少细节上的妥协;主从结构增长了复杂度,好比严格角色分配、约定角色行为等等(TFS的辅集群为什么只读?在主集群挂掉时是否依然只读?这些比较棘手也是由于此复杂度吧)

第三,修复机制。Haystack的修复机制依靠周期性后台任务pitchfork和离线bulk重置等。在TFS中:

"……Dataserver后台线程介绍……"

"……心跳线程……这里的心跳是指Ds向Ns发的周期性统计信息……负责keepalive……汇报block的工做……"

"……检查线程……修复checkfile_queue中的逻辑块……每次对文件进行读写删操做失败的时候,会tryadd_repair_task(blockid, ret)来将ret错误的block加入check_file_queue中……若出错则会请求Ns进行update_block_info……"

除了相似的远程心跳机制,TFS还多了在DataServer上对自身的错误统计和自行恢复,必要时还会请求上级(NameServer)帮助恢复。

 

5 文件系统

 Haystack提到了预分配、磁盘碎片、XFS等方案,TFS中也有所涉及:

"……在!DataServer节点上,在挂载目录上会有不少物理块,物理块以文件的形式存在磁盘上,并在!DataServer部署前预先分配,以保证后续的访问速度和减小碎片产生。为了知足这个特性,!DataServer现通常在EXT4文件系统上运行。物理块分为主块和扩展块,通常主块的大小会远大于扩展块,使用扩展块是为了知足文件更新操做时文件大小的变化。每一个Block在文件系统上以"主块+扩展块"的方式存储。每个Block可能对应于多个物理块,其中包括一个主块,多个扩展块。在DataServer端,每一个Block可能会有多个实际的物理文件组成:一个主Physical Block文件,N个扩展Physical Block文件和一个与该Block对应的索引文件……"

各有各的考究吧,比较了解底层的读者能够深刻研究下。 

6 删除和压缩

Haystack使用软删除(设置flag)、压缩回收来支持delete操做,在TFS中:

"……压缩线程(compact_block.cpp)……真正的压缩线程也从压缩队列中取出并进行执行(按文件进行,小文件合成一块儿发送)。压缩的过程其实和复制有点像,只是说不须要将删除的文件数据以及index数据复制到新建立的压缩块中。要判断某个文件是否被删除,还须要拿index文件的offset去fileinfo里面取删除标记,若是标记不是删除的,那么就能够进行write_raw_data的操做,不然则滤过……"

可见二者大同小异,这也是此类场景中经常使用的解决机制。

 

总结

本篇论文以long tail没法避免出发,探究了文件元数据致使的I/O瓶颈,推导了海量小文件的存储和检索方案,以及如何与CDN等外部系统配合搭建出整套海量图片服务。其在各个痛点的解决方案以及简约而不简单的设计值得咱们学习。文章末尾将这些痛点列出并与淘宝的解决方案逐一对比,以供读者发散。
英文原文:Facebook Haystack,编译:ImportNew - 储晓颖  新浪微博:@疯狂编码中的xiaoY

相关文章
相关标签/搜索