fsspec 是 Dask、Intake、s3fs、gcsfs 及其他项目中文件系统操作的新基础,现已作为独立接口和开发新后端及文件操作的中心位置提供。尽管它作为 Dask 的一部分开发,但您不再需要 Dask 即可使用此功能。
过去几年里,Dask 的 IO 能力逐渐有机地增长,涵盖了多种文件格式,并能够在各种远程/云数据系统上无缝访问数据。这通过一些将云资源视为文件系统的配套软件包以及 dask.bytes 中的专用代码来实现。一些存储后端,特别是 s3fs,在 Dask 之外也立即变得有用,并被 pandas、xarray 等作为可选依赖项采用。
为了整合各种后端的行为,为任何新后端提供单一参考规范,并使这套文件系统操作即使没有 Dask 也能使用,我创建了 fsspec。上周,Dask 改为直接使用 fsspec 来满足其 IO 需求,我想在此详细描述这一变化的好处。
虽然最初这样做是为了减轻维护负担,但重要的意义在于,我们希望使文件系统操作能够轻松地供整个 pydata 生态系统使用,无论是否使用 Dask。
我写的第一个文件系统是 hdfs3,它是 libhdfs3 C 库的一个薄包装器。当时,Dask 已经获得了在分布式集群上运行的能力,而 HDFS 是这些集群中最流行的存储解决方案(至少在商业领域),因此需要一个解决方案。Python API 与 C API 非常接近,而 C API 又遵循 Java API 和 posix 标准。幸运的是,Python 已经有一个 文件类标准,因此提供实现该标准的对象足以使远程字节可供许多软件包使用。
很快,云资源的重要性就显而易见,至少与集群内文件系统同等重要,因此随之出现了 s3fs、adlfs 和 gcsfs。每个都遵循相同的模式,但针对给定的接口有一些特定的代码,并根据先前接口的经验进行了改进。在此期间,由于像 parquet 这样更复杂的文件格式,Dask 的需求也发生了变化。与不同后端接口并调整其方法的代码最终进入了 Dask 仓库。
与此同时,其他文件系统接口也出现了,特别是 pyarrow,它有自己的 HDFS 实现和直接 parquet 读取功能。但我们希望生态系统中的所有工具都能很好地协同工作,以便 Dask 可以使用任何存储后端的任一引擎读取 parquet。
复制一个接口,然后对其进行调整并发布,就像我在每次文件系统迭代中所做的那样,无疑是快速完成工作的方法。然而,当你想要改变其行为或添加新功能时,结果发现你需要在每个地方重复工作(违反了 DRY 原则),或者让接口缓慢地分化。很好的例子是 glob 和 walk,前者支持各种选项,而后者则返回不同的东西(列表、版本目录/文件迭代器)。
>>> fs = dask.bytes.local.LocalFileSystem()
>>> fs.walk('/home/path/')
<迭代器 (tuples)>
>>> fs = s3fs.S3FileSystme()
>>> fs.walk('bucket/path')
[文件名列表]
我们发现,为了满足 Dask 的需求,我们需要构建小型包装类来确保与所有后端兼容的 API,以及一个用于以相同接口操作本地文件系统的类,最后是一个包含各种辅助功能的注册表。其中很少一部分是 Dask 特有的,只有少数几个函数涉及构建图和延迟执行。然而,这确实提出了一个重要问题:文件系统应该可序列化,并且应该有一种方法来指定要打开的文件,该文件也应该可序列化(理想情况下支持透明的文本和压缩)。
我之前已经提到了创建本地文件系统类的工作,该类符合现有其他文件系统的接口。但 Dask 用户(及其他人)可能还需要更多选项,例如 ssh、ftp、http、内存中等。根据用户处理这些选项的需求,我们开始编写更多的文件系统接口,所有这些都位于 dask.bytes 中;但尚不清楚它们是应该只支持非常有限的功能(仅够 Dask 完成某些任务),还是支持一整套文件操作。
尤其是内存文件系统,存在于一个持续时间极长的 PR 中——不清楚这样的东西对 Dask 有多大用处,因为每个工作节点都有自己的内存,因此看到的是“文件系统”的不同状态。
文件系统规范 (file system Spec),后来称为 fsspec,源于对存储后端行为进行编码和整合、减少重复,并为所有后端提供相同功能的愿望。在此过程中,编写新的实现类变得容易得多:参见 实现,其中包括有趣且高度实验性的选项,例如 CachingFileSystem,它为每次远程读取创建本地副本,以便第二次访问更快。然而,更重要的主流实现也形成了,例如 FTP、SSH、Memory 和 webHDFS(后者是集群外部访问 HDFS 的最佳选择,解决了使用 hdfs3 构建和认证的所有问题)。
此外,新的仓库提供了实现新功能的机会,这些功能将比仅在选定仓库中完成具有更广泛的适用性。示例包括 FUSE 挂载、文件系统上的字典式键值视图(例如 zarr 使用的),以及文件的事务性写入。所有文件系统都可序列化且符合 pyarrow 标准。
最终我意识到,文件系统类提供的操作对于不使用 Dask 的人也非常有用。事实上,例如 s3fs,它被广泛地独立使用,或与 fastparquet 之类的工具结合使用,后者可以接受文件系统函数作为其方法参数,或者与 pandas 结合使用。
因此,似乎有必要设立一个专门的仓库来编写 Dask 兼容文件系统应该遵循的规范,我发现可以从现有实现中提取出大量通用行为,将只在某些实现中存在的功能提供给所有实现,并在此过程中全面改进每一个实现。
然而,当将 fsspec 与 Intake 结合考虑时,我才意识到一个独立的文件系统包有多么普遍的用处:该 PR 实现了一个通用的文件选择器,它可以浏览我们可用的任何文件系统中的文件,甚至例如能够将 S3 上的远程 zip 文件视为可浏览的文件系统。请注意,与本博客的总体主旨类似,文件选择器本身不必位于 Intake 仓库中,最终将成为一个独立的东西,或 fsspec 的一个可选功能。您也不应该仅仅为了获得通用文件系统操作而需要 Intake。
这项工作尚未达到诸如知名的 Python 缓冲区协议之类的“协议标准”水平,但我认为它是使各种存储服务中的数据可供人们使用的一个有益步骤,因为您可以使用相同的 API 对每个服务进行操作,预期相同的行为,并创建真实的 Python 类文件对象以传递给其他函数。拥有这样一个集中的中央仓库提供了一个显而易见的地方来讨论和修订规范,并在其上构建额外功能。
许多改进仍有待完成,例如在更多函数中支持 glob 字符串,或一个可以根据提供的 URL 形式分派到各种后端的单一文件系统;但现在有了进行所有这些工作的明显场所。