YAOTU INSIGHTS

pip安装报错No space left on device?磁盘与inode排查详解

pip安装报错No space left on device?磁盘与inode排查详解
今天帮一位同事排查安装报错时又看到了那行红色大字ERROR: Could not install packages due to an OSError: [Errno 28] No space left on device。这行提示我见过太多次几乎每个用 Python 做开发的人迟早都会撞上尤其是服务器上装依赖很多的大包时。用大白话说就是磁盘空间不够了但麻烦的地方在于它往往不是你以为的那块盘满了。收到这条报错别急着找rm -rf也别直接卸包重装。它真正想告诉你的是安装过程中某个底层写入动作失败了。这篇文章我会把报错产生的环节、排查思路、常用解法以及我踩过的一些坑一次性讲清楚适合刚接触 Python 环境管理的开发者也适合经常部署服务的运维和后台同学。1. 一字一句拆解这条报错No space left 里的门道1.1 报错的完整链路并不只有最后一行这条错误通常会出现在执行pip install的过程中看起来只是一句话但它背后对应的是一个完整的安装流程。pip 在安装一个包时不是把文件直接复制进目录就结束了它会经历下载、校验、解压、构建、安装几个阶段。这几个阶段里只要有一步需要向磁盘写入数据而对应分区的剩余空间不够文件系统就会返回一个ENOSPC错误。Python 捕获到之后包装成OSError: [Errno 28] No space left on device抛出来pip 再在最外层补一句Could not install packages due to an OSError。所以你看到那条红色提示时眼睛不要只盯在最后一行应该往上翻日志找到它具体卡在哪个动作上。如果是在Downloading阶段报错往往和下载缓存目录有关如果是在Building wheel阶段报错大多和临时目录有关如果已经走到Installing collected packages才报错那多半是目标安装目录所在的磁盘满了。把日志看明白后面的处理就顺手很多。1.2 为什么一个几百兆的包能把空间“吃”掉几个G很多人会有个困惑包明明不大我磁盘也还有几G空间怎么就说不够了这里有个常见的认知盲区pip 安装时产生的临时数据远不是最终包的大小。压缩格式的 wheel 文件需要先解压解压后的源码目录可能比压缩包大好几倍如果这个包还需要编译编译器产生的中间目标文件又会占一份空间。同时 pip 会在缓存目录里留下原始下载文件安装时还会在临时目录里再复制一份。几份数据叠在一起占用自然成倍往上翻。我处理过一台设备根分区就剩 3G看到一个包显示需要 800M觉得没问题结果一执行安装pip 先下载了 800M 缓存又解压出 1.5G 的构建目录最后安装到 site-packages 还要 900M瞬间把空间挤爆。这类问题根源就是多个写入点都落在同一个小分区上单纯看当前剩余多少空间无法预判。2. 排查现场先确定是哪个磁盘、哪种空间满了2.1 用 df 判断是容量不够还是 inode 不够遇到No space left on device第一步永远是先看磁盘整体状况而不是凭感觉猜。我会先执行df -hT这个命令会列出所有文件系统的容量、已用、可用、挂载点和文件系统类型。重点看两个地方当前工作目录所在分区以及 pip 缓存和临时目录所在分区。有时候你发现根分区满了但/home还有一大半空间那问题就变得简单想办法把写入路径指向/home即可。但有一个非常容易踩的坑很多人只看df -h看到磁盘还有空间就百思不得其解。这时候应该再看一眼 inodedf -iinode 是文件系统用来记录文件元数据的结构每个文件或者目录都会占用一个 inode。如果数据块没满但 inode 耗尽系统同样无法创建新文件同样会报No space left on device。这种情况常见于某个目录下堆积了大量小文件比如日志碎片、编译临时文件。判断方法很简单df -h还有剩余df -i已经 100%那就是 inode 耗尽。找出小文件特别多的目录清理即可后面我会专门讲。2.2 找到本次安装到底会把数据写到哪定位到哪个盘重要搞清楚会写进哪个目录更重要。pip 的写入路径主要有三条下载缓存目录负责存放下载好的包文件临时目录负责构建和打包site-packages 目录负责放置最终安装文件。分别用下面几个命令可以找到它们的位置pip cache dir python3 -c import tempfile; print(tempfile.gettempdir()) python3 -c import sysconfig; print(sysconfig.get_paths()[purelib])拿到这三条路径之后对着df -hT的输出看一眼基本就知道哪一块先撑不住。我在排查的时候习惯列一个表记录路径和对应分区这样处理时心里有数。比如下载缓存默认在家里目录下如果家目录在一号分区临时目录默认在/tmp可能对应二号分区site-packages 在虚拟环境内部对应三号分区。三条路径对应三个不同的“水池”任何一个满了都会触发行错误。看清这个后面的清理或改路径就有的放矢。3. 清缓存清日志清旧包比你想的更立竿见影3.1 pip cache 一把梭清缓存前先看清位置如果报错时你发现根分区快满了而空间占用大户往往就是 pip 的缓存目录。pip 的默认行为是缓存下载过的 wheel 包这样下次安装同一个版本就不用再从网络下载。但缓存只进不出时间一长很容易涨到几个G甚至十几个G。清理命令如下pip cache info pip cache remove *先看缓存信息再清空。如果你用的是比较老版本的 pip没有cache子命令那就直接到缓存目录手动清理。Linux 下通常在当前用户的~/.cache/pipWindows 下在用户目录的AppData\Local\pip\cache。删掉整个目录也没关系最多下次下载慢一点不会影响已安装的包。我个人的操作习惯是先用du -sh ~/.cache/pip看一眼大小有时能惊讶地发现缓存比全部环境占用的空间还大。曾经在一台服务器上光 pip 缓存就占了 8G清理之后根分区使用率从 96% 掉到 68%比装什么优化工具都管用。顺便提醒一句pip cache remove *会把所有缓存清掉如果你只想去掉某几个包可以用包名匹配操作前看清楚参数。3.2 日志、临时文件和旧虚拟环境都是空间大户除了 pip 缓存系统里还藏着很多不起眼的空间消耗源。日志是最典型的很多服务默认保留几个月甚至几年的日志。Linux 下可以检查journald日志如果积压太多可以直接清理最近一周之前的内容journalctl --vacuum-time7d包管理器也会留下历史缓存比如运行清理命令来清除旧安装包和索引。这类命令的选项我一般建议先看帮助但总体思路就是清掉已经下载过的旧安装包。临时目录/tmp也是重灾区。一些程序异常退出后临时文件不会自动清除。尤其 pip 在构建包时会在临时目录下创建一堆以pip-开头的临时目录如果构建失败没被清理会一直留在那里。可以这样查看并清理du -sh /tmp/pip-* 2/dev/null rm -rf /tmp/pip-* 2/dev/null看到文件路径是你确认过的、无关紧要的临时残留再执行删除别一股脑把所有/tmp都清空因为可能仍有正在运行的程序在占用。至于旧虚拟环境我见过很多人项目迭代后旧的虚拟环境一直留在磁盘上一个环境动辄好几个G删掉不用的环境能释放不少空间。先看大小du -sh ~/.virtualenvs/* 2/dev/null | sort -h自己确认哪些环境已经不再需要注意只删确定不用的目录。清理博客后再重新执行安装命令有很大概率直接问题解决。4. 挪窝把缓存、临时目录、安装目标一起搬到别的分区4.1 改 TMPDIR让解压临时文件落在空闲盘如果清理之后空间仍然不够或者你根本不想动系统里其他内容那就换思路不改乾坤只给 pip 挪窝。首先要考虑的是临时目录。pip 在构建 wheel 时会通过 Python 的tempfile模块寻找临时目录默认是/tmp。如果/tmp所在分区太小我们就把它指向大分区。mkdir -p /data/tmp export TMPDIR/data/tmp pip install some-package这里/data可以换成你当前设备上空间充足的那个挂载点。设置环境变量之后pip 和其他很多构建工具都会遵守这个路径。注意需要在执行 pip 的同一个 shell 里设置环境变量否则不会生效。为了稳妥我习惯在安装时同时清理缓存并设置新临时目录mkdir -p /data/tmp TMPDIR/data/tmp pip install --no-cache-dir 包名--no-cache-dir可以防止缓存再写进根分区TMPDIR则把解压和构建过程挪到大分区两条一起用临时盘和缓存盘的压力同时解除。4.2 改 cache-dir让下载缓存不再占满系统盘缓存目录的位置同样可以永久设置。pip 支持通过配置文件设定缓存目录Linux 下全局配置通常在/etc/pip.conf用户级配置在~/.config/pip/pip.conf。你也可以通过命令行直接设置当前用户的配置pip config set global.cache-dir /data/pip-cache执行完之后再安装包时下载产物就会写到新位置。和临时目录不同这个配置是持久的重启终端也不会丢失。我建议在服务器上第一次部署 Python 环境时就设置好这个项避免投产之后缓存不断膨胀把系统盘打满。需要注意旧缓存目录里的文件不会自动迁移如果你之前的缓存已经占了很多空间改完配置之后最好顺手把旧目录清掉。4.3 应急方案--target 和 --user 怎么选有时候你不想动系统分区但又要装一个很大的包。这时可以把安装目标指定到别的磁盘。--target参数允许你把包安装到一个任意目录然后通过PYTHONPATH导入pip install --target /data/pylib 包名 export PYTHONPATH/data/pylib:$PYTHONPATH这个方案非常灵活适合临时测试或者强制绕过空间限制。但要注意用--target安装的包不会走常规的卸载流程也没有生成入口脚本依赖关系需要你自己维护长期使用容易乱。所以我通常在应急场景才用它。另一个思路是装到当前用户目录使用--user参数pip install --user 包名这类包会安装到用户主目录下的.local/lib/python3.x/site-packages。如果你的家目录所在分区比较大这条路很省事。但如果你当前已经在一个虚拟环境里--user的语义会有变化作用并不明显如果你在家目录分区也很小那它帮不上忙。选择哪个取决于你已经定位到的各分区剩余情况。5. 这几个坑让我被坑过见到同类报错先按这条思路查5.1 不是根分区满而是 /tmp 挂载的 tmpfs 小有次我在一台内存很大的设备上装依赖根分区明明还剩 20G但安装大型框架仍然报No space left。查了半天才发现/tmp并不是磁盘上的目录而是挂载的tmpfs。tmpfs使用内存作为存储容量默认只有物理内存的一半左右。当时那台设备的内存看起来很大但可用内存的余量已经很紧张临时目录能写入的空间少得可怜。遇到这种情况第一反应不是清理根分区而是确认df -hT里/tmp的文件系统类型。如果显示tmpfs那你需要定制一下或者干脆把临时目录指向磁盘上的一个目录。方式是修改挂载参数或者在安装时用前面提到的TMPDIR环境变量。我要强调一句报错信息不会告诉你是哪块空间有问题你自己排查定位是唯一可靠的方法。5.2 不是 block 不够而是 inode 用完了还有一个迷惑性更强的场景。磁盘明明显示剩余好几个Gdf -i却显示 inode 已经 100%。报错虽然也叫No space left on device但真正原因是文件数量超过上限。这种场景我见过好几次都是因为某个目录下生成了大量极小文件比如某个测试脚本在/tmp下循环创建文件但没按时删除或者日志系统按秒切分文件。排查命令很简单df -hT df -i如果第二个命令提示已经耗尽接下来用find /tmp -xdev -type f | wc -l统计某个分区的小文件数量找到数量离谱的目录再清理。别在 inode 耗尽时还往同一个分区写大文件那是反向操作。清掉一批垃圾文件之后inode 腾出来问题自然消失。因为 inode 是预分配的即使删了文件它也不会自动增加所以一开始设置分区时如果预期会放很多小文件就要考虑适当调大 inode 数量。5.3 容器环境下“磁盘满”另有说法在容器环境里运行时No space left on device还有一层额外含义它可能不是物理磁盘满了而是容器的可写层满了。容器默认的写入层大小有限如果你在容器里安装包、生成日志写满了这一层同样会报这个错误。这时你在宿主机上查看磁盘使用率可能一切正常因为限制的是容器层大小而不是宿主机磁盘。一般处理方法是清理容器内部不需要的缓存和日志或者给容器存储重新分配更大的空间也可以把数据卷挂载到宿主机。关键点是别把容器当成一台正常的服务器来排查先确认自己所在的运行环境再选方向。我在一套自动化构建系统里就遇到过镜像反复构建每一层都留下大体积文件最终把容器分区撑满的情况最后清掉多余镜像和构建缓存才恢复。6. 预防方案能自动化就自动化别等磁盘爆了才动手6.1 写一个清理脚本定时跑起来这类问题的根本原因是“只进不出”。与其每次都等人报错再上去救火不如提前部署一个清理脚本。我在维护的设备上会放一个简单脚本定期执行三个动作清 pip 缓存、清理 journal 日志、清理过期临时目录。脚本内容并不复杂核心代码如下#!/bin/bash # 清理 pip 缓存 if command -v pip /dev/null; then pip cache purge 2/dev/null || true fi # 清理超过 7 天的 journal 日志 journalctl --vacuum-time7d 2/dev/null || true # 清理 /tmp 下超过 3 天未修改的 pip 临时目录 find /tmp -maxdepth 1 -name pip-* -mtime 3 -exec rm -rf {} \; 2/dev/null || true通过计划任务每天运行一次磁盘使用率长期保持在健康水位。写脚本的时候有一个很重要的细节不要直接清理所有临时文件只清理自己认识的那一类否则很可能误删正在被使用的数据。脚本上线前先手动跑一遍查看输出确认不会误删再交给计划任务。6.2 环境初始化时就把“大件”放到独立分区比定时清理更稳的做法是在环境初始化阶段就把缓存和临时目录指向大分区。用 pip 时我在新环境里第一件要做的事就是设置cache-dir指向数据盘同时把TMPDIR固定下来。如果使用虚拟环境我会在项目说明里标注清楚“安装前请确保分区剩余空间不低于若干G”。与其事后手忙脚乱不如一开始让写入路径落在规划好的位置。容器和 CI 环境同样如此。构建镜像时如果不需要缓存尽量用不缓存的方式构建流水线里定期清理历史构建产物和未使用的中间层。我习惯在 CI 脚本里加一个清理步骤每次构建结束之后清理该轮产生的临时文件防止“节省一时”变成“日后爆盘”。清理脚本和定期监控配合起来能省很多事。你也可以用系统自带的状态检查工具自己在本地写几行代码检测分区使用率超过阈值就输出告警。比如随手写一个判断usage$(df / | awk NR2 {print $5} | sed s/%//) if [ $usage -gt 80 ]; then echo 根分区使用率超过 80% fi这个可以做成计划任务一旦超阈值就把提醒发出来。几十行代码的投资能避免大半夜被“安装失败”的告警吵醒。最后分享一点个人经验处理这个报错多了我最大的体会是遇到No space left on device第一反应不要是找“删除大文件命令大全”而是先回答清楚三个问题——哪个分区满了、是容量还是 inode 满了、写入路径为什么会落在那里。把这三点摸清楚再动手清理或者调整路径基本不会再翻车。另外日常操作时留个心眼任何删文件的操作都先du -sh确认目录大小任何清理命令都先在少量数据上验证。真出问题的时候宁可多花五分钟看日志也不要凭经验盲目删。毕竟空间不足往往只是表象看清底层的写入机制这个问题其实一点也不难。