简介

本文档定义了Hadoop兼容文件系统所需的行为规范,面向Hadoop文件系统的实现者、维护者以及Hadoop FileSystem API的用户

大多数Hadoop操作都在Hadoop测试套件中针对HDFS进行了测试,最初通过MiniDFSCluster进行,在发布前由供应商特定的"生产"测试验证,并隐含地通过其上的Hadoop堆栈进行验证。

HDFS的行为基于POSIX文件系统行为建模,参考了Unix文件系统操作的动作和返回代码。即便如此,HDFS在某些方面仍与POSIX文件系统的预期行为存在差异。

捆绑的S3A文件系统客户端使通过文件系统API访问亚马逊的S3对象存储("blobstore")成为可能。Azure ABFS、WASB和ADL对象存储文件系统与微软的Azure存储进行通信。所有这些都绑定到对象存储,它们确实具有不同的行为,特别是在一致性保证和操作原子性方面。

"本地"文件系统提供对平台底层文件系统的访问。其行为由操作系统定义,可能与HDFS表现不同。本地文件系统的特殊行为示例包括:大小写敏感性、尝试在另一个文件上重命名文件时的操作,以及是否可以在文件末尾之后执行seek()操作。

还有一些由第三方实现的文件系统声称与Apache Hadoop兼容。目前没有正式的兼容性测试套件,因此除了通过他们自己的兼容性测试外,任何人都无法正式声明兼容性。

这些文档并非试图提供兼容性的规范性定义。通过相关测试套件并不保证应用程序的正确行为。

测试套件定义的是预期的操作集——未能通过这些测试将突显潜在问题。

通过使合约测试的每个方面都可配置,可以声明文件系统与标准合约部分的不同之处。这些信息可以传达给文件系统的用户。

命名

本文档遵循RFC 2119关于使用MUST(必须)、MUST NOT(禁止)、MAY(可以)和SHALL(应当)的规范。MUST NOT被视为规范性要求。

Hadoop FileSystem API的隐含假设

原始的FileSystem类及其使用基于一组隐含假设。主要是假设HDFS为底层文件系统,并且它提供了POSIX文件系统行为的一个子集(或至少是由Linux文件系统提供的POSIX文件系统API和模型的实现)。

无论使用哪种API,所有与Hadoop兼容的文件系统都应遵循Unix实现的文件系统模型:

  • 它是一个包含文件和目录的层次化目录结构。

  • 文件包含零个或多个字节的数据。

  • 您不能将文件或目录放在文件下方。

  • 目录包含零个或多个文件。

  • 目录条目本身不包含数据。

  • 您可以将任意二进制数据写入文件。当从集群内部或外部的任何位置读取文件内容时,数据会被返回。

  • 您可以在单个文件中存储数十GB的数据。

  • 根目录 "/" 始终存在,且无法重命名。

  • 根目录 "/" 始终是一个目录,无法通过文件写入操作覆盖。

  • Any attempt to recursively delete the root directory will delete its contents (barring lack of permissions), but will not delete the root path itself.

  • 无法对目录自身进行重命名或移动操作。

  • 您无法在除源文件本身之外的任何现有文件上重命名/移动目录。

  • 目录列表会返回该目录下的所有数据文件(即可能存在隐藏的校验和文件,但所有数据文件都会被列出)。

  • 目录列表中文件的属性(如所有者、长度)与实际文件属性匹配,并与打开的文件引用视图一致。

  • 安全性:如果调用者缺乏执行操作的权限,操作将失败并引发错误。

路径名称

  • 路径由以"/"分隔的路径元素组成。

  • 路径元素是一个由1个或多个字符组成的Unicode字符串。

  • 路径元素不能包含字符 ":""/"

  • 路径元素不应包含ASCII/UTF-8值为0-31的字符。

  • 路径元素不能是 "."".."

  • 还需注意,Azure blob存储文档指出路径不应使用尾随的"."(因为其.NET URI类会将其去除)。

  • 路径基于Unicode码点进行比较。

  • 不得使用不区分大小写和特定于区域设置的比较。

安全假设

除了安全相关的特殊章节外,本文档假设客户端对文件系统拥有完全访问权限。因此,列表中大多数条目不会额外说明"假设用户具备使用所提供参数和路径执行操作的权限"这一前提条件。

未指定用户缺乏安全权限时的故障模式。

网络假设

本文档假设所有网络操作均成功。所有声明均可理解为“假设操作不会因网络可用性问题而失败”

  • 网络故障后,FileSystem的最终状态是未定义的。

  • 网络故障后,FileSystem的即时一致性状态是未定义的。

  • 如果网络故障能够被报告给客户端,该故障必须是IOException或其子类的实例。

  • 异常详情应包含适合经验丰富的Java开发人员运维团队开始诊断的诊断信息。例如,在ConnectionRefused异常中应包含源主机名、目标主机名及端口信息。

  • 异常详情中可包含适合初级开发人员进行诊断的诊断信息。例如,当TCP连接请求被拒绝时,Hadoop会尝试包含对ConnectionRefused的引用。

Hadoop兼容文件系统的核心要求

以下是Hadoop兼容文件系统的核心要求。某些文件系统可能无法满足所有这些要求,因此部分程序可能无法按预期运行。

原子性

某些操作必须是原子性的。这是因为它们通常用于实现集群中进程之间的锁定/独占访问。

  1. 创建一个文件。如果overwrite参数为false,检查和创建操作必须是原子性的。
  2. 删除文件。
  3. 重命名文件。
  4. 重命名目录。
  5. 使用mkdir()创建单个目录。
  • 递归目录删除可能是原子操作。尽管HDFS提供原子递归目录删除功能,但其他Hadoop文件系统(包括本地文件系统)均不提供此类保证。

大多数其他操作没有原子性要求或保证。

一致性

Hadoop文件系统的一致性模型是单副本更新语义;这与传统的本地POSIX文件系统相同。需要注意的是,即使是NFS也放宽了关于变更传播速度的某些限制。

  • 创建. 当对一个新创建文件的输出流执行close()操作完成后,集群内查询该文件元数据和内容的操作必须立即能看到该文件及其数据。

  • 更新. 一旦对写入新创建文件的输出流执行close()操作完成后,集群内查询文件元数据和内容的操作必须立即能看到新数据。

  • 删除。 一旦对非根目录路径的delete()操作成功完成,该路径必须不可见且不可访问。具体而言,listStatus()open()rename()append()操作必须失败。

  • 先删除后创建。 当一个文件被删除后,又创建了同名的新文件时,新文件必须立即可见,并且其内容可以通过FileSystem API访问。

  • 重命名。rename()操作完成后,针对新路径的操作必须成功;尝试通过旧路径访问数据必须失败。

  • 集群内部的一致性语义必须与集群外部保持一致。所有查询未被主动操作文件的客户端,无论其位置如何,都必须看到相同的元数据和数据。

并发性

无法保证对数据的隔离访问:如果一个客户端正在与远程文件交互,而另一个客户端更改了该文件,这些更改可能可见也可能不可见。

操作与故障

  • 所有操作最终都必须完成,无论成功与否。

  • 完成操作的时间未定义,可能取决于系统实现和当前状态。

  • 操作可能会抛出RuntimeException或其子类异常。

  • 操作应将所有网络、远程及高层级问题作为IOException或其子类抛出,而不应为此类问题抛出RuntimeException

  • 操作应通过抛出异常来报告失败,而不是通过操作返回特定的错误代码。

  • 在文本中,当提到异常类名称时,例如IOException,抛出的异常可能是该命名异常的实例或子类。但不能是其父类。

  • 如果某个操作在类中没有实现,则实现必须抛出UnsupportedOperationException

  • 实现方案可以选择重试失败的操作直到成功。如果采用此方式,应确保任何操作序列之间的happens-before关系满足所声明的一致性和原子性要求。参见HDFS-4849示例:HDFS未实现任何可能被其他调用者观察到的重试机制。

未定义的容量限制

以下是文件系统容量的一些限制,这些限制从未被明确定义过。

  1. 一个目录中的最大文件数量。

  2. 目录中的最大目录数

  3. 文件系统中条目(文件和目录)的最大总数。

  4. 目录下文件名的最大长度(HDFS: 8000)。

  5. MAX_PATH - 引用文件的整个目录树的总长度。Blobstores 通常会在约1024个字符处停止。

  6. 路径的最大深度(HDFS:1000个目录)。

  7. 单个文件的最大大小。

未定义的超时

操作超时完全没有定义,包括:

  • 阻塞文件系统操作的最大完成时间。MAPREDUCE-972记录了distcp在缓慢的s3重命名操作中出现的问题。

  • 在关闭空闲读取流之前的超时时间。

  • 写入流在关闭前的空闲超时时间。

在HDFS中,阻塞操作超时实际上是可变的,因为站点和客户端可以调整重试参数,从而将文件系统故障和故障转移转换为操作暂停。相反,存在一个普遍假设,即文件系统操作"快速但不如本地文件系统操作快",并且数据读取和写入的延迟会随着数据量的增加而增加。客户端应用程序的这一假设揭示了一个更基本的假设:就网络延迟和带宽而言,文件系统是"接近"的。

关于某些操作的开销还存在一些隐含假设。

  1. seek() 操作速度很快,几乎不会或完全不会产生网络延迟。[这不适用于blob存储]

  2. 对于条目较少的目录,目录列表操作速度较快。

  3. 对于条目较少的目录,目录列表操作速度很快,但对于包含大量条目的目录,可能会产生O(entries)级别的开销。Hadoop 2新增了迭代式列表功能,以解决在不缓冲的情况下列出包含数百万条目的目录所带来的挑战,但会牺牲一致性。

  4. 无论文件操作是否成功,OutputStreamclose()方法执行速度都很快。

  5. 删除目录所需的时间与其子条目数量无关

对象存储 vs. 文件系统

本规范在部分地方提到对象存储,经常使用术语Blobstore。Hadoop确实为其中一些存储提供了FileSystem客户端类,尽管它们违反了许多要求。

请查阅特定存储的文档,以确定其与特定应用程序和服务的兼容性。

什么是对象存储?

对象存储是一种数据存储服务,通常通过HTTP/HTTPS协议访问。PUT请求用于上传对象/"Blob";GET请求用于检索对象;范围GET操作允许检索Blob的部分内容。要删除对象,则调用HTTP的DELETE操作。

对象通过名称存储:名称是一个字符串,可能包含“/”符号。不存在目录的概念——只要在服务提供商设定的命名规则限制范围内,可以为对象分配任意名称。

对象存储通常提供一种操作来检索具有给定前缀的对象;即对服务根目录执行GET操作并附带适当的查询参数。

对象存储通常优先考虑可用性——没有像HDFS NameNode那样的单点故障。它们还致力于提供简单的非POSIX API:允许的操作仅限于HTTP动词。

Hadoop FileSystem 客户端针对对象存储的设计目标是让这些存储系统模拟成与HDFS具有相同特性和操作的文件系统。但这终究只是一种模拟:它们具有不同的特性,偶尔这种假象会被打破。

  1. 一致性。对象可能是最终一致的:对象变更(创建、删除和更新)可能需要时间才能对所有调用者可见。实际上,无法保证变更对刚发起变更的客户端立即可见。例如,对象test/data1.csv可能被新数据集覆盖,但在更新后不久发起GET test/data1.csv调用时,返回的仍是原始数据。Hadoop假设文件系统具有一致性;即文件的创建、更新和删除操作立即可见,且目录列表结果能实时反映该目录下的文件状态。

  2. 原子性。Hadoop假设目录rename()操作和delete()操作都是原子性的。对象存储FileSystem客户端将这些操作实现为对名称匹配目录前缀的单个对象的操作。因此,更改是一次一个文件进行的,不具备原子性。如果操作在过程中部分失败,对象存储的状态将反映部分完成的操作。还需注意,客户端代码假设这些操作是O(1)的——而在对象存储中,它们更可能是O(child-entries)的。

  3. 持久性。Hadoop假设OutputStream实现会在flush()操作时将数据写入其(持久化)存储。对象存储实现会将所有写入的数据保存到本地文件,该文件仅在最终的close()操作时才PUT到对象存储中。因此,从不包含任何不完整或失败操作的部分数据。此外,由于写入过程仅在close()操作时才开始,该操作可能需要与上传数据量成正比的时间,并与网络带宽成反比。它也可能会失败——这种失败最好上报而不是忽略。

  4. 授权。Hadoop使用FileStatus类来表示文件和目录的核心元数据,包括所有者、用户组和权限。对象存储可能没有可行的方法来持久化这些元数据,因此它们可能需要用存根值填充FileStatus。即使对象存储持久化了这些元数据,对象存储仍然可能无法像传统文件系统那样强制执行文件授权。如果对象存储无法持久化这些元数据,那么推荐的约定是:

    • 文件所有者显示为当前用户。
    • 文件组也会被报告为当前用户。
    • 目录权限显示为777。
    • 文件权限报告为666。
    • 设置所有权和权限的文件系统API能够成功执行且不报错,但这些操作实际不会生效。

具有这些特性的对象存储不能直接替代HDFS。就本规范而言,它们对指定操作的实现与要求不符。Hadoop开发社区认为它们受支持,但支持程度不如HDFS。

时间戳

FileStatus 条目包含修改时间和访问时间。

  1. 关于这些时间戳何时设置以及它们是否有效的具体行为因文件系统而异,甚至可能在同一个文件系统的不同安装实例之间也有所不同。
  2. 时间戳的粒度同样取决于具体的文件系统,甚至可能因个别安装环境而异。

HDFS文件系统在写入时不会更新修改时间。

具体来说

  • FileSystem.create() 创建:会列出一个零字节的文件;修改时间设置为NameNode上显示的当前时间。
  • 通过create()调用返回的输出流写入文件时:修改时间不会改变
  • 当调用OutputStream.close()时,所有剩余数据将被写入,文件关闭,并且NameNode会更新文件的最终大小。修改时间设置为文件关闭的时间。
  • 通过append()操作打开文件进行追加时,在输出流调用close()之前不会改变文件的修改时间。
  • FileSystem.setTimes() 可用于显式设置文件的时间。
  • 当文件被重命名时,其修改时间不会改变,但源目录和目标目录的修改时间会被更新。
  • 很少使用的操作:FileSystem.concat()createSnapshot()createSymlink()truncate()都会更新修改时间。
  • 访问时间粒度以毫秒为单位设置 dfs.namenode.access.time.precision;默认粒度为1小时。如果精度设置为零,则不会记录访问时间。
  • 如果未设置修改或访问时间,则FileStatus字段的值为0。

其他文件系统可能具有不同的行为。特别是,

  • 访问时间可能支持也可能不支持;即使底层文件系统可能支持访问时间,出于性能考虑通常会禁用该选项。
  • 时间戳的粒度是具体实现相关的细节。

对象存储对时间的概念更加模糊,可以概括为“视情况而定”。

  • 时间戳的粒度可能为1秒,这与HTTP HEAD和GET请求返回的时间戳粒度一致。
  • 访问时间可能未设置。即 FileStatus.getAccessTime() == 0
  • 新创建文件的修改时间戳可以是create()调用的时间,也可以是PUT请求发起时的实际时间。这可能发生在FileSystem.create()调用期间、最终的OutputStream.close()操作期间,或是两者之间的某个时间段。
  • 修改时间可能不会在close()调用中更新。
  • 时间戳可能采用UTC或对象存储的时区。如果客户端位于不同时区,对象的时间戳可能会领先或落后于客户端的时间戳。
  • 文件的修改时间通常与其创建时间相同。
  • FileSystem.setTimes() 操作设置文件时间戳可能会被忽略。
  • FileSystem.chmod() 可能会更新修改时间(例如:Azure wasb://)。
  • 如果支持FileSystem.append(),更改和修改时间很可能只有在输出流关闭后才会可见。
  • 对对象存储中数据的带外操作(即绕过Hadoop FileSystem API直接向对象存储发出的请求),可能导致存储和/或返回的时间戳不一致。
  • 由于目录结构的概念通常是模拟的,目录的时间戳可能是人工生成的——可能是使用当前系统时间。
  • 由于rename()操作通常实现为COPY + DELETE,重命名对象的时间戳可能会变成对象重命名开始的时间,而非源对象的时间戳。
  • 即使使用相同的对象存储客户端,不同对象存储安装之间的精确时间戳行为也可能有所不同。

最后请注意,Apache Hadoop项目无法保证远程对象存储的时间戳行为是否会随时间保持一致:它们是第三方服务,通常通过第三方库访问。

这里的最佳策略是“针对您计划使用的具体端点进行实验”。此外,如果您打算使用任何缓存/一致性层,请启用该功能进行测试。在Hadoop版本更新和端点对象存储更新后,需要重新测试。