首先说下 BLOB 的意思, 英文全称是 Binary Large OBjects,能够理解为任意二进制格式的大对象;在 Facebook 的语境下,也就是用户在帐户里上传的的图片,视频以及文档等数据,这些数据具备一次建立,屡次读取,不会修改,偶尔删除 的特色。node
以前简单翻译了 Facebook 的前驱之做 —— Haystack,随着业务量发展,数据量进一步增大,过去玩法又不转了,若是全部 BLOG 都用 Haystack 存,因为其三备份的实现,在这个量级下,性价比很低。可是彻底用网络挂载+传统磁盘+Unix-like(POSIX)文件系统等冷存储,读取跟不上。因而计算机科学中最经常使用的分而治之的思想登场了。数据库
他们首先统计了 BLOBs 的访问频次与建立时间的关系,而后提出了随着时间推移 BLOB 访问出现的冷热分布概念(和长尾效应差很少)。并据此提出了热、温分开的访问策略:用 HayStack 当作热存储去应对那些频繁访问的流量,而后用 F4 去响应剩下的不那么频繁访问的 BLOB流量,在此假设(F4只存储那些基本不怎么变更,访问量相对不大的数据)前提下,能够大大简化 F4 的设计。固然有个专门的路由层于二者之上进行了屏蔽,并进行决策和路由。后端
对于 Haystack 来讲,从其论文出来时,已通过去了七年(07~14)。相对于当时,作了少量更新,好比说去掉了 Flag 位,在 data file,Index file 以外,增长了 journal file,专门用来记录被删除的 BLOB 条目。缓存
对于 F4 来讲,主要设计目的在于保证容错的前提下尽量的减少***有效冗余倍数***(effective-replication-factor),以应对日益增加的***温数据*** 存取需求。此外更加模块化,可扩展性更好,即能以加机器方式平滑扩展应对数据的不断增加。服务器
我总结一下,本论文主要高光点就是温热分开,冗余编码,异地取或。网络
到2014年,Facebook 大概有超 4000 亿张图片。架构
论文的结论是,访问频度的热力图是存在的,建立时间是影响其变化关键因子,并且温部数据是持续增加的。运维
论文的度量方法也很简单,就是追踪其网站上不一样类型的 BLOB 数据的访问频次随着建立时间变化曲线,建立时间小于一天的数据的访问频次大概是建立时间一年的数据的100多倍。具体数据就不列了,能够去 paper 里看。异步
而后论文探讨了区分热数据和温数据的一个界限,经过对访问频次和删除频次随着建立时间的变化的分析,对于大部分 BLOG,获得了一个的大概值:一个月。可是有两个例外,一个是用户头像,一直是热数据;另一个普通图片,使用三个月做为阈值。模块化
热数据老是那些头部数据,相对来讲增加较慢。可是历史数据,也就是温数据是随着时间推移而尾巴愈来愈长,这势必要求存储架构进行相应的调整。
设计原则是让每一个组件尽量简单、内聚而且高度契合其要承担的工做。这是从 UNIX 以来就一直在强调的一个原则。下图是整体架构图,包括建立(C1-C2,由 Haystack 负责),删除(D1-D2,大部分是 Haystack 负责,少部分是 f4 负责)和读取(R1-R4 由 Haystack 和 f4 共同负责)。
如前述论文 Haystack 所述,咱们将一批 BLOG 集结为逻辑卷,尽量减小 meta 信息,从而减小IO次数。每一个逻辑卷咱们设计了 100G 左右的容量,在满以前是为 未锁定 (unlocked) 的状态,一旦达到容量,就变为锁定 (locked)状态,只容许读取和删除。
每一个卷包含三个文件,一个数据文件,一个索引文件和一个备忘文件(journal file)。和 Haystack 论文提到的同样,数据文件就是记录 BLOG 自己和其原信息,索引文件就是内存中的查找结构的快照。备忘文件是新增的,它经过记录全部被删除的 BLOG 的记录来进行删除操做。而原 Haystack 论文中,删除文件是经过直接修改索引文件和数据文件来实现的。在未锁定阶段,三个文件都可读写,在锁定阶段,只有备忘文件能够读写,其余两个文件都会变成只读的。
统筹整个系统,好比提供新的存储机器;维持一个未锁定卷(unlocked volumes )的池子;确保全部逻辑卷有足够的物理卷来备份;根据需求适时建立物理卷;进行周期性的维护任务,好比说数据紧缩(compaction)和垃圾回收。
路由层负责BLOB 存储系统向对外提供接口,它屏蔽了系统底层的实现,使得能够方便添加如 f4 同样的子系统。全部的路由层的机器角色都是同样的,由于该层将全部状态(如逻辑卷到物理卷的映射)都存在了另外的数据库里(将全部相关状态收集起来额外存储,使得剩下的部分无状态能够平滑扩展,这也是系统设计经常使用的原则)。这使得路由层的能够不依赖其余模块来平滑扩展。
对于读取请求,路由模块会从 BLOB id 中解析出 逻辑卷 id,而后根据数据库中读出的映射关系来找到对应的全部物理卷信息。通常来讲会从最近一个主机取数据,若是失败的话,会产生一个超时事件,去下一个物理卷所在的主机进行尝试。
对于建立请求,路由模块会选取一个有空闲空间的逻辑卷,而后将 BLOB 发送到该逻辑卷对应的全部物理卷进行写入(是并行发,仍是链式发仍是串行发?)若是遇到任何问题,就会中断写,而且已经写入的数据会被废弃,且户从新挑选一个可用逻辑卷,重复上述过程。(看起来像并行写,容错策略也超级粗暴)
对于删除请求,路由模块会将其发送到全部对应的物理卷(而后就快速返回),而后对应物理主机程序会异步的进行删除,遇到错误就一直重试,直到成功删除全部对应物理卷上的对应 BLOB。(倒也简单,但不知道实现的时候是会写入 journal file 后返回,仍是只是在内存中标记下就返回。对应的数据文件上的 BLOB 确定是在 compact 的时候才会删掉)。
路由层经过将实现细节隐藏,使得(对用户)无感知地构建温存储成为可能,当一个卷被从热存储移到温存储的时候,会在二者上同时存在一段时间,直到有效(逻辑卷到物理卷)的映射被更新后,客户端的请求将被无感知的地导向温存储。
转换层负责处理对检索到的 BLOB 数据的变换操做,好比图片的缩放和裁剪。在 Facebook 的老版本的系统中,这些计算密集型的操做会在存储节点上完成。
增长转换层能够解放存储节点,使其专一于提供存储服务。将计算任务分离出来也有利于将存储层和转换层进行独立的扩展。而后,它也可让咱们精确地控制存储节点的容量以刚好知足需求。更进一步,也可使咱们针对不一样任务类型进行更优的硬件选型。好比说咱们能够将存储节点设计为具备大量硬盘,但只有一个CPU和少许内存。
一开始是为了处理热点 BLOB 数据的请求,缓解后端存储系统的压力。对于温存储来讲,它也能够减少其请求压力。这里说的应该是 CDN 以及相似 akamai 内容分发商提供的缓存。
Haystack 开始是被设计来尽量的提升 IOPS 的,经过揽下全部建立请求,大部分的删除请求和高频读请求,使得温存储的设计能够大大简化。
如相关 paper 提到的,Haystack 经过合并 BLOB 和简化元信息使得 IOPS 大大提升。具体来讲,包括将逻辑卷设计为集合了一批 BLOB 的单个文件,利用三个物理卷对同一个逻辑卷进行冗余备份等等。
读请求过来后,会在内存中拿到请求的 BLOB 的元信息,而且看其是否被删除,而后经过物理文件位置+ offset + size ,仅进行一次 IO 拿到对应 BLOB 数据。
当主机收到建立请求后,会同步的将 BLOB 数据追加到数据文件上,而后更新内存中的元信息并将更改写入索引文件和备忘文件中(备忘文件不是只记录删除操做吗?)。
当主机收到删除请求时,会更新索引文件和备忘文件。可是对应数据仍然存在于数据文件中,按期地咱们会进行紧缩操做,才会真正的删除数据,并回收相应空间。
Haystack 经过在一个数据中心的不一样机架上各放一个副本,而后再不一样数据中心再放一个副本的三副本策略得到了对硬盘,主机,机架甚至数据中心的容错能力。而后经过 RAID-6(1.2倍冗余数据编码,可以小范围的纠正错误,能够读读纠错码之类的文章)进行额外的硬盘容错,更上一层保险。可是付出的代价是 3*1.2 = 3.6 倍的有效冗余因子,这也是 Haystack 的局限之处,虽然最大化了 IOPS,可是在存储使用上却并不高效,形成了不少 BLOB 的数据冗余。
有些类型的 BLOB 具备必定的过时时间,好比说用户上传的视频,会从原始格式转化为咱们的存储格式。在此以后原始视频须要删掉。咱们会避免将此类具备过时时间的数据移动到 F4 上,从而让 Haystack 负责这些频繁的删除请求,并经过频繁紧缩来回收空间。
设计目标是在容错的基础上尽量高效。也就是在可以容忍硬盘错误,主机故障,机架问题,数据中心灾难的前提下,把有效冗余倍数降一降。
f4 是温数据存储架构的子系统。包含一系列 数据单元(cell),每一个 cell 都在同一个数据中心(机房,datacenter)里。当前(2014)的 cell 包含 14 个机架,每一个机架有15个主机,每一个主机有三十块 4T 容量的硬盘。cell 负责存储逻辑卷,每一个逻辑卷实际存储时,会将数据利用里所码(Reed-Solomon coding,简称RS,这是前面提到的RAID-6 标准的重要成员)进行冗余编码,好比 RS(n, k) 就是每存 n 个比特,就要编入额外的 k 个比特,以此来容忍最多 k 个比特的出错。经过这种编码方式能够解决硬盘,主机和机架出错问题。
此外利用异或编码(XOR coding)来解决跨数据中心或者地理位置的出错问题。咱们选取两个不一样机房的对等数量 volume/stripe/block 结成对子,而后将每一对的异或值存在第三个机房。
每一个 f4 数据单元(cell) 只处理锁定的卷(Volume),也就是只用支持读取和删除操做。数据文件和索引文件都是只读的,Haystack 中的备忘文件在 f4 中是不存在的。咱们用了另外一种方式来达到“删除”的效用,将每一个 BLOB 进行加密后存储,将用于加密的秘钥(key)存在一个外部数据库中。响应删除请求时,只须要将 BLOB 对应的秘钥删掉就行(有点绝,对用户提供了隐私保证,并且将删除操做的延时降到很低)。
索引文件因为比较小,直接用了三副本存储来保证可靠性,能够省去编解码带来的额外复杂度。数据文件用 n=10, k = 4 的里所码进行编码。具体来讲,将每一个数据文件切分为 n 个连续的数据块(block),每一个具备固定尺寸 b(最后一个块不满,而又写不进去一个新 BLOB 的状况下,在结尾补零,相似这种打 padding 也是数据对齐经常使用的手法);对于每 n 个这样的块,生成 k 个一样尺寸的奇偶校验块(parity block),这样 n+k 个数据块构成一个逻辑上的 条带(stripe)。同一条带上的任意两个块互称为兄弟块(companion block)。正常读取时,能够直接从数据块中读(我猜是那n个块,不用额外进行计算还原,有待考证,还得看里所码原理以及具体实现)。若是某些块不可用了,就会在同一条带上任取 n 块,解码后还原;此外还有个性质,就是读取 n 个 block 上对应的 n 截数据(好比某个 BLOB),也能够进行解码(这两个性质都是编码决定的,相似于 n 元线性方程组,有 k 个冗余方程)。
一般 b 为 1G,即每一个数据块(Block)选取 1G 大小(这有个疑问,看起来每一个Block仍在Volume中,而不是单独拿出来,那么定位一个物理block是否是就得经过 volume 文件打开句柄 + offset + length),选这么大有两方面的考虑,一个是尽可能减少 BLOB 的跨块几率,以减小读取一个 BLOB 还得屡次 IO 的频率;另外一个是下降 block 所须要维护的总元信息数量。不选更大的是由于重建起来会付出更大代价(但为何就是 1G 呢?)。
下图是架构图,接下来逐一介绍下各个模块。
name node 维护了数据块、奇偶校验块 到实际存储这些块的存储节点(也就是下一节的存储节点)之间的映射;这些映射(利用标准技术?还说参考了GFS,这没大看懂,留个坑回头读 GFS 填上)分配到存储节点中。名字节点使用主从备份策略进行容错。
存储节点是 Cell 的主要组件,处理全部常规的读取和删除请求。对外暴露两个 API:Index API 负责提供 Volume 的有无检查和位置信息;File API 提供实际的数据访问。(File API 与 Data API 的区别估计在于,前者是提供上层抽象 BLOB 的操做接口,然后者会暴露底层数据块 Block 的访问的接口)
存储节点将 index file (包括BLOB到 volume 的映射,偏移量和长度)存在硬盘上,而且加载到自定义存储结构的内存中。此外还维持了volume 偏移量到物理数据块的映射(因为一个 volume 被整齐的切成了好多 block, 所以定位一个数据块的逻辑位置,须要记下他的所在volume+offset)。上述两个信息都被存在内存里,以免硬盘 IO(彷佛后面也有变化,index 也不小随着ssd更便宜,存ssd也能够)。;
因为每一个 BLOB 都是加密过的,其秘钥放在额外的存储,一般是数据库中。经过删除其秘钥就能够达到事实上的 BLOB 的删除,这样就避免了数据紧缩(为何能够不回收那些删除空间呢,毕竟对于文存储,删除量只有很小一部分,以前的温存储的假设就用在这里);同时也省去了用备忘文件(journal file)来追踪删除信息。
下面说下读取流程。首先经过 Index API 来检查文件是否存在(R1过程),而后将请求转到该 BLOB 所在的数据块所在的存储节点上。Data API 提供了对数据块和奇偶校验块(parity block)的访问。正常状况下的读请求会被导向合适的存储节点(R2流程),而后直接从该 BLOB 所在块读取它(R3)。在失败的状况下,会经过 Data API 读取损坏模块中的全部 n+k 个兄弟模块中无缺的 n 个块,送到回退节点(back-off node)进行重建。
在进行实际数据读取(不管是 R1-R3 的正常流程仍是 R1,R4,R5的出错回退流程)的同时,路由层(route tier)会并行的从外部数据库读取该 BLOB 对应的秘钥,而后在路由层进行解密操做,这是一个计算密集型任务,放在这里可让数据层专一于存储,而且两层能够独立的扩展。
就是负责给出正常读取流程出错时的一种回退方案。
当 cell 中出现故障时,会有些块变得不可用,就须要从其兄弟块和奇偶校验块中进行在线恢复。回退模块都是IO稀疏而计算密集型节点,来处理这些计算密集型的在线恢复操做。
回退模块对外暴露 File API,以处理正常读取失败状况下的回退重试(R4)。在此时,读取请求已经被一个主卷服务器(primary volume-server,不过这是个什么节点?)解析成了数据文件,偏移量和长度的元组,回退节点会向除损坏数据块以外的 n-1 个兄弟块和 k 个奇偶校验块中对应偏移量,读取对应长度的信息。只要收到n个回应(估计是并行发?而后为了节省时间,收到任意n个回应就开始干活,进行差错纠正?)
固然了,回了照顾读取延迟,每次进行在线回退读纠错的时候,都只恢复对应BLOB的数据而不是其所在的整个数据块 Block 的信息。整个数据块的恢复会交给重建节点(Rebuilder Nodes)离线的去作。
在民用物理机数目达到必定量级的状况下,硬盘和节点的故障是不可避免的。存储在损坏模块上的数据块就须要进行重建。重建节点是存储稀疏而计算密集型的,负责在后台默默地进行重建工做。每一个重建节点经过探针(按期扫描其负责的范围内的数据?仍是在每一个数据节点上安装探针?)检测数据块错误,而且将其汇报到协调节点(Coordinator Nodes),而后经过取出同一条带(Stripe)上兄弟块和奇偶校验块中的没有损坏过的n块,对损坏节点进行重建(若是n+k中有其余模块坏了估计也一并重建吧)。这是一个很重的处理过程,而且会给存储节点带来极大的网络和 IO 负载。所以重建节点会对其吞吐量进行限流,以防对正常的用户请求形成不利影响。而统筹调度重建工做,以尽可能减少数据丢失的风险,则是协调节点的工做。
一个数据单元(cell)须要不少平常的运维任务,好比安排(大概就是肯定一个重建顺序,而且在不一样的重建节点间进行分配吧)损坏的数据块重建,调整当前的数据分布以尽量减少数据的不可用几率。协调节点也是存储稀疏计算密集型的,用来执行数据单元范围的任务。
如以前提到的,一个数据条带上的不一样数据块须要被分散放置于不一样的数据容错区域内以最大化可靠性。然而,在通过故障,重建和替换后,确定会有一些不符合上述原则的状况,好比两个同条带上的数据块被放在了同一个数据容错区域中。协调节点会运行一个平衡摆放位置的进程去检查一个数据单元中的数据块分布。和重建操做同样,也会给存储节点带来至关大的额外硬盘和网络负载,所以协调节点也会进行自我限流以减少对正常请求的影响。
单个 f4 的数据单元都存在一个数据中心中,所以难以抵御数据中心的故障。因而在开始的时候,咱们将两份一样的数据单元放在不一样的数据中心中,这样一个损坏仍然能够利用另外一个对请求进行响应。这样将有效冗余因子从 Haystack 的 3.6 下降到了 2.8 。
考虑到数据中心级别的故障仍是很稀少的,咱们找到了一种能够进一步减少有效冗余因子的方案——固然,也减少了吞吐率。不过,如今XOR方案能够将有效冗余因子进一步作到 2.1。
地理备份异或编码(XOR coding)方案经过将两个不一样的卷(Volume,大小同样)作异或后的结果放在第三个数据中心的方式,提供了数据中心级别的容错。如图9同样,每一个数据卷中的数据块和奇偶校验块被与等量的其余数据块或者奇偶校验块(称为哥们块,buddy block)被拿来作异或运算,获得其异或块(XOR block)。这些异或模块的索引也是简单的三备份存储。
一旦某个 datacenter出现问题致使整个 volume 不可用,读取请求会被路由到一个叫作 geo-bakoff node ,而后会从两个 buddy node 和 XOR node 所在数据中心去取对应 BLOB 数据,进行损坏 BLOB的重建。选择XOR编码,固然是简单又能知足需求。
负载因子的计算,(1.4 * 3) / 2 = 2.1
基本思想大概就这些,剩下的不翻了。可是论文说的有点啰嗦,同一个点在不一样地方说了好几遍,但同时一个模块有时又分散在不一样模块中,很差连成一个总体,在这里,我简单总结一下。
一个数据单元(cell)存在一个数据中心中,包含 14 个机架。一个逻辑上的卷 (Volume),大约 100G,被分为 100 个 1G 的数据块(Block);而后每 10 个数据块做为一组(Companion Block)进行数据冗余编码(RS编码)后,产生 4 个新的奇偶校验块(Parity Block),这 14 个数据块+奇偶校验块称为一个条带(stripe),被分别放置在不一样机架上以进行容错。其中哪些数据块属于一组的映射关系在名字节点( Name Node) 中维持着。
在存储节点上,内存中须要维护两个映射做为 index 信息,一个是 BLOB id 到 volume,偏移量和大小的映射,一个是 volume 偏移量到 Block 实际物理位置的映射。当读请求失败的时候,读取请求连同一些元信息(好比所在数据块 id,以及在其上的偏移量)被导向回退节点(Backoff Node)。回退节点会根据 BLOB id 所在的 Block id 在 Name Node 拿到条带上其余数据块位置信息,以及偏移量,只对该 BLOB 的全部对等数据进行解码,还原出该 BLOB 后返回。
此外,协调节点(Coordinator Nodes)会根据探针的心跳信息,获得全局数据分布和状态信息。协调节点据此将损坏的模块交给重建节点(Rebuilder Nodes)进行数据重建;而且平衡、维持条带上的全部块被放在不一样的数据容错阈。
最后,在两个不一样数据中心的将全部数据块配对后,进行异或(XOR)操做,获得一个异或结果,放在第三个数据中心。这样,这三个数据中心的任何数据条带损坏到 RS 码都没法拯救的状况下(好比有四个以上机架出问题了),就能够经过其余两个数据中心数据进行 XOR 操做来抢救一下。
数据文件(data file):存储一堆 BLOB 和其元信息的的文件
索引文件(index file):记录 BLOB 在数据文件偏移量,长度和简单信息的文件,用来快速 seek 取出 BLOB。
备忘文件(journal file):在 Haystack 中,用于记录全部的删除请求。
有效备份因子,有效冗余倍数(effective-replica-factor):实际占用的物理空间和要存的逻辑数据大小之间的比值。
兄弟模块,伙伴模块(companion block):用于编码的 n+k 个数据块中那 n 个模块的称呼。
奇偶校验块(parity block):用于编码 n+k 个数据块中那 k 个模块的称呼
温存储(warm storage):相对于热存储,指那些专门针对访问频次不怎么高的数据所构建的存储。
存储节点,存储机器(storage nodes,storage machines):都是指的负责存储最终数据的的物理机。
紧缩(compact):Haystack 中会按期地检查数据文件,将其复制一遍,可是略过全部重复和已经标记删除的数据,从而回收对应空间。
副本,备份(replica):一种冗余策略,廉价通用型机器上免不了出错,为了留有后手进行恢复,最经常使用策略就是多存几份了,这几份一样的数据成为多副本或者多备份。
秘钥(encryption key):用来给 BLOB 进行加密的键
回退模块(backoff node):其实我以为翻译成兜底模块也挺好哈哈,就是应对出错,取 n 个兄弟块来进行恢复的。
数据单元(cell):由14个机架,每一个机架上有15台机器组成的一个数据部署和回滚的的单元。
数据卷(volume):分逻辑卷和物理卷,包含多个数据条带。
数据条带(stripe):原始n个数据块和生成的k个奇偶校验块所组成的集合,称为条带。
数据块(block):通常是1G左右,被分散在不一样容错单元中。