旷视研究院 · 2021年12月14日

旷视开源 MegFile——Python 文件读写 SDK

起源

旷视研究院在探索大数据存储方案的过程中,先后尝试了多种文件存储系统,包括 POSIX 协议的本地存储、OSS 等。

多种文件存储系统带来了开发上的困难,对于存储于不同文件系统的文件,在访问时需要针对不同文件系统的 SDK 编写复杂的代码,增加了开发者的工作量。除此之外,跨系统的文件访问在代码实现上也不够优雅,导致代码不易读。

为了解决上述问题,我们开发了 MegFile。

特性

MegFile 作为 Python 文件读写 SDK,主要提供三个特性:

1.方便、齐全并和标注库风格保持一致的 API
2.快速 s3 文件文件读写
3.支持 s3 的 Glob AP

API 接口

与 Python 标准库相似,MegFile 提供了函数式 API 和 Path-Like API 两种文件访问方式。

以 smart 开头的函数库提供了形如 smart_open, smart_glob,smart_listdir,smart_exists 等函数,来进行文件访问;同时,用户也可以使用 SmartPath 构建一个 Pathlike 对象,提供了open,unlink,remove 等方法。

不论是 SmartPath 还是 smart 系列函数,都能够识别不同文件系统路径协议 (例如:file:///path/to/file、s3://bucket/key),并正确完成对应的操作,为多种文件系统提供了统一流畅的访问体验,极大简化了跨后端文件访问的代码量和编写难度。

文件读写速度

在大型项目和读写密集型任务上,文件读写速度非常重要。如果为了支持跨后端文件读写而牺牲读写速度是得不偿失的。在 MegFile 中,使用 smart_open 方法不仅可以打开各种协议的资源,在旷视内部最常用的 s3 协议上的 reader / writer 内部提供了多线程实现,并通过自适应的预读策略,使得其速度比目前已知 Python 竞品(smart-open 和 facebook-iopath)都要快。具体对比呈现在后文表中。

MegFile 读 s3 文件如此快的秘诀,就是多线程加预读。

在 MegFile 中,s3 所有的文件读写都是以块为单位的,一个块的大小为 8MB。在 s3 读取文件时,会通过线程池预读若干块数据到本地,本地维护一个 LRU 来存储数据块,为每个文件最多维护 16 个块。

而预读的策略,通过观察用户的行为,我们发现,用户通常会以三种模式访问文件:

1.从头到尾完整顺序读取文件,这种方式最常见,称之为:顺序读取模式;

2.在 2、3 个位置来回跳着读,常见于读取 tar 包和视频时,在文件的 header 和 内容 之间来回读取,称之为:多块反复读取模式;

3.在随机的位置读取很少数据,称之为:纯随机读。

MegFile 的 S3PrefetchReader 能够通过记录用户在每个文件句柄上过往的行为,判断用户有几个读取的热区,来自适应选择这 3 种读取模式加快读取速度。

当文件被顺序读取时,文件只有一个读取热区,reader 会预读 16(LRU 的大小) / 1(读取热区个数) = 16 个块。

with smart_open('path/to/file') as fd:
    fd.read()

预读的文件块如图所示:

image.png
顺序读取的性能对比如下表所示:

image.png

当文件在若干个块中不停切换时,reader 会为每个热区预读取 [16 / n] 个块,n 为读取热区的数量,进入多块反复读取模式,以加快读取 tar 包和视频等结构化文件的读取速度。

with smart_open('path/to/file') as fd:
    fd.seek(0)
    fd.read(1024)  # 读取文件头
    fd.seek(65536)
    fd.read(1024)  # 读取数据内容
 
    fd.seek(0)
    fd.read(1024)  # 读取文件头
    fd.seek(65536 + 1024)
    fd.read(1024)  # 读取数据内容
 
    ...

预读的文件块如图所示:

image.png

多块反复读的性能对比如下表所示:

image.png

当文件读取的热区大于 16 个,每次只会读取 [16 / 16] = 1 个块,也就是不再预读文件,同时,如果每次 seek 后读取内容都小于 8KB 时,会进入随机读取模式。

这时我们认为预读已经不再划算,每次用户需要多少数据就读取多少数据是最优解,因此这时 reader 不会再预读数据,并且不再一次性读取 8MB 数据。


with smart_open('path/to/file') as fd:
    fd.seek(2048)
    fd.read(1024)  # 随机读一段
 
    fd.seek(65536)
    fd.read(1024)  # 再随机读一段
 
    ...

读取的文件块如图所示:

image.png

随即读取的性能对比如下:

image.png

由于 iopath 会把整个文件下载下来再进行访问,所以在文件大小不大且多次随即读写的情况下,优于 refile 和 smart-open,但当文件过大时,可能导致内存溢出。

MegFile 考虑到文件下载速度和随机读取性能,在文件小于 256 MB 时,不进入随即读取模式,而采用多块随即读的策略,在这样的情景下,性能对比如下表:

image.png

在 writer 的写入数据时,维护了最多 2 个数据块。默认采用多线程 multipart upload 的上传方式获得更高的速度。但是在写入大量小文件时,使用 multipart 上传文件,每个文件需要 create_multipart_upload / upload_part / complete_multipart_upload 三次 API 请求,会导致 QPS 过大,因此,当数据块不超过 2 块时,会使用 put_object 方法上传文件。

通过 reader / writer 的自适应多模式读写, 在 s3 协议上取得了目前已知 Python 竞品都要快的文件读写性能。

Glob 语法支持

glob 是用于匹配符合指定模式的文件集合的一种语言,在操作系统终端和 Python 中都支持 glob 语法进行文件匹配,在 MegFile 中也支持基本的 glob 进行文件匹配。

相比标准库的 glob 模块,MegFile glob 额外支持了 zsh 中 {} 进行选择匹配的扩展语法,例如:s3://bucket/video.{mp4,avi}。在 s3 上不仅支持匹配同一个 bucket 上的路径,还能够对多个 bucket 上的文件进行匹配。

smart_glob('s3://{bucket1,bucket2}/key')
smart_glob('s3://bucket/[2-3]/**/?.mp4')
smart_glob('/folder/[123]/video{.avi,.mp4}')

在最开始实现 s3 上的 glob 时,我们仿照将 Python 标准库的 glob 方法,将 os.listdir 改用 s3 API 实现,完成了我们的 glob 的第一个版本。

但是实际使用过程中,我们发现这个 glob 的实现,在 s3 中存在大量目录,每个目录中文件较少的场景下,速度很慢;分析其原因,我们认为频繁地调用 listdir,也就是在每个目录上调用 list_object_v2 请求,会导致 QPS 过高,因此我们对 glob 进行了进一步改进。

新版的 glob 在 s3 上在根目录上调用 list_objects_v2 一次性得到所有包含匹配前缀的所有路径,再与模式串进行匹配。

相比第一版中基于 listdir 逐层目录匹配的 glob,大大减少了请求次数,速度有了显著提升,在一些特殊的场景下,速度甚至可比第一版 glob 提升 3 个数量级。

体验

通过上述功能,MegFile 提供了统一且完善的文件操作体验。通过使用 MegFile 访问文件,你能丝滑无感地在不同后端访问文件,让你能够更聚焦在自己项目的逻辑而不是「这个文件在哪个后端」的问题,同时还无需担心由此带来的性能问题。

首发:旷视研究院
作者:李阳

专栏文章推荐

欢迎关注旷视研究院极术社区专栏,定期更新最新旷视研究院成果
加入旷视:career@megvii.com
推荐阅读
关注数
7696
内容数
164
专注旷视研究院学术论文解读推送,涵盖计算机视觉,文字识别等
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息