Google File System之扩展阅读

【扩展阅读】

“GFS……也支持小文件,但是不需要着重优化”,这是论文中的一句原话,初读此文时还很纳闷,GFS不是据说解决了海量小文件存储的难题吗,为何前后矛盾呢?逐渐深读才发现这只是个小误会。下面译者尝试在各个视角将GFS、TFS、Haystack进行对比分析,读者可结合前文基础,了解个中究竟。

1. 愿景和目标

GFS的目标可以一言以蔽之:给用户一个无限容量、放心使用的硬盘,快速的存取文件。它并没有把自己定位成某种特定场景的文件存储解决方案,比如小文件存储或图片存储,而是提供了标准的文件系统API,让用户像使用本地文件系统一样去使用它。这与Haystack、TFS有所不同,GFS的目标更加通用、更加针对底层,它的编程界面也更加标准化。

比如用户在使用Haystack存取图片时,可想而知,编程界面中肯定会有类似create(photo)、read(photo_id)这样的接口供用户使用。而GFS给用户提供的接口则更类似File、FileInputStream、FileOutputStream(以Java语言举例)这样的标准文件系统接口。对比可见,Haystack的接口更加高层、更加抽象,更加贴近于应用,但可能只适合某些定制化的应用场景;GFS的接口则更加底层、更加通用,标准文件系统能支持的它都能支持。举个例子,有一万张图片,每张100KB左右,用Haystack、GFS存储都可以,用Haystack更方便,直接有create(photo)这样的接口可以用,调用即可;用GFS就比较麻烦,你需要自己考虑是存成一万张小文件还是组装为一些大文件、按什么格式组装、要不要压缩……GFS不去管这些,你给它什么它就存什么。假如把一万张图片换成一部1GB的高清视频AVI文件,总大小差不多,一样可以放心使用GFS来存储,但是Haystack可能就望而却步了(难道把一部电影拆散放入它的一个个needle?)。

这也回答了刚刚提到的那个小误会,GFS并不是缺乏对小文件的优化和支持,而是它压根就没有把自己定位成小文件存储系统,它是通用的标准文件系统,它解决的是可靠性、可扩展性、存取性能等后顾之忧,至于你是用它来处理大文件、存储增量数据、打造一个NoSQL、还是解决海量小文件,那不是它担心的问题。只是说它这个文件系统和标准文件系统一样,也不喜欢数量太多的小文件,它也建议用户能够将数据合理的组织安排,放入有结构有格式的大文件中,而不要将粒度很细的一条条小数据保存为海量的小文件。 相反,Haystack和TFS则更注重实用性,更贴近应用场景,并各自做了很多精细化的定制优化。在通用性和定制化之间如何抉择,前文2.3中Haystack的架构师也纠结过,但是可以肯定的是,基于GFS,一样可以设计needle结构、打造出Haystack。

2 存储数据结构

这里的数据结构仅针对真实文件内容所涉及的存储数据结构。三者在这种数据结构上有些明显的相同点:

首先,都有一个明确的逻辑存储单元。在GFS中就是一个Chunk,在Haystack、TFS中分别是逻辑卷和Block。三者都是靠各自的大量逻辑存储单元组成了一个庞大的文件系统。设计逻辑存储单元的理由很简单——保护真正的物理存储结构,不被用户左右。用户给的数据太小,那就将多个用户数据组装进一个逻辑存储单元(Haystack将多个图片作为needle组装到一个逻辑卷中);用户给的数据太大,那就拆分成多个逻辑存储单元(GFS将大型文件拆分为多个chunk)。总之就是不管来者是大还是小,都要转换为适应本系统的物理存储格式,而不按照用户给的格式。一个分布式文件系统想要保证自身的性能,它首先要保证自己能基于真实的物理文件系统打造出合格的性能指标(比如GFS对chunk size=64MB的深入考究),在普通Linux文件系统上,固定大小的文件+预分配空间+合理的文件总数量+合理的目录结构等等,往往是保证I/O性能的常用方案。所以必须有个明确的逻辑存储单元。

另一个很明显的相同点:逻辑存储单元和物理机器的多对多关系。在GFS中一个逻辑存储单元Chunk对应多个Chunk副本,副本分布在多个物理存储chunkserver上,一个chunkserver为多个chunk保存副本(所以chunk和chunkserver是多对多关系)。Haystack中的逻辑卷与Store机器、TFS中的Block与DataServer都有这样的关系。这种多对多的关系很好理解,一个物理存储机器当然要保存多个逻辑存储单元,而一个逻辑存储单元对应多个物理机器是为了冗余备份。

三者在数据结构上也有明显的区别,主要是其编程界面的差异导致的。比如在Haystack、TFS的存储结构中明确有needle这种概念,但是GFS却不见其踪影。这是因为Haystack、TFS是为小文件定制的,小文件是它们的存储粒度,是用户视角下的存储单元。比如在Haystack中,需要考虑needle在逻辑卷中如何组织检索等问题,逻辑卷和needle是一对多的关系,一个逻辑卷下有多个needle,某个needle属于一个逻辑卷。这些关系GFS都不会去考虑,它留给用户自行解决(上面愿景和目标里讨论过了)。

3 架构组件角色

Haystack一文中的对比已经看到分布式文件系统常用的架构范式就是“元数据总控+分布式协调调度+分区存储”。在Haystack中Directory掌管所有应用元数据、为客户端指引目标(指引到合适的目标Store机器做合适的读写操作,指引过程就是协调调度过程),单个Store机器则负责处理好自身的物理存储、本地数据读写;TFS中NameServer控制所有应用元数据、指引客户端,DataServer负责物理存储结构、本地数据读写。可以看出这个范式里的两个角色——协调组件、存储组件。协调组件负责了元数据总控+分布式协调调度,各存储组件作为一个分区,负责实际的存储结构和本地数据读写。架构上的相同点显而易见——GFS也满足此范式,作为协调组件的master、和作为存储组件的chunkserver。在组件角色的一些关键设计上,三者也曾面临了一些相似的技术难题,比如它们都竭力减少对协调组件的依赖、减少其交互次数,不希望给它造成过大压力。协调组件为啥显得这么脆弱、这么缺乏安全感呢?从两篇论文都可以看出,GFS和Haystack的作者非常希望协调组件能够简化,其原因很简单——人如果有两个大脑那很多事会很麻烦。协调组件可认为是整个系统的大脑,它不停的派发命令、指点迷津;如果大脑有两个,那客户端听谁的、两个大脑信息是否需要同步、两个大脑在指定策略时是否有资源竞争……就跟人一样,这种核心总控的大脑有多个会带来很多复杂度、一致性问题,难以承受。即使各平台都有备用协调组件,比如GFS的阴影master,但都只是作为容灾备用,不会在线参与协调。

4 元数据

虽然Haystack的Directory、TFS的NameServer、GFS的master干的是同样的活,但是其在元数据问题上也有值得讨论的差异。上篇文章中曾重点介绍了译者对于全量缓存应用元数据的疑惑(Haystack全量缓存所有图片的应用元数据,而TFS用了巧妙的命名规则),难道GFS不会遇到这个难题吗?而且此篇文章还反复强调了GFS的master是单例的、纯内存的,难道它真的不会遭遇单点瓶颈吗?答案还是同一个:编程界面。GFS的编程界面决定了即使它整个集群存了几百TB的数据,也不会有太多的文件。对于Haystack和TFS,它们面对的是billions的图片文件,对于GFS,它可能面对的仅仅是一个超巨型文件,此文件里有billions的图片数据。也就是说Haystack和TFS要保存billions的应用元数据,而GFS只需保存一个。那GFS把工作量丢给谁了?对,丢给用户了。所以GFS虽然很伟大、很通用、愿景很酷,但是对特定应用场景的支持不一定友好,这也是Haystack最终自行定制的原因之一吧。

同时,这也是GFS敢于设计单点、内存型master的原因,它认为一个集群内的文件数量不会超过单点master的能力上限。同样的,GFS也没有着重介绍文件系统元数据的方案(Haystack文章中反复强调了文件系统元数据牵扯的I/O问题),原因也是一样:文件系统元数据的I/O问题对于Haystack来说很难缠是因为Haystack要面对海量小文件,而GFS并不需要。 有了这样的前提,GFS的master确实可以解脱出来,做更多针对底层、面向伟大愿景的努力。比如可以轻轻松松的在单机内存中全量扫描所有元数据、执行各种策略(如果master受元数据所累、有性能压力、需要多个master分布式协作,那执行这种全局策略就会很麻烦了,各种一致性问题会扑面而来)。

不过GFS中会多出一种Haystack和TFS都没有的元数据——文件命名空间。当你把GFS当做本地磁盘一样使用时,你需要考虑文件的目录结构,一个新文件产生时是放到一个已有目录还是创建新目录、创建新文件时会不会有另一个请求正在删除父目录……这些内容是GFS特有的(Haystack和TFS的用户不需要面对这些内容),所以它考虑了master的命名空间锁、操作日志顺序等机制,来保护命名空间的更新。

5 控制流、数据流(以及一致性模型)

GFS、Haystack、TFS的控制流大致相同,其思路不外乎:1 客户端需要发起一次新请求;2 客户端询问协调组件,自己应该访问哪个存储组件;3 协调组件分析元数据,给出答复;4 客户端拿到答复直接访问指定的存储组件,提交请求;5 各存储组件执行请求,返回结果。

但是细节上各自有差异,上篇文章提到Haystack是对等结构,客户端直接面对各对等存储组件(比如写入时看到所有物理卷、分别向其写入),而TFS是主备结构,客户端只面对主DataServer,主向备同步数据……GFS在这方面最大的差异在于:

5.1 租赁机制。回想Haystack,客户端直接将写请求提给各个Store机器即可解决问题,GFS也是对等设计,为何要捣鼓出令人费解的租赁机制?不能直接由客户端分别写入各个对等chunkserver吗?这个问题已经在一致性模型章节讨论过,当时提到了租赁机制是为了保证各个副本按相同顺序串行变异,那我们也可以反过来问一句:Haystack、TFS里为啥没有遇到这等问题?是它们对一致性考虑太少了吗? 这个答案如果要追本溯源,还是要牵扯到编程界面的问题:Haystack、TFS中用户面对的是一个个小图片,用户将图片存进去、拿到一个ID,将来只要保证他拿着此ID依然能找到对应图片就行,即使各个副本执行存储时顺序有差别,也丝毫不影响用户使用(比如Hasytack收到3个并发图片A、B、C的存储请求,提交到3台Store机器,分别存成了ABC、ACB、BAC三种不同的顺序,导致副本内容不一致,但是当用户无论拿着A、B、C哪个图片ID来查询、无论查的是哪个Store机器,都能检索到正确的图片,没有问题;Haystack和TFS的编程界面封装的更高级,GFS在底层遭遇的难题影响不到它们)。而在GFS比较底层的编程界面中,用户面对的是众多图片组装而成的一整个文件,如果GFS在不同副本里存储的文件内容不一致,那就会影响用户的检索逻辑,那摊上大事儿了。另外,Haystack、TFS只支持小文件的append和remove,而GFS怀抱着伟大的愿景,它需要支持对文件的随机写,只有保证顺序才能避免同个文件区域并发随机写导致的undefined碎片问题。所以GFS花心思设计租赁机制也就合情合理了。

谈到串行就不得不考虑一下性能问题,串行绝不是大规模并发系统的目标,而只是一种妥协——对于多核操作系统下的I/O密集型应用,当然是越并行越有益于性能。比如现在有10个并发的append请求,是逐个依次执行,每一个都等待上一个I/O处理完成吗?这样每个请求的I/O Wait时间cpu不就空闲浪费吗?根据译者的推测,GFS所谓的串行仅仅应该是理论上的串行(即执行效果符合串行效果),但真正执行时并不会采用加锁、同步互斥、按顺序单线程依次执行等性能低劣的方案。以10个并发append为例,理论上只需要一个AtomicLong对象,保存了文件长度,10个append线程可并发调用其addAndGet(),得到各自不冲突的合法偏移位置,继而并发执行I/O写入操作,互不干扰。再比如对同个文件的随机偏移写,也不是要全部串行,只有在影响了同一块文件区域时才需要串行,因此可以减小串行粒度,影响不同区域的请求可并行写入。通过这样的技巧,再结合在首要chunkserver上分配的唯一序号,GFS应该可以实现高性能的串行效果,尤其是append操作。

TFS和Haystack的文章之所以没有过多提及一致性问题,并不是因为它们不保证,而是在它们的编程界面下(面向小文件的append-only写),一致性问题不大,没有什么难缠的陷阱。

5.2 数据流分离。Haystack没法在数据流上做什么文章,它的客户端需要分别写入各个Store机器;TFS可以利用其主备机制,合理安排master-slave的拓扑位置以优化网络负载。而GFS则是完全将真实文件的数据流和控制流解耦分离,在网络拓扑、最短路径、最小化链式传输上做足了文章。系统规模到一定程度时,机房、机架网络拓扑结构对于整体性能的影响是不容忽略的,GFS在这种底层机制上考虑的确实更加到位。

6 可扩展性和容错性

在上篇文章中已经详尽的讨论了Haystack、TFS是如何实现了优雅的高可扩展性。对于元数据不成难题的GFS,当然也具备同样的实力,其原理就不重复叙述了。值得一提的是,GFS从元数据的负担中解放出来后,它充分利用了自身的优势,实现了各种全局策略、系统活动,极大的提高了系统的可扩展性及附属能力,比如restore、重负载均衡等等。而且其并不满足于简单的机器层面,还非常深入的考虑了网络拓扑、机架布置……GFS这些方面确实要领先于同类产品。

容错性的对比同样在上篇文章中详细描述过,在结构方面GFS的chunkserver也是对等结构(控制流的首次之分与容错无关),其效果与Haystack的容错机制类似,无论是机器故障还是机房故障。相对来说,GFS在故障的恢复、侦测、版本问题、心跳机制、协调组件主备等环节介绍的更加细致,这些细节Haystack没有怎么提及。值得一提的差异是GFS在数据完整性上的深入考究,其checksum机制可有效的避免腐化的数据,这一点Haystack、TFS没有怎么提及。

7  删除和修改

很多时候我们会重点关注一个系统是否能删除和修改数据,这是因为当今越来越多架构设计为了追求更高的主流可用性,而牺牲了其他次要的特性。比如你发表一篇微博时一瞬间就提交成功,速度快、用户体验非常好;结果你发现不小心带了一句“大概八点二十分发”,傻眼了,到处找不着微博修改的按钮;于是你只能删除老微博,再发一条新的,老微博的评论、转发都丢失了。当你不承认自己发过八点二十分的微博时,网友拿出了截图,你说是PS的,新浪在旁冷笑,你那条八点二十分的微博一直在原处,从未被删除,估计要过好几个小时之后才会真正从磁盘删掉。

在学习Haystack、TFS、GFS的过程中我们可以看出大家都有这种倾向,原因可能有很多,其中有两个是比较明显的。首先,对于存储的数据结构,增加新数据造成的破坏较小,而删除、修改造成的破坏较大。新增只是往末尾附加一些新data,而不会影响已有的data;删除则导致已有data中空出了一块区域,这块区域不能就这么空放着,最低劣的做法是直接将已有data进行大规模位移来填补狭缝,比较婉转的做法是先不着急等不忙的时候做一次碎片整理;修改会导致某条数据size发生变化,size变小会导致狭缝碎片,size变大则空间不足,要挤占后面数据的位置。其次,新增数据可以做到无竞争(刚才5.1讨论了GFS的原子append,并发的新增操作影响的是不同的文件区域,互不干扰),而删除和修改则很难(并发的删除和修改操作可能影响相同的文件区域,这就必须有条件竞争)。有这两个原因存在,大家都偏爱append、不直接实现修改和删除,就不难理解了。

不过GFS依然支持直接的修改功能——随机偏移写。这并不是因为GFS的架构设计更强大,而是因为它不需要承担某些责任。比如在Haystack和TFS中,它们需要承担图片在真实文件中的存储格式、检索等责任,当将一个图片从100KB修改为101KB时,对存储格式的破坏是难以承受的,所以它们不支持这种直接的修改,只能采用删除+新增来模拟修改。而GFS并没有维护一个文件内部格式的责任,还是那句话,你交给它什么它就存什么。所以用户会保护自己的文件内部格式,他说可以写那就可以写,GFS并没有什么难题,只需解决好一致性、defined、统一变异顺序等问题即可。

同样,GFS的删除与Haystack、TFS的删除,其意义也不同。GFS的删除指的是整个大文件的删除。Haystack和TFS删除的是用户视角下的一条数据,比如一张图片,它是逻辑存储单元中的一个entry而已。而GFS的删除则是用户视角下的一整个文件,一个文件就对应了多个逻辑存储单元(chunk),里面也包含了海量的应用实体(比如图片)。在这种差异的背景下,他们面临的难题也完全不同。Haystack和TFS面临的就是刚才提到的“删除会破坏存储文件已有数据的格式、造成狭缝碎片”问题,它们的对策就是懒惰软删除、闲了再整理。而GFS则不会遭遇此难题,用户在文件内部删除几条数据对于GFS来说和随机偏移写没啥区别,狭缝碎片也是用户自己解决。GFS需要解决的是整个文件被删除,遗留下了大量的chunk,如何回收的问题。相比Haystack和TFS,GFS的垃圾回收其实更加轻松,因为它要回收的是一个个逻辑存储单元(chunk),一个chunk(副本)其实就是一个真实的Linux文件,调用file.delete()删了就等于回收了,而不需要担心文件内部那些精细的组织格式、空间碎片。

8 其他细节和总结

GFS还是有很多与众不同的技巧,比如合理有效的利用操作日志+存档来实现元数据持久化存储;比如细粒度、有条不紊的命名空间锁机制;便捷的快照功能等等。从整体风格来说,GFS偏爱底层上的深入考究,追求标准化通用化的理想,它希望别人把它当成一个普通的文件系统,无需华丽的产品包装,而只是务实的搭建好底层高可靠、高可用、高可扩展的基础。但是美好的不一定是合适的,通用的不一定是最佳的,Haystack、TFS在追求各自目标的道路上一样风雨无阻披荆斩棘。这两篇论文的翻译和对比,只希望能将巨头的架构师们纠结权衡的分岔路口摆到读者面前,一起感同身受,知其痛,理解其抉择背后的意义。

posted on 2018-08-15 13:18  lucelu  阅读(201)  评论(0编辑  收藏  举报

导航