
我最近在部署一个Python服务时又撞见了那条熟悉的报错——ERROR: Could not install packages due to an OSError: [Errno 28] No space left on device。明明前几天还能正常安装依赖怎么突然就不行了更迷惑的是当时我用df -h一看根分区只用了76%理论上还剩不少空间可pip就是拧着不肯装。我相信不少朋友都遇到过这个让人血压升高的瞬间。这条报错看似简单——设备上没有空间但实际排查起来往往比想象中曲折可能是临时目录满了可能是pip缓存失控可能是inode耗尽甚至可能是容器Overlay文件系统的幻觉。这篇博文我就把自己踩过的坑、完整的排查链路和最终的根治方案都摊开讲讲希望帮你少走几步弯路。这个内容主要适合三类人被这条报错卡住的生产环境维护者、正在用pip安装大量依赖的Python开发者以及管理虚拟环境和CI/CD流水线的同学。不需要你有多深的内核知识但看完之后你至少能自己动手定位问题、快速恢复安装并且知道怎么避免下次再翻车。1. 报错现场这条OSError到底在说什么1.1 典型场景pip在哪个环节崩溃先还原一下现场。通常我们会执行类似这样的命令pip install -r requirements.txt然后pip开始下载依赖解析版本正准备一轮安装时终端里猛地甩出一段红色告警ERROR: Could not install packages due to an OSError: [Errno 28] No space left on device有意思的是这段报错经常出现在安装过程的回滚阶段。因为pip在安装多个包时并不是一个个原子写入的它会先把新版本写入目标路径如果中途发现空间不足就会尝试把之前装了一半的包恢复原状。所以很多时候报错末尾还会跟着一堆回滚日志看起来特别吓人但本质上还是空间不够。这背后有个容易被忽略的细节pip的安装过程涉及多个目录——下载缓存目录、解压临时目录、实际的site-packages目录以及虚拟环境的根目录。其中任何一个环节所在分区满了都会抛出这条同样的OSError。如果你只盯着项目目录看空间大概率会漏掉真正的病灶。1.2 没有空间的两种含义块空间与inodeLinux系统里没有空间其实有两种截然不同的情况第一种是磁盘块空间不足也就是常规意义上的容量满了。大文件写不进去df -h直接显示100%。第二种是索引节点inode耗尽。每个文件、目录都会占用一个inode而一个分区的inode总数在格式化时是固定的。当分区里塞满了海量小文件时即使磁盘块还有剩余系统也无法再创建新文件。我见过不少同事用df -h看到还有20%空间就百思不得其解其实只要用df -i一看inode使用率已经100%了。pip安装时会创建大量临时文件、编译中间文件一旦inode耗尽就会出现和磁盘满一模一样的报错。这一点在后面会专门展开说。所以拿到这条报错的第一步不是急着删库跑路而是先搞清楚到底是哪种空间不够再对症下药。2. 从报错到定位我的完整排查链路2.1 第一步不要急着删文件先看几个命令报错出现后我习惯说一句先别上手删数据无价。然后按顺序敲三组命令df -h df -i du -sh /* 2/dev/null | sort -rh | head -20df -h看分区容量使用率df -i看inode使用率du -sh /*则用来找具体是哪个一级目录吃掉了空间。这里有个经验如果df -h显示使用率超过90%别犹豫磁盘块空间就是主要嫌疑。如果使用率看起来还算健康而df -i爆了那就是inode问题。如果两者都正常就要考虑是不是有进程占用了已删除文件或者某个挂载点把临时目录指到了特殊位置。2.2 第二步锁定pip的作案路径既然报错来自pip那先摸清pip到底会往哪些目录写东西。以最常见的CPython环境为例pip缓存目录默认在~/.cache/pipLinux下通常是/root/.cache/pip或/home/user/.cache/pip。这里保存了所有下载过的wheel包日积月累可以轻松占用几GB到几十GB。临时解压目录pip下载后需要解压默认会用到/tmp也可能受TMPDIR环境变量影响。site-packages目录虚拟环境或系统环境的第三方包安装位置。当前项目的虚拟环境根目录包括lib/pythonX.Y/site-packages和bin目录。我的排查策略很简单逐个目录去du -sh看谁最可疑。有一次我在帮朋友排查时发现他/tmp下堆了12GB的临时文件里面全是各种编辑器产生的swap文件pip一解压就当场崩溃。2.3 第三步结合挂载点判断真实分区很多服务器有专门的数据盘比如/data挂载了独立分区而/home可能在系统盘上。如果pip缓存放在/root/.cache那就是在根分区上如果你的项目在/data上但虚拟环境却在/root/venv下那占用的还是根分区。一个比较实用的操作是查看挂载点df -h /root/.cache df -h /tmp df -h /path/to/your/venv这样逐个目录确认它们落在哪个分区就能快速判断是哪个分区出了问题。而不是傻傻地把整个/分区翻个底朝天。3. 磁盘空间不足的高频元凶与清理实战3.1 元凶一pip缓存失控pip缓存可以说是头号嫌疑。默认情况下pip会缓存所有下载过的包留待下次直接使用。我的某台开发机上~/.cache/pip曾经膨胀到23GB连我自己都吓了一跳。清理方式非常简单pip cache purge或者手动删rm -rf ~/.cache/pip但要注意在生产环境或者多用户共用的服务器上每个用户的缓存目录都可能是独立存在的。如果使用root安装包清的是/root/.cache/pip如果用普通用户清的是/home/user/.cache/pip。漏掉任何一个用户都可能排查半天后还是没解决。更稳妥的做法是调整pip的全局配置从根本上限制缓存行为。在/etc/pip.conf或~/.config/pip/pip.conf里写[global] no-cache-dir true这样pip就不会再写本地缓存。对于依赖变化频繁的开发环境关掉缓存其实能避免不少缓存污染导致版本冲突的怪问题。3.2 元凶二/tmp与系统临时目录/tmp通常位于根分区默认容量与系统盘共享。很多程序都爱往/tmp里写临时文件比如Python的tempfile模块、打包工具、编译器以及pip解压wheel时。如果/tmp所在分区满了报错信息跟pip一点关系都不像但本质上还是空间不足。我踩过一个典型的坑某次通过pyenv安装新Python版本时编译过程占满了/tmp结果pip再安装包时就报出了这条OSError。当时我还以为是pip本身出了问题后来才发现是/tmp已经99%了。清理/tmp时要注意不建议随意删除正在被进程使用的临时文件。比较稳妥的方式是看文件修改时间删除超过7天且没有被进程占用的文件。更专业的做法是用lsof L1查找当前被删除但仍被占用的文件这在后面会详细讲。3.3 元凶三历史版本残留与日志文件还有一种很容易被忽略的场景多次升级虚拟环境或者反复pip install --upgrade之后site-packages里会残留旧版本包的目录和.pyc文件。这些碎片文件虽然单个体积不大但数量一多占用的块空间和inode都不少。另外日志文件也是吞空间的巨兽。我见过一台机器/var/log下有一个应用程序的日志文件因为一直没做轮转膨胀到了40GB。当时那个应用还不停把日志往磁盘写直到把分区写满导致所有新建文件失败。用find / -xdev -type f -size 100M -exec ls -lh {} \;这类命令可以快速找出大文件然后逐个确认是大日志还是大缓存。3.4 元凶四内存盘与Docker Overlay的假满如果你在跑Docker或者使用了tmpfs那空间问题就更隐蔽了。tmpfs内存盘如果你把某些目录挂成tmpfs比如常见的/dev/shm那它的容量默认只有物理内存的一半。一旦容器或者程序大量写入共享内存就会报设备上没有空间。我曾在某个容器平台上见过/dev/shm被写满后pip安装直接失败。Docker OverlayFS容器内的根文件系统是分层的df -h在容器里看到的容量实际上是宿主机给容器分配的上限。如果宿主机磁盘满了容器里照样会报同样的OSError而且你在容器里执行df -h看到的可能还是健康状态如果你只盯着容器看指标会欺骗你。遇到这种情况要跳出容器直接到宿主机上去看df -h同时检查Docker容器的读写层大小docker system df这个命令会列出镜像、容器、卷、构建缓存各自占用的空间。开发环境下docker system prune -a能清理掉大量悬空镜像和构建缓存往往一清理完pip安装就恢复正常了。4. 解决安装失败的落地操作从临时方案到长久方案4.1 临时绕过先让pip装上再说如果你现在急需把依赖装上顾不上深究可以用两个参数先撑过去pip install --no-cache-dir -r requirements.txt--no-cache-dir会让pip跳过本地缓存不需要写缓存文件能省一笔空间。同时还可以重定向临时目录TMPDIR/data/tmp pip install --no-cache-dir -r requirements.txt前提是/data/tmp所在分区有足够空间并且该目录已经存在且可写。我经常把TMPDIR指向一个独立的数据盘这样即使系统盘满了pip也能顺利完成解压和安装。不过这只是权宜之计。如果site-packages本身所在分区已经满了那--no-cache-dir也没用。这种情况下要么清理出空间要么把虚拟环境迁移到别的分区这一点后面会说到。4.2 清理后重装一套标准的复现步骤当定位到具体占空间的大户之后我的操作顺序是这样的停止正在写入大文件的服务比如日志量很大的应用先暂停或切换日志输出路径避免刚清理完又立刻写满。清理pip缓存执行pip cache purge或手动删~/.cache/pip。清理临时文件删除/tmp下三天前的文件注意别删正在被占用的并确认TMPDIR环境变量所指目录的性质。清理包管理器缓存如果你的服务器还用yum/apt顺手执行yum clean all或apt clean系统包里也藏着不少缓存。扩大site-packages所在分区如果有LVM卷组可以试着扩逻辑卷。没有LVM的话只能迁移虚拟环境目录。重新创建虚拟环境如果修复后虚拟环境结构已经混乱建议直接删掉重建rm -rf /path/to/venv python3 -m venv /path/to/venv source /path/to/venv/bin/activate pip install --no-cache-dir -r requirements.txt我习惯在重新安装时加--no-cache-dir一边装一边留意磁盘的变化。如果清理完空间后还是报错那就要检查是不是inode问题下面会专门讲。4.3 扩容与迁移给系统盘减负如果排查下来发现根分区真的不够用而且你手上还有一块空闲数据盘那么迁移虚拟环境是比扩容更省事的方案。迁移的步骤如下在数据盘上创建新目录比如/data/envs。将旧的虚拟环境整体拷贝过去cp -a /root/venv /data/envs/project-venv注意用cp -a保留软链接和权限不要用cp -r那种会破坏symlink的方式。修改虚拟环境内的路径硬编码。由于venv内部有很多activate脚本和shebang引用了绝对路径最简单的方法是用virtualenv --relocatable试试但不要依赖它更可靠的是删掉旧venv直接在目标路径下重建python3 -m venv /data/envs/project-venv这样所有路径都会以新位置为准避免后续各种找不到入口点的诡异问题。在项目的bin脚本或启动脚本中改用新路径激活虚拟环境。如果整个系统盘都不够用那就得考虑把/home、/root或/var这些目录迁到数据盘。我常用的方法是直接挂载绑定# 先确认数据盘只被当前目标使用 mkdir /mnt/data mount /dev/sdb1 /mnt/data cp -a /root /mnt/data/ mount --bind /mnt/data/root /root然后把挂载写入/etc/fstab重启后自动生效。当然直接动根目录的文件有风险建议在有快照或备份的前提下操作。如果没把握宁可把pip缓存目录和虚拟环境目录单独迁走也别轻易重排系统盘目录。4.4 虚环境与目录规划一劳永逸的思路与其每次等报错才清理不如一开始就把目录规划好。我在新服务器上的习惯是这样的数据盘挂载到/data下面建/data/cache和/data/envs。pip缓存指到数据盘通过环境变量PIP_CACHE_DIR/data/cache/pip配置或者写在pip.conf里。虚拟环境统一放到/data/envs/project。/tmp继续用系统盘但定时清理。项目代码放在/data/projects/project。这样即使系统盘临时满了pip的下载缓存、虚拟环境都还在独立分区上最多是/tmp受影响用TMPDIR/data/tmp就能绕过去。再配合下面的定时清理基本能和这条报错说再见了。5. 那些年我们踩过的假空间坑inode耗尽与文件删除不释放5.1 明明还有容量却写不了文件inode耗尽的真相有一次在生产服务器上排查类似问题时我发现df -h显示根分区只用了64%但pip就是报OSError。当时一头雾水直到我敲了df -i才发现根分区的inode使用率已经100%。这个坑的隐蔽之处在于inode耗尽通常都是因为产生了海量小文件。常见来源包括Python的__pycache__目录每个.pyc文件都要占inode程序异常生成的空文件或临时文件邮件系统的/var/mail里堆积了大量日志邮件某个应用把每行日志都写进单独的小文件。查询占用inode最多的目录可以用for d in /*; do echo $d $(find $d -xdev -type f | wc -l); done | sort -k2 -rn | head或者用find / -xdev -type f -size 0 2/dev/null | wc -l当时我发现某工作目录下有几万个空文件全是失败的下载任务留下的残留。把那些空文件清掉之后inode使用率立刻回落到40%pip安装也顺利通过了。所以遇到空间报错一定要把df -i当成必备检查项而不是只看容量。5.2 删了文件空间却不释放被进程占用的已删除文件另一种常见情形是你用rm删掉了一个大文件df -h却显示空间根本没变化。原因是某个进程还在持续占用着这个文件文件描述符一直开着。在Linux下你删除文件只是删掉了一个链接真正的文件数据块要等所有打开它的进程关闭后才会释放。查找这种凶手进程的方式是lsof | grep deleted我当时排查一个web服务时发现它的日志文件被rm -f删除但进程没有重新打开新文件导致旧文件一直占着磁盘。当时报错的就是pip因为写完日志之后系统已经满了pip新建任何文件都会失败。处理方式很简单重启那个服务或者发信号让它重新打开日志文件即可。这种问题在高频写入日志的服务上尤其容易发生建议编写日志轮转脚本用copytruncate方式切分日志而不是直接删除正在使用的文件能有效避免空间不释放。5.3 OverlayFS与容器场景宿主机的空间才是真空间在Docker容器里跑pip时这条OSError还有一个特殊背景容器内的写操作会写入容器的可写层而可写层最终占用的还是宿主机磁盘。容器内执行df -h看到的是宿主机分区的整体使用情况但如果你把容器挂载了外部卷那外部卷的容量又是一回事。更有迷惑性的是很多CI流水线里构建容器会反复下载完整的wheel包构建缓存堆积在/var/lib/docker目录下。时间一长宿主机根分区就会被Docker镜像层占满。你在容器里看到df -h是80%但宿主机可能是97%。所以排查时一定要回到宿主机的视角用docker system df或者直接du -sh /var/lib/docker看看到底谁在吞噬空间。另外一个容器相关的坑是/dev/shm太小。某些应用的运行库和数据集加载会用到共享内存默认的64MB往往不够报错看起来都像磁盘问题。如果确定是共享内存不足可以用docker run --shm-size2g ...或者调整容器编排的shm_size参数就能绕过这类假性空间不足。6. 预防优于救火可落地的监控与配置建议6.1 定时清理任务与pip配置在经历过几次深夜被线上告警吵醒之后我养成了在每台服务器上做预防措施的习惯。首先为pip做全局配置限制缓存和超时行为。下面是我的/etc/pip.conf[global] no-cache-dir true timeout 60 retries 3如果你希望保留缓存但限制体积可以写一个定时任务每周清一次缓存0 3 * * 0 find /root/.cache/pip -type f -mtime 7 -delete 2/dev/nullfind /tmp -type f -mtime 7 -delete 2/dev/nullfind /var/tmp -type f -mtime 7 -delete 2/dev/null我自己的crontab里会写0 4 * * * pip cache purge /dev/null 21 15 4 * * * find /tmp /var/tmp -type f -mtime 7 -delete 2/dev/null这几个任务看着简单但足以避免九成以上的空间不足突发事件。注意别去清理正在使用的socket文件或锁文件所以-mtime 7这个条件很重要。6.2 磁盘空间与inode的监控告警监控不能只看容量使用率inode使用率同样重要。我常用的监控逻辑是当任一分区使用率超过85%触发警告超过95%触发紧急告警任意分区inode使用率超过90%触发警告inode使用率超过95%触发紧急告警。如果你用Prometheus可以直接采集node_filesystem_avail_bytes和node_filesystem_files_free这两个指标。没有统一监控平台的话cron配合df也能实现简单的告警#!/bin/bash df -h | awk NR1 {print $5, $6} | while read used mount; do usage${used%\%} if [ $usage -gt 90 ]; then echo WARNING: $mount used ${used} | mail -s Disk space alert opsexample.com fi done df -i | awk NR1 {print $7, $8} | while read used mount; do usage${used%\%} if [ $usage -gt 90 ]; then echo WARNING: inode usage ${used} on $mount | mail -s Inode alert opsexample.com fi done脚本虽糙但管用。至少能在问题变成生产事故前给你留出一段缓冲时间。6.3 长期规划镜像瘦身、日志轮转与依赖管理最后聊几个长期规划的思路。依赖安装策略给生产构建使用--no-cache-dir避免在构建机或CI节点上留下无用的缓存。同时锁定依赖版本避免每次构建都尝试拉新版本导致下载量激增。Docker镜像瘦身多阶段构建能把运行时镜像从几个GB压到几百MB。扔掉构建期工具和中间层既能降低镜像仓库压力也能减少宿主机磁盘占用间接避免pip安装时的空间告警。我见过一个团队把Python服务的镜像从1.8GB压到420MB效果立竿见影。日志轮转用logrotate确保日志文件按天或按大小切分并保留有限份数。不要手动rm -rf正在写入的日志尽量用copytruncate。依赖缓存服务如果团队规模大、依赖下载频繁可以在内网部署一个简单的wheel镜像服务器比如结合pip download做离线环万这样既加快安装速度又能把缓存集中到一个有足够空间的分区上避免每台机器各自的缓存重复占用磁盘。我自己在实际运维中的体会是这条OSError报错虽然刺眼但它的排查逻辑非常固定——先分清楚是块空间还是inode再看pip和临时目录落在哪个分区然后清缓存、清临时文件、腾退虚拟环境最后用定时任务和监控把风险摁住。没有一个步骤是玄学只要你按这个链路走一遍基本都能在五分钟内找到病根。最后再分享一个小技巧以后遇到任何Could not install packages due to an OSError开头的问题别急着搜pip安装报错先敲df -h再敲df -i这两个命令能帮你排除掉一半以上的故障源。剩下的一半就在/tmp、pip缓存和Docker构建缓存之间。把它们看好这条报错就会从你的黑名单里彻底消失。