Python 的 import 缓存机制与 s3fs 的冲突

背景

一个用来运行 Python 代码的 Jupyter 服务,由于某些原因,将 Python 的 pip 包安装目录使用 s3fs 挂载到了 MinIO。

然后就发生了一个很奇怪的现象,当使用

1
! pip install module

安装某个包,然后代码中使用

1
import module

来导入包时,总会报错显示找不到包。但当重启了 jupyter 服务后,import module 就会正常运行。

排查步骤及原因分析

一. 确认在 !pip install module 执行之后,在 import 执行前,对应库的安装文件确实存在。

通过如下代码确认包安装后的路径:

1
2
3
import module

print(module)

结果是文件确实存在。

二. 研究 Python 的导包机制,分析为何没有找到包。

具体源码参见 importlib 标准库。通过追踪如下代码的调用链

1
importlib.import_module("module")

发现有几个可疑点使用了缓存:

  1. sys.path_importer_cache

此对象数据结构就是一个字典。key 是路径,value 是查找器对象(比如 importlib._bootstrap_external.FileFinder )

每次对包的查找,都会使用这个缓存来记录查找过的路径。这样下次就可以很快的定位,主要是为了提高找包效率。

当导入一个包时,包所在的目录及其上级目录都会在 sys.path_importer_cache 里有记录。这样当第二次导入同目录下的包时,就不需要再次遍历子目录。

但同样的,如果缓存的 value 出了问题,那么就会影响此路径下包的导入

  1. importlib._bootstrap_external.FileFinder

此对象在构建时会缓存指定路径下的目录列表。避免多次调用操作系统接口来获取目录,也是为了提高效率。

但目录下的文件在安装包之后是会变化的,所以 FileFinder 本身也有一个机制来发现目录的变化,用的是目录的 mtime 修改时间

1
2
3
4
5
mtime = _path_stat(self.path or _os.getcwd()).st_mtime

// posix.stat(path).st_mtime

// nt.stat(path).st_mtime

当发现目录的 mtime 发生了变化时,会刷新缓存。

看起来很合理,也确实合理,但就是还是找不到包。

三. 重点锁定 FileFinder 的缓存机制,测试是否触发了缓存刷新。

通过输出 !pip install module 执行前后,对应目录的 mtime, 发现没有变化而且都是 0。

此时问题其实就定位到了 s3fs 挂载的目录无法修改 mtime 的问题。

正常的操作系统目录,当目录内有文件发生改变时,目录的 mtime 也会相应发生改变。而通过 s3fs 挂载的目录则不会。

也没有办法修改。

猜测一下原因:

  S3本来是没有目录的概念的,所谓的目录仅仅是对同样前缀的文件的虚拟,并不是真实存在的一个东西。

但一般文件系统的目录是真实存在于磁盘上。所以如果要实现目录内文件修改的同时同步修改目录属性,则

s3fs 就需要额外的地方来存储这些信息,但这些信息无论是放在主机上,还是放在S3上都不太合适。一旦支

持了,就需要考虑多主机挂载同一目录时的同步问题。

解决方案

  s3fs 这个问题无法解决,所以目录一旦缓存就无法自动刷新。

  所能做的就是在每次 !pip install module 之后手动将 sys.path_importer_cache 置为空字典,强制触发磁盘扫描。

参考文档

https://docs.python.org/zh-cn/3/library/sys.html#sys.path_importer_cache

https://www.cnblogs.com/zhaojiedi1992/p/zhaojiedi_linux_031_linuxtime.html