简介

本文件为 Hadoop 文件系统的实现者和维护者以及 Hadoop FileSystem API 的用户定义了 Hadoop 兼容文件系统所需的行为

在发布之前,大多数 Hadoop 操作都会在 Hadoop 测试套件中针对 HDFS 进行测试,最初通过 MiniDFSCluster 进行测试,然后通过供应商特定的“生产”测试进行测试,并由其上的 Hadoop 堆栈隐式进行测试。

HDFS 的操作以 POSIX 文件系统行为为模型,使用 Unix 文件系统操作的操作和返回代码作为参考。即便如此,HDFS 仍有与 POSIX 文件系统的预期行为不同的部分。

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

“本地”文件系统提供对平台底层文件系统的访问。它的行为由操作系统定义,并且可能与 HDFS 不同。本地文件系统怪癖的示例包括区分大小写、在尝试将文件重命名为另一个文件上方的文件时执行的操作,以及是否可以seek()超过文件结尾。

还有由第三方实现的文件系统,它们声称与 Apache Hadoop 兼容。没有正式的兼容性套件,因此除了他们自己的兼容性测试之外,没有人可以声明兼容性。

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

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

通过使契约测试的每个方面可配置,可以声明文件系统如何偏离标准契约的部分。这是可以传达给文件系统用户的信息。

命名

本文件遵循 RFC 2119 规则,关于使用 MUST、MUST NOT、MAY 和 SHALL。MUST NOT 被视为规范性。

Hadoop FileSystem API 的隐含假设

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

无论 API 如何,都希望所有兼容 Hadoop 的文件系统都展示在 Unix 中实现的文件系统的模型

  • 它是一个带有文件和目录的分层目录结构。

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

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

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

  • 目录项本身没有数据。

  • 可以将任意二进制数据写入文件。当从集群内或集群外任何地方读取文件内容时,将返回数据。

  • 可以在单个文件中存储多个千兆字节的数据。

  • 根目录"/"始终存在,并且不能重命名。

  • 根目录"/"始终是目录,并且不能被文件写操作覆盖。

  • 任何尝试递归删除根目录都将删除其内容(除非没有权限),但不会删除根路径本身。

  • 不能将目录重命名/移动到其自身之下。

  • 不能将目录重命名/移动到除源文件本身之外的任何现有文件上。

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

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

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

路径名称

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

  • 路径元素是 1 个或多个字符的 unicode 字符串。

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

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

  • 路径元素不得为 "."".."

  • 另请注意,Azure Blob 存储文档指出,路径不应使用尾随 "."(因为其 .NET URI 类会将其删除)。

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

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

安全假设

除了有关安全性的特殊部分外,本文档假设客户端具有对文件系统的完全访问权限。因此,列表中的大多数项目都不会添加“假设用户有权使用提供的参数和路径执行操作”的限定条件。

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

网络假设

本文档假设所有网络操作都会成功。可以假设所有语句都有资格作为“假设操作不会因网络可用性问题而失败”

  • 网络故障后文件系统的最终状态未定义。

  • 网络故障后文件系统的即时一致性状态未定义。

  • 如果可以向客户端报告网络故障,则故障必须是 IOException 或其子类的实例。

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

  • 异常详细信息可能包括适合缺乏经验的开发人员开始诊断的诊断信息。例如,当 TCP 连接请求被拒绝时,Hadoop 尝试包含对 ConnectionRefused 的引用。

与 Hadoop 兼容的文件系统的核心期望

以下是与 Hadoop 兼容的文件系统的核心期望。有些文件系统不满足所有这些期望;因此,某些程序可能无法按预期工作。

原子性

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

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

大多数其他操作不附带任何原子性要求或保证。

一致性

Hadoop FileSystem 的一致性模型是单副本更新语义;与传统本地 POSIX 文件系统一致。请注意,即使 NFS 也放松了一些有关更改传播速度的约束。

  • 创建。一旦写入新创建文件的输出流上的 close() 操作完成,查询文件元数据和内容的群集内操作必须立即看到该文件及其数据。

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

  • 删除。一旦对除“/”之外的路径执行 delete() 操作成功完成,则该路径不应可见或可访问。具体而言,listStatus()open()rename()append() 操作必须失败。

  • 删除然后创建。删除文件后创建同名新文件时,新文件必须立即可见,并且其内容可通过 FileSystem API 访问。

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

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

并发性

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

操作和故障

  • 所有操作最终都必须成功或不成功地完成。

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

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

  • 操作应将所有网络、远程和高级问题作为 IOException 或其子类引发,而不应针对此类问题引发 RuntimeException

  • 操作应通过引发异常来报告故障,而不是通过操作的特定返回代码。

  • 在文本中,当命名异常类(如 IOException)时,引发的异常可能是命名异常的实例或子类。它不能是超类。

  • 如果操作未在类中实现,则实现必须抛出 UnsupportedOperationException

  • 实现可以重试失败的操作,直到成功。如果这样做,它们应以一种方式进行,使任何操作序列之间的“先发生”关系满足所述的一致性和原子性要求。请参阅 HDFS-4849 以获取此示例:HDFS 未实现任何其他调用者可以观察到的重试功能。

未定义的容量限制

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

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

  2. 目录中的最大目录数

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

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

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

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

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

未定义的超时

操作的超时根本没有定义,包括

  • 阻塞 FS 操作的最大完成时间。MAPREDUCE-972 记录了 distcp 在 s3 重命名速度较慢时如何中断。

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

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

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

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

  1. seek() 操作很快,几乎不会产生网络延迟。[这在 blob store 中不成立]

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

  3. 对于条目较少的目录,目录列表操作很快,但可能会产生 O(entries) 的成本。Hadoop 2 添加了迭代列表,以应对列出包含数百万个条目的目录的挑战,而无需以一致性为代价进行缓冲。

  4. OutputStreamclose() 很快,无论文件操作是否成功。

  5. 删除目录的时间与子项数量的大小无关

对象存储与文件系统

此规范在某些地方引用了“对象存储”,通常使用术语“Blobstore”。Hadoop 为其中一些提供了 FileSystem 客户端类,即使它们违反了许多要求。

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

什么是对象存储?

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

对象按名称存储:一个字符串,可能包含“/”符号。没有目录的概念;可以在对象中分配任意名称——在服务提供商施加的命名方案的限制内。

对象存储始终提供一个操作来检索具有给定前缀的对象;使用适当的查询参数对服务的根目录执行 GET 操作。

对象存储通常优先考虑可用性——没有等效于 HDFS NameNode(s) 的单点故障。它们还努力实现简单的非 POSIX API:HTTP 动词是允许的操作。

对象存储的 Hadoop FileSystem 客户端尝试使存储假装它们是一个 FileSystem,一个具有与 HDFS 相同特性和操作的 FileSystem。这——最终——是一种假装:它们具有不同的特性,有时这种错觉会失败。

  1. 一致性。对象可能为最终一致:对象更改(创建、删除和更新)可能需要时间才能对所有调用者可见。事实上,无法保证更改对刚刚进行更改的客户端立即可见。例如,对象 test/data1.csv 可能被新数据集覆盖,但在更新后不久进行 GET test/data1.csv 调用时,返回的却是原始数据。Hadoop 假设文件系统是一致的;创建、更新和删除操作会立即可见,而且列出目录的结果与该目录中的文件是当前的。

  2. 原子性。Hadoop 假设目录 rename() 操作是原子的,delete() 操作也是如此。对象存储文件系统客户端将这些操作实现为对名称与目录前缀匹配的各个对象的单独操作。因此,这些更改一次发生在一个文件上,而且不是原子的。如果操作在进程中中途失败,则对象存储的状态反映部分完成的操作。还要注意,客户端代码假设这些操作是 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 版本更新和端点对象存储更新后重新测试。