以大型分布式文件系统的鼻祖GFS为例,看一下分布式文件系统的实现原理

作为互联网技术的先驱,谷歌率先遇到了大数据(即大规模的搜索索引)的问题。2003年,谷歌发表了GFS论文,向业界介绍了其分布式文件系统设计方案。当时,著名的开源搜索引擎Apache Lucene的作者Doug Cutting也正在被同样的问题所困扰,看到谷歌的这篇论文后,他很吃惊,这正是他所需要的啊!于是,以此GFS的设计为蓝图,他开始用Java实现一个分布式文件系统,这就是后来的Hadoop HDFS。

受GFS设计思想影响的不只有Doug Cutting,还有国内的互联网巨头们。淘宝面临的问题有些不同,它要存储的是大量的商品快照缩略图(促销时为了防止商家赖账,淘宝就将商家在促销时打出的价格和优惠措施截图保存起来),单个缩略图尽管不大,但数量众多,因此传统的文件系统无法有效存储[2]。通过采用和GFS类似的架构与设计,淘宝开发了自己的TFS分布式文件系统。京东的JFS和百度的BFS在架构和设计上也是与GFS非常类似的。

下面我们以大型分布式文件系统的鼻祖GFS为例,看一下分布式文件系统的实现原理。

1.关于GFS的几点假设

GFS论文中提到了几点假设。

  • 使用普通商用服务器而不是专用服务器,以降低成本。
  • 因为使用的是普通商用服务器,且服务器数量很多(上千台),所以硬件发生故障是常态。
  • 文件尺寸都比较大,几个GB的文件是常态。
  • 文件的写操作主要是在文件尾的添加操作,随机写很少。
  • 一旦文件生成,则主要是顺序读操作。
  • 文件系统和应用程序是紧密结合在一起的,而且是一起进行设计的。

2.GFS对外提供的接口

与单机文件系统类似,GFS也提供了文件与目录的抽象和类似于POSIX[3]文件系统API的编程接口,支持文件和目录的创建、删除、打开、关闭、读和写操作。此外,GFS还支持高效的取快照(snapshot)操作和满足原子性的记录追加(record append)操作。

GFS提供了一个客户端库,实现了所有对外提供的API。

3.GFS架构

图9-1展示的是GFS的架构。

整个系统包括几个角色:多个GFS客户端(GFS Client)、一个GFS主服务器(GFS Master Server)、0个或多个GFS影子主服务器(GFS Shadow Master Server)和多个GFS数据块服务器(GFS Chunk Server)。

  • GFS客户端:客户端通过GFS提供的客户端库使用GFS提供的功能。
  • GFS主服务器:主服务器上面存放了整个GFS系统的元数据(命名空间、权限控制、文件/数据库/副本之间的映射)。元数据存储在主服务器的内存中。对元数据进行修改前,要先写操作日志,只有在写日志成功后才能对元数据进行修改。操作日志保存在磁盘上,且在多个机器上保存多份。为了避免操作日志变得太大,每隔一段时间,GFS就创建一个检查点(checkpoint)。检查点被组织成一棵紧凑的、类似于B树的形式存储在磁盘上,便于快速加载到内容中使用。GFS系统重启或恢复时,仅需要加载最新的检查点,然后再重放操作日志即可。
  • GFS影子主服务器:影子主服务器对外提供“只读”服务,它上面保存的元数据不保证是最新的,与主服务器上保存的元数据相比可能会有一点滞后(通常不到1秒)。因此,在主服务器重启期间,那些可以接收非最新数据的应用可以通过影子服务器继续使用GFS。
  • GFS数据块服务器:数据块的实际存储者,它和主服务器有心跳联系,并告诉主服务器它上面保存的文件块信息,主服务器据此维护其保存的元数据。

..\19-0423二校改图\0901.tif{80%}

图9-1 GFS架构

每个GFS文件都被分成固定大小的数据块(64 MB)。数据块以文件的形式被保存在运行着Linux的GFS数据块服务器上。为了增强可靠性,每个数据块被保存在多个数据块服务器上(默认为3个)。

GFS主服务器通过周期性的心跳机制监控数据块服务器的状态。

4.读操作的实现

读操作的步骤如图9-1所示,具体描述如下。

(1)编译时,GFS客户端连接GFS客户端库。读文件内容的API(即open函数),带至少两个参数,其中一个是文件名称,另一个是开始读的文件偏移(offset)。因为数据块的大小是固定的,所以可将文件偏移转换成块索引(chunk index)。然后,GFS客户端库发送一个请求给主服务器,请求中包含欲读的文件名和块索引。

(2)收到来自客户端库的请求后,主服务器查询元数据,得到欲读块的各个副本的存储位置。然后,返回给客户端库一个句柄(handle)及其各个副本的存储位置。

(3)收到主服务器返回的元数据后,GFS客户端库就挑选一个副本(通常是最近的一个副本),然后发送读请求给存放这个副本的数据块服务器。

(4)该数据块服务器返回欲读块的内容给客户端库。

从上面的步骤可见,只有在查询块存放位置时,GFS客户端才需要和主服务器打交道。一旦获知块存放放置后,GFS客户端就直接和数据块服务器联系,而不需要主服务器了。

5.单数据块写操作的实现

因为一个数据块被存储多份(一般为3份),分布在多个数据块服务器上,所以对某个块的写操作必须修改其所有副本。为了减轻主服务器对写操作的管理负担,GFS引入了租期机制(lease mechanism)。

假设每个块被存储3份,那么主服务器就从这3个副本中选出一个作为主副本(primary replica),在租约(一般为60秒)到期之前,这个主副本一直负责该数据块的多个副本的一致性,并定义租约内对该数据块所有写操作的执行顺序。其余的两个副本为从副本(secondary replica)。

图9-2演示了GFS单数据块写操作的实现方式,单数据块写操作的步骤如下。

(1)同读操作类似,GFS客户端库先将要写的文件偏移转换成块索引,然后发送请求给主服务器,查询该数据块的存储位置。

(2)主服务器查询自己维护的元数据,将该数据块的3个副本所在的数据块服务器以及当前的主副本是谁等信息返回给客户端库。

(3)为了提高写操作的执行性能,客户端库先将欲写入的数据块内容推送到3个副本所在的数据块服务器上。收到后,数据块服务器将其缓存到本地的LRU(Least Recently Used)缓存中。

(4)当所有的副本都确认数据块接收成功后,客户端库就发送写请求给主副本。收到写请求后,主副本就给该写请求分配一个序列号。该序列号用于给当前租约期内的所有写操作赋予一个确定的顺序。然后,主副本按照序列号约定的顺序,在本地执行该写操作。

(5)当本地的写入成功后,主副本将写请求转发给其余两个从副本所在的数据块服务器。每个从副本都按照序列号约定的顺序执行该写请求。

(6)从副本返回给主副本写操作执行完成,无论是否写入成功。

(7)主副本返回给客户端库写操作执行完成。因为主副本一定写成功了,否则主副本不会将写请求转发给从副本,所以最后的结果一定是“主副本成功 + 0个或2个从副本成功”。

(8)如果不是所有的副本都写成功,客户端库就重复第3~7步数次,直到所有的副本都写成功或者重试次数达到最大值为止。

(9)如果重试次数达到最大值后依然有某些从副本没有写成功,那么主服务器很快会发现该数据块的副本数低于3,因此它就会选择新的数据块服务器,并将数据块复制到上面去,以满足数据副本数不低于3的要求。

..\19-0423二校改图\0902.tif{80%}

图9-2 GFS写操作的实现

6.多数据块写操作的实现

如果一次写操作跨多个数据块,那么GFS将其拆分成多个单数据块写操作而分别独立执行。

如果有两个多数据块写操作A和B,假如A涉及两个连续的数据块A1和A2,B涉及两个连续的数据块A2和A3。注意,A和B都会修改数据块A2。由于GFS并发地执行这4个单数据块写操作,因此最后A2的结果是不确定的,这取决于是A的A2先执行,还是B的A2先执行,但不论哪个先执行,由于单数据块写操作中的序列号机制,最后A2的内容要么来自A,要么来自B,而不会是二者的混合。

换句话说,对于单数据块写操作,GFS是串行执行的,因此能保证其原子性。而对于跨多个数据块的写操作,GFS不能保证多个单数据块操作的串行性,因此也就不能保证其原子性。

7.追加操作的实现

GFS对追加操作有一个限制,就是追加的数据大小不能超过数据块大小的1/4。

GFS的追加操作很多地方与单数据块的写操作类似,不同的地方如下。

(1)在单数据块操作的第1步,客户端库向主服务器查询的是最后一个数据块的存储位置。

(2)在单数据块操作的第4步,当多个副本(即最后一个数据块的多个副本)所在的数据块服务器都收到欲追加的数据后,客户端库给主副本所在的数据块服务器发送追加请求。主副本所在的数据块服务器收到追加请求后,检查其剩余的空间是否能够容纳得下欲追加的数据。如果剩余空间不够,就将剩余空间用某种方式填充满[4],实际上就是将最后一块的剩余空间浪费了,然后通知各个从副本也做同样的处理,然后通知客户端库在下一个数据块上重试。如果剩余空间足以容纳欲追加的数据,则将其追加到最后一块中,然后通知从副本也在同样的偏移追加。

(3)在单数据块操作的最后一步,如果不是全部副本都追加成功了,则客户端库一直重试,直到全部副本都追加成功为止。

上面的追加处理方式,对于追加的数据,在某些副本中有可能会被追加多次。例如,第一次尝试时,副本1、2成功,但副本3失败了。第二次尝试时,3个副本都追加成功了。那么,在副本1和2中,数据被追加了两次,而副本3则只追加了一次。但无论如何,最后一次尝试的所有副本都是写成功的。另外,每次写的位置(即文件的偏移值)都是一样的。也就是说,GFS并不保证不同副本的每个字节都是相同的,但保证每份副本都包含所有的数据,虽然有些数据会被包含多次。

8.取快照操作的实现

熟悉Linux内核的读者都知道,当fork()/clone()系统调用创建一个新的进程时,并不会复制当前进程的所有内存页面到新建的进程中,而是采用了所谓的写时复制(Copy On Write,COW)技术,即只是增加当前进程所有内存页面的引用计数,只有当某个页面的内容在以后的某个时刻被修改时,才会给新进程建立该页面的独立副本。这种做法的好处是显而易见的,既节约了内存空间,也缩短了系统调用的执行时间。

GFS的取快照操作也采用了COW技术。具体地说就是,当对一个文件(或目录)取快照时,并不会立刻对该文件(或目录)的所有数据块进行复制,而是仅增加其引用计数,只有当以后某个数据块的内容被修改时,才为该数据块创建新的独立副本。

9.GFS的保证

因为GFS上的文件和目录内容都存储了多份,所以就存在数据一致性的问题。对此,GFS的实现有如下保证。

(1)对于文件(或目录)命名空间的修改(如创建新文件),GFS通过加锁保证其原子性。

(2)每次写操作后,文件(或目录)最后一个字节的偏移值在各个副本中都是一样的。

(3)每次写操作修改的文件(或目录)位置(即偏移值)都是一样的。

(4)单数据块的写操作保证其原子性,而且结果是确定的。

(5)一致性是指多个副本上都保存有同样的数据,确定性是指无论几个多数据块写操作如何执行,其结果都是唯一的。因此,对于多数据块写操作,GFS仅保证其多个副本上的数据是一致的,但不保证有确定的结果,因为多个单数据块写操作是独立进行的。

(6)追加操作保证其原子性,而且结果是确定的。

本文截选自《分布式系统设计实践》,李庆旭 著

  • 系统梳理现有的各种分布式技术
  • 具体阐述各种分布式产品的设计思想和架构,包含前端、数据库、存储技术等几大重要内容
  • 架构师、工程师、项目管理人员参考书

本书将分布式系统中涉及的技术分为前端构造技术、分布式中间件技术和分布式存储技术三大类,对每类技术都详细介绍了其原理、设计思想和架构,以及相关应用场景。此外,本书还总结了分布式系统的构建思想,并分别对业界几个非常成功的大型分布式系统(谷歌搜索系统、淘宝网电商平台、阿里云公有云平台、领英社交平台)进行了案例研究。

发布了455 篇原创文章 · 获赞 273 · 访问量 81万+

猜你喜欢

转载自blog.csdn.net/epubit17/article/details/103277831
今日推荐