背景
一个用来运行 Python 代码的 Jupyter 服务,由于某些原因,将 Python 的 pip 包安装目录使用 s3fs 挂载到了 MinIO。
然后就发生了一个很奇怪的现象,当使用
! pip install module
安装某个包,然后代码中使用
import module
来导入包时,总会报错显示找不到包。但当重启了 jupyter 服务后,import module 就会正常运行。
排查步骤及原因分析
一、确认在 !pip install module 执行之后,在 import 执行前,对应库的安装文件确实存在。
通过如下代码确认包安装后的路径:
import module
print(module)
结果是文件确实存在。
二、研究 Python 的导包机制,分析为何没有找到包。
具体源码参见 importlib 标准库。通过追踪如下代码的调用链
importlib.import_module("module")
发现有几个可疑点使用了缓存:
1. sys.path_importer_cache
此对象数据结构就是一个字典。key 是路径,value 是查找器对象(比如 importlib._bootstrap_external.FileFinder )
每次对包的查找,都会使用这个缓存来记录查找过的路径。这样下次就可以很快的定位,主要是为了提高找包效率。
当导入一个包时,包所在的目录及其上级目录都会在 sys.path_importer_cache 里有记录。这样当第二次导入同目录下的包时,就不需要再次遍历子目录。
但同样的,如果缓存的 value 出了问题,那么就会影响此路径下包的导入
2. importlib._bootstrap_external.FileFinder
此对象在构建时会缓存指定路径下的目录列表。避免多次调用操作系统接口来获取目录,也是为了提高效率。
但目录下的文件在安装包之后是会变化的,所以 FileFinder 本身也有一个机制来发现目录的变化,用的是目录的 mtime 修改时间
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 这个mtime为0的问题无法解决,所以目录一旦缓存就无法自动刷新。
所能做的就是在每次 !pip install module 之后手动将 sys.path_importer_cache 置为空字典,强制触发磁盘扫描。
参考文档:
https://docs.python.org/zh-cn/3/library/sys.html#sys.path_importer_cache