ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux服务器磁盘爆满排查与清理:从df到inode、lsof实战指南

Linux服务器磁盘爆满排查与清理:从df到inode、lsof实战指南 上周五下午我这边一台跑着应用服务的 Linux 服务器磁盘使用率冲到了 98%随后监控告警、业务方投诉一起涌过来。我登录上去先执行df -h发现/data分区已经红了再用df -i看了一眼 inode 使用率也已经到临界值。这种时候最忌讳的是看到某个大文件就随手rm -f删错了可能比磁盘满更麻烦。这篇文章就把我这些年处理 Linux 服务器磁盘爆满的排查链路、常用命令和清理动作完整写下来给遇到同样问题的运维和开发朋友做参考。文章面向的是需要自己处理服务器问题的后端开发、运维工程师也包括刚开始学 Linux 的新人。我会按照“先确认满的性质 → 再逐层定位大文件 → 再排查隐藏空间黑洞 → 最后做清理和长期预防”的顺序来写每一步都给出可以直接复制执行的命令并解释为什么这样做。1. 确认“满”的具体含义空间占用还是 inode 耗尽1.1 df -h 和 df -i 必须同时看很多人拿到磁盘爆满的告警后第一反应就是跑df -h看到某个分区 Use% 到了 100%就开始在目录里翻大文件。但这里有一个很容易踩的坑df -h看的是磁盘块空间占用而 Linux 上还有一个同样会导致“磁盘满”的资源叫 inode也就是文件索引节点。你可以把磁盘空间理解为仓库的货架面积inode 理解为货架上的编号标签。货架面积再大如果编号标签用光了新到的货物也摆不进去。对于 ext4、xfs 这类文件系统每个文件或目录都要占用一个 inode如果服务器上的小文件特别多比如程序生成的临时文件、缓存碎片、消息队列的持久化消息就非常容易出现“块空间还有不少但 inode 已满导致读写报错”的情况。排查时我会把两条命令同时执行df -h df -i我的习惯是看输出里的 Mounted on也就是挂载点。比如/data分区/dev/sdb1的df -h显示已经 100%同时df -i显示 IUse% 只有 30%那基本可以确定是块空间满。反过来如果df -h显示 60%df -i显示 100%那就是 inode 耗尽需要找的是“大量小文件所在目录”而不是单一的大文件。这里还要提一个容易混淆的细节df显示的 Use% 可能还会出现大于 100% 的情况比如 102%这通常发生在 ext4 文件系统的预留块被抵扣、或者异常删除后统计延迟导致的。此时不要慌先确认业务是否真的在报磁盘写入错误再继续排查。1.2 找到真正影响业务的挂载点df -h输出里通常会有多个挂载点比如/、/boot、/data、/var等。有时候某个分区满了但业务根目录在另一个分区互相之间未必有直接关联。所以关键是先弄清楚“哪个挂载点是影响当前业务的那个”。举例来说如果业务数据写在/data下但 MySQL 的 binlog 写在/var/lib/mysql下而/var是独立分区那你说“磁盘满了”到底是/data满还是/var满处理方向完全不同。我在排查时习惯先问自己三句话告警说的是哪个挂载点业务进程的工作目录、数据目录、日志目录分别在哪个挂载点这些挂载点之间的容量关系是什么样的如果业务没有明确指向那就直接看df -h输出里哪个分区 Use% 最高同时看/根分区是否还有剩余。很多时候应用安装在根分区数据在数据分区日志又可能被 logrotate 转到/var/log这三者的占用都要心里有数。还有一个容易忽视的挂载点/dev/shm。它默认是 tmpfs大小约为物理内存的一半如果某些程序把临时文件写到/dev/shm同样会体现在df -h中。它不是磁盘而是内存但清理方式跟普通文件目录不同后面我会单独说明。1.3 先记录现状再看是突增还是缓慢上涨不要一上来就删除。删除是不可逆操作至少先花一分钟记录现场。我会按下面的顺序做快照date df -h df -i du -sh /data/* 2/dev/null | sort -rh | head -20这样做的目的有两个一是确认到底哪些目录占用靠前二是给后续“清理之后是否有效”留一个对比基线。如果磁盘是从几天前就开始缓慢上涨还是今天突然跳满这决定了排查的方向。如果是突然跳满优先怀疑日志文件、临时目录、core dump、被进程持续写入的大文件如果是缓慢上涨则优先怀疑业务数据增长、数据库 binlog、备份文件未清理、Docker 镜像堆积。看文件修改时间可以辅助判断find /data -xdev -type f -mtime -1 -size 1G -exec ls -lh {} \;这条命令列出/data下最近一天内修改过、且大小超过 1GB 的文件。如果一个文件同时满足“最近在写入”“体积巨大”它大概率就是当前磁盘还在继续涨的直接原因。2. 把大文件找出来的三条路径du、find、ncdu2.1 用 du 逐层下钻比 ls 更准确找到占用最高的分区后下一步是在这个分区内定位大目录。很多新人会用ls -lh看文件大小这其实不够准确。ls看到的是文件逻辑大小而du统计的是文件实际占用的磁盘块大小两者对于稀疏文件、压缩文件、有空洞的文件来说可能差异很大。更重要的是du可以递归统计目录的总占用直接帮你看清哪个目录最“重”。我通常用的第一层命令是du -h --max-depth1 /data 2/dev/null | sort -rh | head -20--max-depth1表示只统计/data下一级目录的总大小然后按大小倒序排。查看结果后进入最大的子目录继续执行同样的命令。这样逐层下钻直到找到真正占用空间的文件或子目录。这里有几个细节需要注意加上2/dev/null可以过滤掉因权限不足产生的错误输出但要注意如果某些子目录连读权限都没有du统计结果会不完整此时需要切换到 root 或调整权限后再查。加上-x参数可以避免跨越文件系统边界。比如/data下挂载了一个独立磁盘不带-x时du会把所有挂载点的占用都算进来容易误导排查方向。du对非常庞大的目录树递归耗时较长如果磁盘上数百万文件建议用--max-depth2先看粗粒度再逐步深入不要直接对根分区做全量递归。2.2 用 find 按大小和时间筛出可疑文件du告诉我们目录大小但还不够精确到具体是哪个文件在占空间。这时用find按大小条件筛选大文件更高效。我的常用组合find /data -xdev -type f -size 2G -exec ls -lh {} \;这条命令找出/data下所有大于 2GB 的普通文件并显示详细信息。如果你怀疑是缓存或日志文件可以把时间条件也加进来find /data -xdev -type f -mtime -3 -size 500M -exec ls -lh {} \;表示最近 3 天内修改过、且超过 500MB 的文件。执行结果里如果出现.log、.out、core.*、*.pid这类名字基本就能锁定目标了。另外提醒一句find的-size 2G是识别文件逻辑大小对于稀疏文件比如某些数据库的预分配文件可能会误判但这类文件极少出现在真的需要清理的场景。更常见的是find会扫描出大量.trash、.cache目录下的历史文件这些通常是安全删除对象。2.3 用 ncdu 交互式浏览适合大目录排查命令行逐层du比较繁琐如果你和我一样更习惯看可视化界面可以装一个ncdu。它在很多发行版仓库里都有# Debian / Ubuntu apt install ncdu -y # CentOS / RHEL yum install ncdu -y安装后直接运行ncdu /data它会先递归统计目录大小然后进入交互界面按大小排列目录方向键可以逐层进入。这个工具在 SSH 终端里用起来非常顺手比反复执行du | sort要直观得多。不过要注意ncdu在目录特别大时初始扫描也要等一会儿而且它统计的也是磁盘占用不是文件大小。还有一点如果你的服务器是内网离线环境装不了这个工具那还是老老实实用du和find完全够用。3. 最隐蔽的空间黑洞句柄未释放、日志与容器3.1 删了文件但空间没回来先查被进程占用的文件句柄有一种特别让人头大的场景你通过du和find找到了一个大日志文件确定它是罪魁祸首于是执行了rm -f但df -h一看可用空间一点没变。这种时候不用怀疑通常是有进程仍然持有这个已经被删除文件的文件句柄。Linux 下文件被删除后只要还有进程打开着它占用的磁盘块就不会被释放直到进程关闭该句柄或进程退出。这类文件不会出现在du或者find的结果里因为它们已经不在目录树中了但会被空间占用计算在内。排查方法是用lsof查看已删除但仍被打开的文件lsof L1L1表示列出 link count 小于 1 的文件也就是删除后仍被打开的文件。输出里会有进程名、PID、文件路径路径后面通常会带(deleted)标记。也可以针对某个分区精确查看lsof L1 /data确认是哪个进程后处理方式有两种优先是让业务方平滑重启该服务进程重启后句柄自然释放如果该进程比较关键不能重启可以通过/proc/PID/fd/逐个确认对应句柄内容但一般最稳妥的还是协商重启时间后操作。这里有一个实践中的经验很多长期运行的服务比如 Java 应用、Nginx、Python 常驻进程如果配置了按天切分的日志但旧日志被外部脚本删掉服务进程仍然持有旧句柄就会造成磁盘“删了白删”的假象。所以在清理日志前最好先确认日志是否被进程打开、日志文件是否配置了copytruncate机制。3.2 systemd-journald 日志在 /var/log/journal 里悄悄膨胀如果/var/log分区或/分区经常被写满一个常见元凶是 systemd 的 journald 日志。journald 会把系统日志和内核日志以二进制形式写到/var/log/journal/默认情况下会持续累积。不像传统的 text 日志跨行处理journal 文件按时间增长到后期一个几百兆甚至几个 G 都不奇怪。先看它占了多少journalctl --disk-usage输出会显示当前日志占用大小和日志文件数量。临时清理可以按容量来journalctl --vacuum-size500M这条命令会把日志总量压缩到 500MB 以内只删除旧日志。如果系统日志不重要也可以直接清空journalctl --vacuum-time7d表示只保留最近 7 天的日志。但要注意--vacuum-size是立即生效的临时操作永久限制 journal 大小需要在配置文件里设置vim /etc/systemd/journald.conf找到SystemMaxUse把它设置成你想要的上限比如 500M去掉注释。修改后重启 journald 服务systemctl restart systemd-journald这种做法对根分区长期稳定非常有帮助。另外如果你用的是 rsyslog 或 syslog-ng 收集远程日志这类旧式日志通常在/var/log/messages、/var/log/secure下配合 logrotate 做轮转即可后面会讲。3.3 Docker overlay2 与容器日志的容量管理现在的服务器上跑 Docker 非常常见Docker 所在的数据目录/var/lib/docker往往是磁盘占用大户。如果你不限制容器日志大小时间久了/var/lib/docker/containers/*/*-json.log会单个达到数十 GB而且这个问题在普通du中一眼就能看到。先整体看一眼 Docker 占了多少docker system df它会显示镜像、容器、卷、构建缓存各自的占用。然后是这几个清理动作容器日志限制最推荐的方式是修改 Docker daemon 配置/etc/docker/daemon.json加入{ log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }这样每个容器日志上限 100MB保留 3 个文件。修改后systemctl restart docker。这个方案对已经存在的容器需要重建才生效但对新容器立竿见影。悬空镜像清理docker image prune -a可以删除没有被容器引用的镜像。执行前确认业务不需要回滚到旧镜像。构建缓存清理docker builder prune可以清理 BuildKit 缓存。对于已经写出来很大的容器日志文件可以直接找到后暂时用truncate -s 0清空文件内容而不是rm。因为容器进程可能还持有句柄删除后空间依然不会释放清空内容可以立刻腾出空间。Docker 的 overlay2 目录里还会有大量临时层目录除非你非常确定某个目录没有容器使用否则不建议手动删除用上面的docker system prune更安全。4. 清完之后如何不再次爆满轮转、监控与回归验证4.1 清理后必须做的验证动作清理完成后不要急着说“搞定了”。我的习惯是至少做三件事第一重新确认磁盘空间和 inodedf -h df -i第二确认业务进程还能正常写入。对应用发一个小请求或者看应用日志是否在持续更新。比如tail -f /data/log/application.log第三观察一段时间确认没有新的异常增长。通常在清理完大日志后我会隔 10 分钟再看一次df -h如果空间使用率又快速上升说明写入源还没找对得回到第 2 章继续排查。这里还要提醒一个细节如果清理时选择了rm而不是truncate要特别注意日志文件所在的服务进程是否持有句柄。正确做法是先确认进程再决定是删除还是清空。对于应用自己正在写的日志最安全的方式往往是truncate -s 0或者直接配置 logrotate 的copytruncate。4.2 用 logrotate 和 systemd 定时器把清理自动化手动清理只能救一时长期稳定还要靠自动化。Linux 自带的 logrotate 是日志轮转的标配你可以在/etc/logrotate.d/下新建一个配置文件针对业务日志设置轮转策略。比如我经常给业务方写的配置/data/log/application.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate }含义是日志每天轮转一次保留 7 份旧日志压缩轮转时复制当前日志并清空原文件。copytruncate适合那些不支持重新打开日志文件的应用它通过复制和清空的方式避免应用进程持有旧句柄导致空间不释放。如果应用支持 signal 通知重新打开日志比如 Nginx可以改用create配合postrotate发送信号这样日志切割更精确。对于 journald前面已经说过设置SystemMaxUse。对于临时目录和缓存我还会用 crontab 定期执行清理脚本0 3 * * * find /tmp -type f -mtime 3 -delete /dev/null 21但这里的定时清理一定要慎重/tmp下某些文件可能是正在运行的进程需要的。更好的方式是引导业务方把临时文件写到单独目录并制定明确的保留周期。4.3 监控告警设置在爆满之前而不是之后说实话我处理过的磁盘爆满故障绝大多数都可以靠一个好的监控系统避免。与其等磁盘 100% 再救火不如在 85% 的时候就收到通知提前处理。监控维度至少包含两个指标块设备使用率按挂载点区分比如/data85% 告警、95% 严重告警。inode 使用率有时块设备空间还没满inode 先爆所以也要单独监控。如果公司没有商业监控平台自己用 node_exporter Prometheus 或者简单的 cron 脚本也可以做到。我写过最简单的告警脚本逻辑THRESHOLD85 USE$(df -h /data | awk NR2{print $5} | sed s/%//) if [ $USE -ge $THRESHOLD ]; then echo disk usage high: $USE% | mail -s disk alarm adminexample.com fi更完善的还可以把 Top10 大目录一起放在告警内容里让处理人一进 SSH 就知道该往哪查。另外基于我个人的实战体会每次磁盘故障处理完我都会花 5 分钟想一下“这个容量是被什么业务消耗的预期增长周期是多少”。如果增长周期很短比如日志每天 10GB那光清理没有意义尽快做轮转和容量扩容才是正路。最后再分享一个我自己的习惯拿到磁盘告警后第一件事先执行df -i和lsof L1这两条命令。前者排除 inode 满后者提前发现是否有已经删除但未释放的句柄。这两条看似简单的检查能省下后面至少半小时的绕路排查时间。
返回列表