org.apache.hadoop.fs.FileSystem

抽象类FileSystem是访问Hadoop文件系统的原始类;针对所有Hadoop支持的文件系统都存在非抽象子类。

所有接受路径参数的操作都必须支持相对路径。在这种情况下,它们必须相对于通过setWorkingDirectory()定义的工作目录进行解析。

因此,对于所有客户端,我们还添加了一个状态组件PWD:这表示客户端的当前工作目录。对此状态的更改不会反映在文件系统本身中:它们是客户端实例独有的。

实现说明:静态方法FileSystem get(URI uri, Configuration conf)可能会返回一个已存在的文件系统客户端类实例——这个类可能同时被其他线程使用。Apache Hadoop自带的FileSystem实现没有对工作目录字段的访问进行任何同步处理

不变式

有效FileSystem的所有要求被视为隐含的前置条件和后置条件:对有效FileSystem的所有操作必须产生一个同样有效的新FileSystem。

可行功能

受保护的目录

HDFS 具有 受保护目录 的概念,这些目录在选项 fs.protected.directories 中声明。任何尝试删除或重命名此类目录或其父目录的操作都会引发 AccessControlException。因此,如果存在受保护目录,任何尝试删除根目录的操作都将导致抛出此类异常。

谓词和其他状态访问操作

boolean exists(Path p)

def exists(FS, p) = p in paths(FS)

boolean isDirectory(Path p)

def isDir(FS, p) = p in directories(FS)

boolean isFile(Path p)

def isFile(FS, p) = p in filenames(FS)

FileStatus getFileStatus(Path p)

获取路径的状态

前提条件

if not exists(FS, p) : raise FileNotFoundException

后置条件

result = stat: FileStatus where:
    if isFile(FS, p) :
        stat.length = len(FS.Files[p])
        stat.isdir = False
        stat.blockSize > 0
    elif isDir(FS, p) :
        stat.length = 0
        stat.isdir = True
    elif isSymlink(FS, p) :
        stat.length = 0
        stat.isdir = False
        stat.symlink = FS.Symlinks[p]
    stat.hasAcl = hasACL(FS, p)
    stat.isEncrypted = inEncryptionZone(FS, p)
    stat.isErasureCoded = isErasureCoded(FS, p)

返回路径的FileStatus状态还包含ACL、加密和擦除编码的详细信息。可以通过查询getFileStatus(Path p).hasAcl()来判断路径是否具有ACL。通过查询getFileStatus(Path p).isEncrypted()可以确定路径是否已加密。getFileStatus(Path p).isErasureCoded()将告知路径是否采用了擦除编码。

YARN的分布式缓存允许应用程序通过Job.addCacheFile()Job.addCacheArchive()添加需要在容器和应用程序之间缓存的路径。该缓存将添加的全局可读资源路径视为可在应用程序间共享,并以不同方式下载它们,除非这些路径被声明为加密。

为避免容器启动期间出现故障,尤其是在使用委托令牌时,未对文件和目录实现POSIX访问权限的文件系统和对象存储,必须始终对isEncrypted()谓词返回true。这可以通过在创建FileStatus实例时将encrypted标志设置为true来实现。

msync()

将客户端的元数据状态与文件系统元数据服务的最新状态同步。

在高可用性文件系统中,备用服务可用作只读元数据副本。此调用对于确保从备用副本读取的一致性以及避免读取过时数据至关重要。

目前仅针对HDFS实现,其他情况会抛出UnsupportedOperationException

前提条件

后置条件

此调用在内部记录调用时元数据服务的状态。这保证了后续从任何元数据副本读取的一致性。它确保客户端永远不会访问记录状态之前的元数据状态。

HDFS 实现说明

HDFS在HA模式下通过调用Active NameNode并请求其最新的日志事务ID来支持msync()功能。更多详情请参阅HDFS文档Consistent Reads from HDFS Observer NameNode

Path getHomeDirectory()

函数 getHomeDirectory 返回当前用户账户在FileSystem中的主目录。

对于某些文件系统,路径是["/", "users", System.getProperty("user-name")]

然而,对于HDFS来说,用户名来源于客户端用于与HDFS进行身份验证的凭据。这可能与本地用户账户名不同。

FileSystem 负责确定调用者的实际主目录。

前提条件

后置条件

result = p where valid-path(FS, p)

该方法被调用时,路径不需要事先存在;即使存在,也不要求它指向一个目录。但代码通常会假设not isFile(FS, getHomeDirectory())成立,否则后续代码可能会执行失败。

实现说明

  • FTPFileSystem会从远程文件系统查询该值,如果存在连接问题,可能会抛出RuntimeException或其子类异常。该操作的执行时间不受限制。

FileStatus[] listStatus(Path path, PathFilter filter)

列出路径 path 下的条目。

如果path指向一个文件且过滤器接受了该文件,则该文件的FileStatus条目将以单元素数组形式返回。

如果路径指向一个目录,该调用将返回其所有直接子路径的列表,这些子路径被过滤器接受——但不包括目录本身。

一个PathFilter filter是一个类,当且仅当路径path满足过滤条件时,其accept(path)方法返回true。

前提条件

路径 path 必须存在:

if not exists(FS, path) : raise FileNotFoundException

后置条件

if isFile(FS, path) and filter.accept(path) :
  result = [ getFileStatus(path) ]

elif isFile(FS, path) and not filter.accept(P) :
  result = []

elif isDir(FS, path):
  result = [
    getFileStatus(c) for c in children(FS, path) if filter.accepts(c)
  ]

隐式不变量:通过listStatus()获取的子项FileStatus内容,与对同一路径调用getFileStatus()获取的内容相同:

forall fs in listStatus(path) :
  fs == getFileStatus(fs.path)

结果排序:无法保证列出条目的顺序。虽然HDFS目前返回按字母数字排序的列表,但无论是Posix的readdir()还是Java的File.listFiles() API调用都没有定义返回值的任何排序方式。需要对结果进行统一排序的应用程序必须自行执行排序。

空返回: 3.0.0版本之前的本地文件系统在访问错误时会返回null。这被视为错误行为。预期在访问错误时会抛出IOException。

原子性与一致性

listStatus()操作返回给调用者时,不能保证响应中包含的信息是最新的。这些细节可能已经过时,包括任何目录的内容、任何文件的属性以及所提供路径的存在性。

目录的状态在评估过程中可能会发生变化。

  • 在路径 P 处创建条目后,在对文件系统进行任何其他更改之前,listStatus(P) 必须能够找到该文件并返回其状态。

  • 在路径 P 上的条目被删除后,且在对文件系统进行任何其他更改之前,listStatus(P) 必须抛出 FileNotFoundException

  • 在路径P处创建条目后,在对文件系统进行任何其他更改之前,listStatus(parent(P))的结果应该包含getFileStatus(P)的值。

  • 在路径P的条目被删除后,在对文件系统进行任何其他更改之前,listStatus(parent(P))的结果不应包含getFileStatus(P)的值。

这不是理论上的可能性,当HDFS中的一个目录包含数千个文件时,这种情况是可以观察到的。

考虑一个目录 "/d" 包含以下内容:

a
part-0000001
part-0000002
...
part-9999999

如果文件数量较多,导致HDFS在每次响应中返回部分列表,那么当列表操作listStatus("/d")与重命名操作rename("/d/a","/d/z"))同时发生时,结果可能是以下情况之一:

[a, part-0000001, ... , part-9999999]
[part-0000001, ... , part-9999999, z]
[a, part-0000001, ... , part-9999999, z]
[part-0000001, ... , part-9999999]

虽然这种情况可能很少发生,但它确实可能出现。在HDFS中,这种不一致的视图通常只会在列出包含大量子项的目录时发生。

其他文件系统可能具有更强的一致性保证,或者更容易返回不一致的数据。

FileStatus[] listStatus(Path path)

这完全等同于 listStatus(Path, DEFAULT_FILTER),其中 DEFAULT_FILTER.accept(path) = True 对所有路径都成立。

原子性和一致性约束与listStatus(Path, DEFAULT_FILTER)相同。

FileStatus[] listStatus(Path[] paths, PathFilter filter)

枚举传入目录列表中的所有文件,对每个目录调用listStatus(path, filter)方法。

listStatus(path, filter)类似,结果可能不一致。也就是说:文件系统的状态在操作过程中发生了变化。

无法保证路径是否按特定顺序列出,唯一能确定的是所有路径都会被列出,并且在列出时存在。

前提条件

所有路径必须存在。不要求唯一性。

forall p in paths :
  exists(FS, p) else raise FileNotFoundException

后置条件

结果是一个数组,其条目包含路径列表中找到的所有状态元素,不包含其他内容。

result = [listStatus(p, filter) for p in paths]

实现方案可以合并重复条目;和/或通过识别重复路径并仅列出一次条目来优化操作。

默认实现会遍历列表;它不执行任何优化。

原子性和一致性约束与listStatus(Path, PathFilter)相同。

RemoteIterator listStatusIterator(Path p)

返回一个枚举路径下FileStatus条目的迭代器。这与listStatus(Path)类似,不同之处在于它返回的是迭代器而非完整列表。只要在列举期间没有其他调用者更新目录,结果就与listStatus(Path)完全相同。需要注意的是,如果在执行列举操作时有其他调用者在目录中添加/删除文件,这并不能保证操作的原子性。不同文件系统可能提供更高效的实现,例如S3A会分页列举,并在处理当前页时异步获取下一页。

请注意,由于初始列表是异步的,桶/路径不存在的异常可能会在后续的next()调用中出现。

调用者应优先使用listStatusIterator而非listStatus,因为前者本质上是增量式的。

FileStatus[] listStatus(Path[] paths)

枚举传入目录列表中的所有文件,对每个目录调用listStatus(path, DEFAULT_FILTER),其中DEFAULT_FILTER会接受所有路径名称。

RemoteIterator[LocatedFileStatus] listLocatedStatus(Path path, PathFilter filter)

返回一个迭代器,用于枚举路径下的LocatedFileStatus条目。这与listStatus(Path)类似,不同之处在于返回值是FileStatus的子类LocatedFileStatus的实例,并且返回的是迭代器而非整个列表。

这实际上是一个protected方法,由listLocatedStatus(Path path)直接调用。对它的调用可能会通过分层文件系统(如FilterFileSystem)进行委托,因此即使listLocatedStatus(Path path)以不同方式实现,也必须将其实现视为强制性的。目前有开放的JIRA提议将此方法公开;未来可能会实现。

迭代器不需要提供路径子条目的一致视图。默认实现确实使用listStatus(Path)来列出其子项,其一致性约束已有文档说明。其他实现可能会更动态地执行枚举。例如获取子条目的窗口子集,从而避免构建大型数据结构和传输大消息。在这种情况下,文件系统的更改更有可能变得可见。

调用者必须假设,如果在本次调用返回与迭代完全执行之间文件系统发生变化,迭代操作可能会失败。

前提条件

路径 path 必须存在:

if not exists(FS, path) : raise FileNotFoundException

后置条件

该操作会生成一组结果集resultset,等同于listStatus(path, filter)的执行结果:

if isFile(FS, path) and filter.accept(path) :
  resultset =  [ getLocatedFileStatus(FS, path) ]

elif isFile(FS, path) and not filter.accept(path) :
  resultset = []

elif isDir(FS, path) :
  resultset = [
    getLocatedFileStatus(FS, c)
     for c in children(FS, path) where filter.accept(c)
  ]

操作 getLocatedFileStatus(FS, path: Path): LocatedFileStatus 被定义为一个生成 LocatedFileStatus 实例 ls 的生成器,其中:

fileStatus = getFileStatus(FS, path)

bl = getFileBlockLocations(FS, path, 0, fileStatus.len)

locatedFileStatus = new LocatedFileStatus(fileStatus, bl)

迭代器中返回的resultset元素的顺序是未定义的。

原子性和一致性约束与listStatus(Path, PathFilter)相同。

RemoteIterator[LocatedFileStatus] listLocatedStatus(Path path)

等同于 listLocatedStatus(path, DEFAULT_FILTER),其中 DEFAULT_FILTER 接受所有路径名称。

RemoteIterator[LocatedFileStatus] listFiles(Path path, boolean recursive)

创建一个遍历目录中/下所有文件的迭代器,可能会递归进入子目录。

该操作的目的是通过减少单次RPC调用中必须收集的数据量,使文件系统能更高效地处理大型递归目录扫描。

前提条件

if not exists(FS, path) : raise FileNotFoundException

后置条件

结果是一个迭代器,通过一系列iterator.next()调用产生的输出可以定义为集合iteratorset

if not recursive:
  iteratorset == listStatus(path)
else:
  iteratorset = [
    getLocatedFileStatus(FS, d)
      for d in descendants(FS, path)
  ]

函数 getLocatedFileStatus(FS, d) 的定义与 listLocatedStatus(Path, PathFilter) 中相同。

原子性和一致性约束与listStatus(Path, PathFilter)相同。

ContentSummary getContentSummary(Path path)

给定路径返回其内容摘要。

getContentSummary() 首先检查给定路径是否为文件,如果是,则返回目录计数0和文件计数1。

前提条件

if not exists(FS, path) : raise FileNotFoundException

后置条件

返回一个ContentSummary对象,其中包含给定路径的目录计数和文件计数等信息。

原子性和一致性约束与listStatus(Path, PathFilter)相同。

BlockLocation[] getFileBlockLocations(FileStatus f, int s, int l)

前提条件

if s < 0 or l < 0 : raise {HadoopIllegalArgumentException, InvalidArgumentException}
  • HDFS 会针对无效的偏移量或长度抛出 HadoopIllegalArgumentException;该异常继承自 IllegalArgumentException

后置条件

如果文件系统支持位置感知,它必须返回可以找到[s:s+l]范围内数据的块位置列表。

if f == null :
    result = null
elif f.getLen() <= s:
    result = []
else result = [ locations(FS, b) for b in blocks(FS, p, s, s+l)]

位置

  def locations(FS, b) = a list of all locations of a block in the filesystem

  def blocks(FS, p, s, s +  l)  = a list of the blocks containing data(FS, path)[s:s+l]

请注意,由于当isDir(FS, f)length(FS, f)被定义为0,因此在目录上调用getFileBlockLocations()的结果为[]

如果文件系统不具备位置感知能力,它应该返回

  [
    BlockLocation(["localhost:9866"] ,
              ["localhost"],
              ["/default/localhost"]
               0, f.getLen())
   ] ;

*Hadoop 1.0.3版本中存在一个缺陷,这意味着必须提供与集群拓扑元素数量相同的拓扑路径,因此文件系统应返回"/default/localhost"路径。虽然这不再是问题,但该约定通常仍被保留。

BlockLocation[] getFileBlockLocations(Path P, int S, int L)

前提条件

if p == null : raise NullPointerException
if not exists(FS, p) : raise FileNotFoundException

后置条件

result = getFileBlockLocations(getFileStatus(FS, P), S, L)

long getDefaultBlockSize()

获取文件系统的“默认”块大小。这通常用于在拆分计算期间将工作最优地分配到一组工作进程中。

前提条件

后置条件

result = integer > 0

尽管此结果没有定义最小值,但由于它用于在作业提交期间划分工作,块大小过小将导致工作负载划分不佳,甚至导致JobSubmissionClient及其等效组件在计算分区时耗尽内存。

任何实际上不会将文件分割成块的FileSystem都应返回一个能实现高效处理的数值。FileSystem可以允许用户对此进行配置(对象存储连接器通常支持此功能)。

long getDefaultBlockSize(Path p)

获取路径的“默认”块大小——即在文件系统中向该路径写入对象时将使用的块大小。

前提条件

后置条件

result = integer >= 0

此操作的结果通常与getDefaultBlockSize()相同,不会检查给定路径是否存在。

支持挂载点的文件系统可能对不同路径有不同的默认值,在这种情况下,应返回目标路径的特定默认值。

如果路径不存在,这不是错误:必须返回文件系统该部分的默认/推荐值。

long getBlockSize(Path p)

此方法与查询getFileStatus(p)返回的FileStatus结构中的块大小完全等效。该方法已被弃用,以鼓励用户调用一次getFileStatus(p),然后利用返回结果检查文件的多个属性(例如长度、类型、块大小)。如果查询多个属性,这将显著优化性能——并减轻文件系统的负载。

前提条件

if not exists(FS, p) : raise FileNotFoundException

后置条件

if len(FS, P) > 0 :  getFileStatus(P).getBlockSize() > 0
result == getFileStatus(P).getBlockSize()
  1. 此操作的结果必须与getFileStatus(P).getBlockSize()的值相同。
  2. 根据推断,对于任何长度大于0的文件,该值必须大于0。

Path getEnclosingRoot(Path p)

该方法用于查找给定路径的根目录。这在创建位于同一父根目录下的暂存和临时目录时非常有用。关于如何允许原子性重命名操作(例如跨HDFS卷或跨加密区域)存在一些限制。

对于任何两个不具有相同根目录的路径p1和p2,rename(p1, p2)操作预计会失败或无法保证原子性。

对于对象存储而言,即使在同一根目录下,也无法保证文件或目录重命名操作是原子性的

以下陈述始终为真:getEnclosingRoot(p) == getEnclosingRoot(getEnclosingRoot(p))

path in ancestors(FS, p) or path == p:
isDir(FS, p)

前提条件

路径不必存在,但路径必须有效且能被文件系统协调处理 * 如果使用了链接回退,则所有路径都可协调 * 如果未使用链接回退,则必须有一个挂载点覆盖该路径

后置条件

  • 返回的路径不会为空,如果没有更深层的根路径,将返回根路径(“/”)。
  • 返回的路径是一个目录

状态变更操作

boolean mkdirs(Path p, FsPermission permission)

创建一个目录及其所有父目录。

前提条件

路径必须是一个目录或不存在

 if exists(FS, p) and not isDir(FS, p) :
     raise [ParentNotDirectoryException, FileAlreadyExistsException, IOException]

上级目录不能是文件

forall d = ancestors(FS, p) : 
    if exists(FS, d) and not isDir(FS, d) :
        raise {ParentNotDirectoryException, FileAlreadyExistsException, IOException}

后置条件

FS' where FS'.Directories = FS.Directories + [p] + ancestors(FS, p)
result = True

文件系统目录、文件和符号链接的条件排他性要求必须得到满足。

检查路径是否存在及其类型以及目录创建操作必须是原子性的。包括mkdirs(parent(F))在内的组合操作可以是原子性的。

返回值始终为true——即使未创建新目录(这在HDFS中定义)。

FSDataOutputStream create(Path, ...)

FSDataOutputStream create(Path p,
      FsPermission permission,
      boolean overwrite,
      int bufferSize,
      short replication,
      long blockSize,
      Progressable progress) throws IOException;

前提条件

对于不覆盖创建操作,文件必须不存在:

if not overwrite and isFile(FS, p) : raise FileAlreadyExistsException

写入或覆盖目录的操作必须失败。

if isDir(FS, p) : raise {FileAlreadyExistsException, FileNotFoundException, IOException}

上级目录不能是文件

forall d = ancestors(FS, p) : 
    if exists(FS, d) and not isDir(FS, d) :
        raise {ParentNotDirectoryException, FileAlreadyExistsException, IOException}

文件系统可能因其他原因拒绝请求,例如文件系统处于只读状态(HDFS)、块大小低于允许的最小值(HDFS)、副本数超出范围(HDFS)、命名空间或文件系统配额超出限制、保留名称等。所有拒绝都应抛出IOException或其子类,也可能是RuntimeException或其子类。例如,HDFS可能抛出InvalidPathException

后置条件

FS' where :
   FS'.Files[p] == []
   ancestors(p) subset-of FS'.Directories

result = FSDataOutputStream

必须在指定路径的末尾存在一个零字节文件,且对所有用户可见。

更新后的(有效)FileSystem必须包含路径的所有父目录,这些目录由mkdirs(parent(p))创建。

结果是FSDataOutputStream,通过其操作可能会生成新的文件系统状态,其中FS.Files[p]的值会被更新

返回流的行为在Output中有详细说明。

实现说明

  • 某些实现将创建操作拆分为检查文件是否存在和实际创建两个步骤。这意味着该操作不是原子性的:当客户端尝试以overwrite==true参数创建文件时,如果在两次检查之间有其他客户端创建了该文件,则可能导致操作失败。

  • S3A以及其他可能的对象存储连接器目前不会更改FS状态,直到输出流close()操作完成。这是对象存储与文件系统行为之间的显著差异,因为它允许多个客户端使用overwrite=false创建文件,并可能导致文件/目录逻辑混乱。特别是,当使用对象存储时,使用create()获取文件的独占锁(任何无错误创建文件的人被视为锁的持有者)可能不是一个安全的算法。

  • 对象存储可能在创建文件时生成一个空文件作为标记。然而,具有overwrite=true语义的对象存储可能无法原子性地实现此操作,因此使用overwrite=false创建文件不能作为进程间的隐式互斥机制。

  • 当尝试在目录上创建文件时,Local FileSystem会抛出FileNotFoundException异常,因此它被列为当此前提条件失败时可能抛出的异常之一。

  • 不包括:符号链接。符号链接的解析路径将作为最终路径参数传递给 create() 操作

FSDataOutputStreamBuilder createFile(Path p)

创建一个FSDataOutputStreamBuilder来指定创建文件的参数。

返回流的行为在Output中有详细说明。

实现说明

createFile(p) 仅返回一个 FSDataOutputStreamBuilder 而不会立即对文件系统进行更改。当在 FSDataOutputStreamBuilder 上调用 build() 时,会验证构建器参数并在底层文件系统上调用 create(Path p)build() 具有与 create(Path p) 相同的前置条件和后置条件。

  • 类似于create(Path p),默认情况下文件会被覆盖,除非通过builder.overwrite(false)进行指定。
  • create(Path p)不同,默认情况下不会自动创建缺失的父目录,除非通过builder.recursive()指定。

FSDataOutputStream append(Path p, int bufferSize, Progressable progress)

没有合规调用的实现应该抛出UnsupportedOperationException

前提条件

if not exists(FS, p) : raise FileNotFoundException

if not isFile(FS, p) : raise {FileAlreadyExistsException, FileNotFoundException, IOException}

后置条件

FS' = FS
result = FSDataOutputStream

返回:FSDataOutputStream,可以通过向现有列表追加数据来更新条目FS'.Files[p]

返回流的行为在Output中有详细说明。

FSDataOutputStreamBuilder appendFile(Path p)

创建一个FSDataOutputStreamBuilder来指定要追加到现有文件的参数。

返回流的行为在Output中有详细说明。

实现说明

appendFile(p) 仅返回一个 FSDataOutputStreamBuilder 而不会立即对文件系统进行更改。当在 FSDataOutputStreamBuilder 上调用 build() 时,会验证构建器参数并在底层文件系统上调用 append()build() 具有与 append() 相同的前置条件和后置条件。

FSDataInputStream open(Path f, int bufferSize)

没有合规调用的实现应该抛出UnsupportedOperationException

前提条件

if not isFile(FS, p)) : raise {FileNotFoundException, IOException}

这是一个关键前提条件。某些文件系统(例如对象存储)的实现可能会通过推迟其HTTP GET操作直到返回的FSDataInputStream上首次调用read()来减少一次往返。然而,许多客户端代码确实依赖于在open()操作时执行存在性检查。实现必须在创建时检查文件是否存在。这并不意味着文件及其数据在后续read()或任何后续操作时仍然存在。

后置条件

result = FSDataInputStream(0, FS.Files[p])

该结果提供了对由FS.Files[p]定义的字节数组的访问;这种访问是针对调用open()操作时的内容,还是能够以及如何获取FS后续状态中对该数据的更改,这是一个实现细节。

该操作对于本地和远程调用者的结果必须相同。

HDFS 实现说明

  1. HDFS 在尝试遍历符号链接时可能会抛出 UnresolvedPathException

  2. 如果路径存在于元数据中,但无法定位其数据块的任何副本,HDFS会抛出IOException("Cannot open filename " + src);-FileNotFoundException似乎更准确且有用。

FSDataInputStreamBuilder openFile(Path path)

参见 openFile()

FSDataInputStreamBuilder openFile(PathHandle)

参见 openFile()

PathHandle getPathHandle(FileStatus stat, HandleOpt... options)

没有合规调用的实现必须抛出UnsupportedOperationException

前提条件

let stat = getFileStatus(Path p)
let FS' where:
  (FS'.Directories, FS.Files', FS'.Symlinks)
  p' in paths(FS') where:
    exists(FS, stat.path) implies exists(FS', p')

在解析时,FileStatus实例的引用对象与getPathHandle(FileStatus)的结果具有相同的引用。PathHandle可用于后续操作,以确保调用之间保持不变量。

options 参数指定后续调用(例如 open(PathHandle))是否会在引用数据或位置发生更改时成功。默认情况下,任何修改都会导致错误。调用者可以指定放宽条件,即使引用对象存在于不同路径和/或其数据已更改,也允许操作成功。

如果实现无法支持调用者指定的语义,则必须抛出UnsupportedOperationException。默认选项集如下所示。

未移动 已移动
未更改 精确匹配 内容
已更改 路径 参考

所有权、扩展属性和其他元数据的变更不需要与PathHandle匹配。实现可以通过自定义约束扩展HandleOpt参数集。

示例

客户端指定PathHandle应使用REFERENCE跟踪跨重命名的实体。除非无法解析引用意味着该实体不再存在,否则实现必须在创建PathHandle时抛出UnsupportedOperationException

客户端指定PathHandle仅当实体未更改时才应解析,使用PATH。除非实现能够区分随后位于相同路径的相同实体,否则在创建PathHandle时必须抛出UnsupportedOperationException

后置条件

result = PathHandle(p')

实现说明

PathHandle的引用对象是创建FileStatus实例时的命名空间,而非创建PathHandle时的状态。实现方案可以拒绝那些有效但服务成本过高的PathHandle实例创建或解析请求。

通过复制对象实现重命名的对象存储,除非解决了对象的血缘关系,否则不得声称支持CONTENTREFERENCE

必须能够序列化PathHandle实例,并在一个或多个进程中、另一台机器上、以及未来任意时间重新实例化它,而不改变其语义。如果实现无法再保证其不变量,则必须拒绝解析实例。

HDFS 实现说明

HDFS不支持通过PathHandle引用目录或符号链接。对CONTENTREFERENCE的支持是通过INode查找文件。由于INode在多个NameNode之间不具备唯一性,因此联邦集群应在PathHandle中包含足够的元数据,以检测来自其他命名空间的引用。

FSDataInputStream open(PathHandle handle, int bufferSize)

没有合规调用的实现必须抛出UnsupportedOperationException

前提条件

let fd = getPathHandle(FileStatus stat)
if stat.isdir : raise IOException
let FS' where:
  (FS'.Directories, FS.Files', FS'.Symlinks)
  p' in FS'.Files where:
    FS'.Files[p'] = fd
if not exists(FS', p') : raise InvalidPathHandleException

实现必须根据getPathHandle(FileStatus)创建时指定的约束条件来解析PathHandle的引用对象。

为满足此合约,FileSystem所需的元数据可被编码在PathHandle中。

后置条件

result = FSDataInputStream(0, FS'.Files[p'])

返回的流需遵守open(Path)方法返回流的约束条件。在打开时验证的约束条件可能适用于该流,但这并非绝对保证。

例如,使用CONTENT约束创建的PathHandle可能会返回一个忽略文件打开后更新的流,前提是在解析open(PathHandle)时该文件未被修改。

实现说明

实现可以选择在服务器端或在将流返回给客户端之前检查不变量。例如,实现可以打开文件,然后使用getFileStatus(Path)验证PathHandle中的不变量来实现CONTENT。这可能导致误报,并且需要额外的RPC通信。

boolean delete(Path p, boolean recursive)

删除一个路径,无论是文件、符号链接还是目录。recursive标志表示是否应执行递归删除——如果未设置,则无法删除非空目录。

除了根目录的特殊情况外,如果此API调用成功完成,则路径末尾不存在任何内容。也就是说:达到了预期结果。返回标志仅告知调用者文件系统的状态是否发生了任何更改。

注意:许多使用此方法的情况都会检查返回值是否为false,如果是则抛出异常。例如

if (!fs.delete(path, true)) throw new IOException("Could not delete " + path);

此模式并非必需。代码应直接调用delete(path, recursive)并假定目标路径已不存在——根目录这一特殊情况除外(根目录将始终保留,下文将专门讨论根目录的特殊处理)。

前提条件

带有子目录且recursive == False的目录无法被删除

if isDir(FS, p) and not recursive and (children(FS, p) != {}) : raise IOException

(HDFS 在此处抛出 PathIsNotEmptyDirectoryException 异常。)

后置条件

不存在的路径

如果文件不存在,文件系统状态不会改变

if not exists(FS, p) :
    FS' = FS
    result = False

结果应为False,表示没有文件被删除。

简单文件

引用文件的路径被移除,返回值:True

if isFile(FS, p) :
    FS' = (FS.Directories, FS.Files - [p], FS.Symlinks)
    result = True
根目录为空,recursive == False

删除空根目录不会改变文件系统状态,可能返回true或false。

if isRoot(p) and children(FS, p) == {} :
    FS ' = FS
    result = (undetermined)

尝试删除根目录时没有一致的返回码。

实现应返回true;这样可以避免检查返回值为false的代码反应过度。

对象存储: 参见 Object Stores: root directory deletion.

空目录(非根目录)recursive == False

删除一个非根的空目录将从文件系统中移除该路径并返回true。

if isDir(FS, p) and not isRoot(p) and children(FS, p) == {} :
    FS' = (FS.Directories - [p], FS.Files, FS.Symlinks)
    result = True
递归删除非空根目录

删除一个包含子目录的根路径且recursive==True时,通常会产生三种结果:

  1. POSIX模型假设如果用户拥有删除所有内容的正确权限,他们可以自由执行此操作(这将导致文件系统为空)。

    if isDir(FS, p) and isRoot(p) and recursive :
        FS' = ({["/"]}, {}, {}, {})
        result = True
    
  2. HDFS 不允许删除文件系统的根目录;如果需要清空文件系统,必须先将文件系统下线并重新格式化。

    if isDir(FS, p) and isRoot(p) and recursive :
        FS' = FS
        result = False
    
  3. 对象存储:参见 Object Stores: root directory deletion

本规范并未推荐任何具体操作。但请注意,POSIX模型假设存在一个权限模型,使得普通用户无权删除根目录;该操作仅系统管理员应有权执行。

任何与缺乏此类安全模型的远程文件系统交互的文件系统客户端,可能会拒绝调用delete("/", true),理由是这会导致数据丢失过于容易。

对象存储:根目录删除

一些基于对象存储的文件系统实现在删除根目录时总是返回false,保持存储状态不变。

if isRoot(p) :
    FS' = FS
    result = False

这与递归标志状态或目录的状态无关。

这是一种简化处理方式,避免了不可避免地非原子性扫描和删除存储内容的问题。同时它也消除了关于该操作是否实际删除特定存储/容器本身的任何混淆,以及更简单存储权限模型可能带来的不利后果。

非根目录的递归删除

删除非根路径且包含子路径时,设置recursive==true将移除该路径及其所有后代

if isDir(FS, p) and not isRoot(p) and recursive :
    FS' where:
        not isDir(FS', p)
        and forall d in descendants(FS, p):
            not isDir(FS', d)
            not isFile(FS', d)
            not isSymlink(FS', d)
    result = True

原子性

  • 删除文件必须是一个原子操作。

  • 删除空目录必须是一个原子操作。

  • 目录树的递归删除必须是原子操作。

实现说明

  • 对象存储和其他模拟目录树的非传统文件系统,往往将delete()实现为递归列出和逐条删除操作。这会打破客户端应用对O(1)原子目录删除的预期,导致这些存储无法作为HDFS的直接替代品使用。

boolean rename(Path src, Path d)

就其规范而言,rename()是文件系统中最复杂的操作之一。

在实现方面,关于何时返回false与何时抛出异常,这是最具模糊性的情况。

重命名操作包含目标路径的计算。如果目标已存在且是一个目录,则重命名的最终目标路径将变为目标路径加上源路径的文件名。

let dest = if (isDir(FS, d) and d != src) :
        d + [filename(src)]
    else :
        d

前提条件

所有对目标路径的检查必须在最终dest路径计算完成后进行。

src 必须存在:

if not exists(FS, src) : raise FileNotFoundException

dest 不能是 src 的子目录:

if isDescendant(FS, src, dest) : raise IOException

这隐含涵盖了isRoot(FS, src)的特殊情况。

dest 必须是根目录,或者拥有一个已存在的父目录:

if not (isRoot(FS, dest) or exists(FS, parent(dest))) : raise IOException

目标路径的父级路径不能是文件:

if isFile(FS, parent(dest)) : raise IOException

这隐式涵盖了父级的所有祖先。

目标路径末尾不能存在已有文件:

if isFile(FS, dest) : raise FileAlreadyExistsException, IOException

后置条件

将目录重命名为自身

将目录重命名为自身是无操作;返回值未指定。

在POSIX系统中结果为False;在HDFS系统中结果为True

if isDir(FS, src) and src == dest :
    FS' = FS
    result = (undefined)
将文件重命名为自身

将文件重命名为自身是无操作;结果为True

 if isFile(FS, src) and src == dest :
     FS' = FS
     result = True
将文件重命名到不存在的路径

重命名文件时,若目标路径是一个目录,则该文件会被移动到目标目录下作为其子项,同时保留源路径中的文件名部分。

if isFile(FS, src) and src != dest:
    FS' where:
        not exists(FS', src)
        and exists(FS', dest)
        and data(FS', dest) == data (FS, source)
    result = True
将目录重命名为目录

如果src是一个目录,那么它的所有子项将存在于dest下,而路径src及其后代将不再存在。dest下的路径名称将与src下的名称一致,内容也将保持一致:

if isDir(FS, src) and isDir(FS, dest) and src != dest :
    FS' where:
        not exists(FS', src)
        and dest in FS'.Directories
        and forall c in descendants(FS, src) :
            not exists(FS', c))
        and forall c in descendants(FS, src) where isDir(FS, c):
            isDir(FS', dest + childElements(src, c)
        and forall c in descendants(FS, src) where not isDir(FS, c):
                data(FS', dest + childElements(s, c)) == data(FS, c)
    result = True
重命名到父路径不存在的路径中
  not exists(FS, parent(dest))

这里没有一致的行为。

HDFS

结果是对文件系统状态没有改变,返回值为false。

FS' = FS
result = False

本地文件系统

结果与普通的重命名操作相同,但额外具备一个(隐式)特性:目标路径的父目录也会被自动创建。

exists(FS', parent(dest))

S3A 文件系统

结果与普通的重命名操作相同,但额外具备一个(隐式)特性:目标路径的父目录会被自动创建:exists(FS', parent(dest))

会检查并拒绝parent(dest)是文件的情况,但不会检查其他祖先路径。

其他文件系统

其他文件系统会严格拒绝该操作,并抛出FileNotFoundException

并发需求
  • rename()的核心操作——将文件系统中的条目移动到另一个位置——必须是原子性的。某些应用程序依赖此操作作为协调数据访问的方式。

  • 某些FileSystem实现在重命名操作前后会对目标FileSystem执行检查。其中一个例子是ChecksumFileSystem,它为本地数据提供校验和访问。整个操作序列可能不具备原子性。

实现说明

用于读取、写入或追加的文件

在打开的文件上执行rename()的行为是未定义的:是否允许该操作,以及对后续尝试从打开的流中读取或写入的影响

将目录重命名为自身

将目录重命名为自身的返回代码未作规定。

目标路径已存在且是一个文件

在已有文件上重命名文件被指定为失败操作,会引发异常。

  • 本地文件系统:重命名操作成功;目标文件被源文件替换。

  • HDFS : 重命名操作失败,但不会抛出异常。该方法调用仅返回false。

缺少源文件

如果源文件 src 不存在,则应抛出 FileNotFoundException 异常。

HDFS 失败时不会抛出异常;rename() 仅返回 false。

FS' = FS
result = false

此处HDFS的行为不应被视为可复制的特性。FileContext明确更改了该行为以引发异常,而将该操作改造到DFSFileSystem实现中仍是一个持续讨论的问题。

void concat(Path p, Path sources[])

将多个数据块合并成一个单独的文件。这是一个目前仅由HDFS实现的较少使用的操作。

没有合规调用的实现应该抛出UnsupportedOperationException

前提条件

if not exists(FS, p) : raise FileNotFoundException

if sources==[] : raise IllegalArgumentException

所有源文件必须位于同一目录下:

for s in sources:
    if parent(s) != parent(p) : raise IllegalArgumentException

所有块大小必须与目标匹配:

for s in sources:
    getBlockSize(FS, s) == getBlockSize(FS, p)

无重复路径:

let input = sources + [p]
not (exists i, j: i != j and input[i] == input[j])

HDFS: 除最后一个文件外,所有源文件必须是一个完整的块:

for s in (sources[0:length(sources)-1] + [p]):
    (length(FS, s) mod getBlockSize(FS, p)) == 0

后置条件

FS' where:
    (data(FS', p) = data(FS, p) + data(FS, sources[0]) + ... + data(FS, sources[length(sources)-1]))
    for s in sources: not exists(FS', s)

HDFS的限制可能是其实现concat方式的实现细节,即通过更改inode引用将它们按顺序连接在一起。由于Hadoop核心代码库中没有其他文件系统实现此方法,因此无法区分实现细节与规范。

boolean truncate(Path p, long newLength)

将文件 p 截断至指定的 newLength 长度。

没有合规调用的实现应该抛出UnsupportedOperationException

前提条件

if not exists(FS, p) : raise FileNotFoundException

if isDir(FS, p) : raise {FileNotFoundException, IOException}

if newLength < 0 || newLength > len(FS.Files[p]) : raise HadoopIllegalArgumentException

HDFS: 源文件必须处于关闭状态。无法对正在写入或追加的文件执行截断操作。

后置条件

len(FS'.Files[p]) = newLength

返回: true,如果截断已完成且文件可以立即打开进行追加操作,否则返回false

HDFS: HDFS返回false表示已启动调整最后一个块长度的后台进程,客户端应等待该进程完成才能继续执行后续文件更新操作。

并发性

如果在执行truncate()时输入流处于打开状态,那么与被截断文件部分相关的读取操作结果将是未定义的。

boolean copyFromLocalFile(boolean delSrc, boolean overwrite, Path src, Path dst)

位于src的源文件或目录在本地磁盘上,会被复制到目标文件系统的dst位置。如果移动后需要删除源文件,则必须将delSrc标志设为TRUE。如果目标位置已存在且需要覆盖目标内容,则必须将overwrite标志设为TRUE。

前提条件

源和目标必须不同

if src = dest : raise FileExistsException

目标路径和源路径不能互为子目录

if isDescendant(src, dest) or isDescendant(dest, src) : raise IOException

源文件或目录必须在本地存在:

if not exists(LocalFS, src) : raise FileNotFoundException

无论覆盖标志设置为何值,目录都无法被复制到文件中:

if isDir(LocalFS, src) and isFile(FS, dst) : raise PathExistsException

除上述前提条件抛出异常的情况外,若目标路径已存在,则必须将覆盖标志设置为TRUE才能使操作成功。此操作还将覆盖目标路径下的所有文件/目录:

if exists(FS, dst) and not overwrite : raise PathExistsException

确定副本的最终名称

给定源路径上的基础路径 base 和子路径 child,其中 base 位于 ancestors(child) + child 中:

def final_name(base, child, dest):
    if base == child:
        return dest
    else:
        return dest + childElements(base, child)

当源为文件时的结果 isFile(LocalFS, src)

对于文件,目标位置的数据将与源数据相同。所有上级路径均为目录。

if isFile(LocalFS, src) and (not exists(FS, dest) or (exists(FS, dest) and overwrite)):
    FS' = FS where:
        FS'.Files[dest] = LocalFS.Files[src]
        FS'.Directories = FS.Directories + ancestors(FS, dest)
    LocalFS' = LocalFS where
        not delSrc or (delSrc = true and delete(LocalFS, src, false))
else if isFile(LocalFS, src) and isDir(FS, dest):
    FS' = FS where:
        let d = final_name(src, dest)
        FS'.Files[d] = LocalFS.Files[src]
    LocalFS' = LocalFS where:
        not delSrc or (delSrc = true and delete(LocalFS, src, false))

对于本地LocalFS和远程FS,都不保证文件变更具有原子性。

当源为目录时的结果 isDir(LocalFS, src)

if isDir(LocalFS, src) and (isFile(FS, dest) or isFile(FS, dest + childElements(src))):
    raise FileAlreadyExistsException
else if isDir(LocalFS, src):
    if exists(FS, dest):
        dest' = dest + childElements(src)
        if exists(FS, dest') and not overwrite:
            raise PathExistsException
    else:
        dest' = dest

    FS' = FS where:
        forall c in descendants(LocalFS, src):
            not exists(FS', final_name(c)) or overwrite
        and forall c in descendants(LocalFS, src) where isDir(LocalFS, c):
            FS'.Directories = FS'.Directories + (dest' + childElements(src, c))
        and forall c in descendants(LocalFS, src) where isFile(LocalFS, c):
            FS'.Files[final_name(c, dest')] = LocalFS.Files[c]
    LocalFS' = LocalFS where
        not delSrc or (delSrc = true and delete(LocalFS, src, true))

不保证操作的隔离性/原子性。这意味着在操作执行期间,源文件或目标文件可能会发生变化。除了尽最大努力外,无法保证复制后文件或目录的最终状态。例如:在复制目录时,一个文件可能已从源位置移动到目标位置,但在复制操作仍在进行时,无法阻止目标位置的新文件被更新。

实现

默认的HDFS实现是递归遍历src中的每个文件和文件夹,并按顺序将它们复制到最终目标位置(相对于dst)。

基于对象存储的文件系统应当注意上述实现所带来的限制,并可以利用并行上传以及对复制到存储中的文件进行可能的重新排序来最大化吞吐量。

接口 RemoteIterator

RemoteIterator接口作为java.util.Iterator的远程访问等价物,允许调用者遍历有限的远程数据元素序列。

核心差异在于

  1. Iterator的可选方法void remove()不被支持。
  2. 对于那些支持的方法,可能会抛出IOException异常。
public interface RemoteIterator<E> {
  boolean hasNext() throws IOException;
  E next() throws IOException;
}

该界面的基本视图是,当hasNext()为真时,意味着next()将成功返回列表中的下一个条目:

while hasNext(): next()

同样地,成功调用next()意味着如果在调用next()之前调用了hasNext(),那么它应该返回true。

boolean elementAvailable = hasNext();
try {
  next();
  assert elementAvailable;
} catch (NoSuchElementException e) {
  assert !elementAvailable
}

next()操作符必须遍历所有可用结果列表,即使没有调用hasNext()也是如此

也就是说,可以通过循环来枚举结果,该循环仅在引发NoSuchElementException异常时终止。

try {
  while (true) {
    process(iterator.next());
  }
} catch (NoSuchElementException ignored) {
  // the end of the list has been reached
}

迭代的输出等同于循环

while (iterator.hasNext()) {
  process(iterator.next());
}

由于在JVM中抛出异常是一项开销较大的操作,因此使用while(hasNext())循环选项效率更高。(另请参阅Concurrency and the Remote Iterator了解关于此主题的讨论)。

接口的实现者必须支持两种形式的迭代;测试的作者应验证两种迭代机制都能正常工作。

迭代必须返回一个有限序列;两种循环形式最终都必须终止。Hadoop代码库中该接口的所有实现都满足此要求;所有使用者均假定这一条件成立。

boolean hasNext()

当且仅当后续单次调用next()会返回元素而非抛出异常时,返回true。

前提条件

后置条件

result = True ==> next() will succeed.
result = False ==> next() will raise an exception

多次调用hasNext()而不进行任何next()调用时,必须返回相同的值。

boolean has1 = iterator.hasNext();
boolean has2 = iterator.hasNext();
assert has1 == has2;

E next()

返回迭代中的下一个元素。

前提条件

hasNext() else raise java.util.NoSuchElementException

后置条件

result = the next element in the iteration

重复调用next()会返回序列中的后续元素,直到整个序列都被返回完毕。

并发性与远程迭代器

在文件系统API中,RemoteIterator的主要用途是列出(可能是远程的)文件系统上的文件。这些文件系统总是被并发访问;文件系统的状态可能在hasNext()探测和调用next()之间发生变化。

在遍历RemoteIterator过程中,如果远程文件系统上的目录被删除,那么调用hasNext()next()可能会抛出FileNotFoundException异常。

因此,通过RemoteIterator进行健壮迭代时,应当捕获并丢弃处理过程中抛出的NoSuchElementException异常。这可以通过上述while(true)迭代示例实现,或者通过带有外部try/catch子句的hasNext()/next()序列来捕获NoSuchElementException以及其他可能在故障期间抛出的异常(例如FileNotFoundException

try {
  while (iterator.hasNext()) {
    process(iterator.next());
  }
} catch (NoSuchElementException ignored) {
  // the end of the list has been reached
}

值得注意的是,这在Hadoop代码库中并未实现。这并不意味着不推荐使用健壮的循环——更多是因为在实现这些循环时没有考虑并发问题。

接口 StreamCapabilities

StreamCapabilities 提供了一种以编程方式查询 OutputStreamInputStream 或其他 FileSystem 类所支持功能的方法。

public interface StreamCapabilities {
  boolean hasCapability(String capability);
}

boolean hasCapability(capability)

当且仅当OutputStreamInputStream或其他FileSystem类具备所需功能时返回true。

调用者可以使用字符串值查询流的性能。以下是可能的字符串值表:

字符串 常量 实现 描述
hflush HFLUSH Syncable 刷新客户端用户缓冲区中的数据。此调用返回后,新的读取者将能看到数据。
hsync HSYNC Syncable 将客户端用户缓冲区中的数据完全刷新到磁盘设备(但磁盘可能仍保留在其缓存中)。类似于POSIX的fsync函数。
in:readahead READAHEAD CanSetReadahead 设置输入流的预读取量。
dropbehind DROPBEHIND CanSetDropBehind 丢弃缓存。
in:unbuffer UNBUFFER CanUnbuffer 减少输入流的缓冲。

通过接口 EtagSource 进行Etag探测

文件系统实现可能支持从FileStatus条目查询HTTP etags。如果支持,要求如下

Etag支持必须贯穿所有list/getFileStatus()调用。

也就是说:在添加etag支持时,所有返回FileStatusListLocatedStatus条目的操作都必须返回作为EtagSource实例的子类。

当远程存储提供etags时,FileStatus实例必须包含etags。

为了支持etags,必须在getFileStatus()和列表调用中都提供它们。

实现者注意:必须重写以下核心API才能实现此功能:

FileStatus getFileStatus(Path)
FileStatus[] listStatus(Path)
RemoteIterator<FileStatus> listStatusIterator(Path)
RemoteIterator<LocatedFileStatus> listFiles([Path, boolean)

文件的Etags在所有list/getFileStatus操作中必须保持一致。

EtagSource.getEtag() 的返回值必须与返回特定对象 getFileStatus() 调用etag的list*查询保持一致。

((EtagSource)getFileStatus(path)).getEtag() == ((EtagSource)listStatus(path)[0]).getEtag()

同样地,对于路径的listFiles()listStatusIncremental()方法以及在列出父路径时,对于列表中所有文件都必须返回相同的值。

不同文件内容的ETag必须不同。

写入同一路径的两个不同数据数组在被探测时,必须具有不同的etag值。这是HTTP规范的要求。

文件ETag在重命名操作中应保持不变

文件重命名后,((EtagSource)getFileStatus(dest)).getEtag()的值应与重命名前((EtagSource)getFileStatus(source)).getEtag()的值相同。

这是存储的实现细节;它不适用于AWS S3。

当且仅当存储始终满足此要求时,文件系统才应在hasPathCapability()中声明其支持fs.capability.etags.preserved.in.rename

目录可以包含etags

目录条目可能会在列出/探测操作中返回etags;这些条目可能会在重命名操作中保留。

同样地,目录条目可能不提供此类条目,可能在重命名操作中不保留它们,也可能不保证随时间推移的一致性。

注意:特别提及根路径“/”。由于这不是一个真正的“目录”,不应期望它拥有etag。

所有支持etag的FileStatus子类必须实现Serializable接口;可选择实现Writable接口

基础类FileStatus实现了SerializableWritable接口,并对其字段进行了适当的序列化处理。

子类必须支持Java序列化(某些Apache Spark应用程序会用到),并保留etag。这需要将etag字段设为非静态并添加serialVersionUID

Writable支持曾用于在Hadoop IPC调用中编组状态数据;在Hadoop 3中,该功能通过org/apache/hadoop/fs/protocolPB/PBHelper.java实现,相关方法已被弃用。子类可以重写这些弃用方法以添加etag编组功能。但请注意——这并非强制要求,且此类编组操作很可能永远不会实际发生。

应声明适当的etag路径功能

  1. hasPathCapability(path, "fs.capability.etags.available") 必须返回true,当且仅当文件系统在文件状态/列表操作中返回有效(非空etags)时。
  2. hasPathCapability(path, "fs.capability.etags.consistent.across.rename") 当且仅当重命名操作后etags保持不变时,必须返回true。

不要求支持etag

  • 没有要求或期望FileSystem.getFileChecksum(Path)返回的校验值必须与对象的etag相关(如果该方法返回任何值的话)。
  • 如果将相同的数据两次上传到相同或不同的路径,第二次上传的etag可能与第一次上传的不匹配。