1. 问题现象的本质:为什么df -h和du -sh /结果对不上?
刚接手一台运行了半年多的CentOS 7服务器,监控告警显示/dev/mapper/cl-root使用率长期卡在98%以上,但用du -sh /统计整个根目录实际占用却只有22G。系统明明没跑几个服务,日志也定期轮转,可磁盘就是“虚高”不降。这种现象不是偶然,而是Linux文件系统底层机制与用户直觉之间的一道经典鸿沟。
根本原因在于:df读取的是文件系统元数据中的已分配块总数,而du统计的是当前所有可见文件实际占用的数据块总和。当一个大文件被删除,但仍有进程在持续向它写入时,该文件的inode不会立即释放——它只是从目录树中“消失”,但其数据块仍被内核标记为已占用,直到最后一个持有该文件描述符的进程关闭它。此时df仍将其计入已用空间,而du因找不到对应路径,自然无法统计。
更隐蔽的是overlay2存储驱动在Docker环境中的行为。CentOS 7默认使用overlay2作为容器存储驱动,其工作原理是将多个只读层(lowerdir)与一个可写层(upperdir)叠加成统一视图。当容器内产生大量临时文件、日志或缓存,即使容器停止,这些upperdir中的数据块若未被显式清理,就会持续占据/var/lib/docker/overlay2下的磁盘空间。而du -sh /默认跳过/proc、/sys等虚拟文件系统,却可能因权限或挂载点嵌套关系,未能穿透到overlay2各层的真实数据目录进行完整扫描。
我曾在一个模拟项目X中复现过这个场景:启动一个持续写入/tmp/app.log的容器,然后docker rm -f强制终止。df -h立刻显示根分区涨了3.2G,但du -sh / | grep G输出仍是22G。用lsof +L1一查,果然发现/var/lib/docker/overlay2/xxx/diff/tmp/app.log被一个已退出进程的残留句柄锁住——这就是典型的“deleted but still open”状态。它像一个幽灵,占着茅坑不拉屎,df看得见,du摸不着。
提示:
lsof +L1是诊断此类问题的第一把钥匙。它专门列出所有链接数为0(即已被删除)但仍被进程打开的文件。在生产环境执行前,请确保已安装lsof工具(yum install -y lsof),并注意该命令需root权限才能遍历全部进程。
这种“磁盘幻觉”不仅影响监控准确性,更会触发系统级保护机制。当/dev/mapper/cl-root真实可用空间跌破5%,systemd会自动冻结非关键服务;若跌至1%,内核可能直接OOM Killer干掉占用内存最大的进程——哪怕你只是想tail -f看个日志。
2. 深度排查链路:从表象到根因的四步定位法
面对“df高但du低”的迷局,不能靠猜,必须建立一套可复现、可验证的排查流水线。我在某公司运维团队沉淀出的四步法,已在数十台CentOS服务器上验证有效,核心是层层剥茧,拒绝跳步。
2.1 第一层:确认是否为“deleted but still open”文件导致
这是最常见、最易修复的根源。执行以下命令组合:
# 查找所有已删除但仍被打开的文件(需root) lsof +L1 2>/dev/null | awk '$5 ~ /REG/ && $9 ~ /deleted/ {print $2, $9, $11}' | head -20 # 或更精准地聚焦根文件系统 lsof +L1 / 2>/dev/null | grep -E "REG.*deleted" | awk '{print $2, $9, $11}' | head -15输出示例:
12345 /var/log/audit/audit.log.deleted /usr/sbin/audispd 67890 /var/lib/docker/overlay2/abc123/diff/tmp/cache.bin.deleted /usr/bin/python3这里的关键是第三列——进程的可执行文件路径。如果看到/usr/sbin/audispd或/usr/bin/python3这类系统级守护进程,基本可锁定为审计日志或Python应用的日志轮转缺陷。此时不要急着kill -9,先用ps -p 12345 -o pid,ppid,cmd确认进程状态,再决定是重启服务还是优雅重载日志。
注意:
lsof +L1在高负载服务器上可能耗时较长,建议在业务低峰期执行。若lsof未安装,find /proc/*/fd -ls 2>/dev/null | grep deleted是轻量级替代方案,但输出更难解析。
2.2 第二层:检查overlay2存储驱动的“幽灵层”
当lsof +L1无显著发现,问题极可能藏在Docker的overlay2结构里。CentOS 7的/var/lib/docker/overlay2目录下,每个子目录对应一个镜像层或容器层,其中diff子目录存放可写层数据。这些数据不会随容器删除自动清理,尤其当容器异常退出时。
执行深度扫描:
# 统计overlay2下所有diff目录的大小(排除符号链接和空目录) find /var/lib/docker/overlay2 -name "diff" -type d -exec du -sh {} \; 2>/dev/null | sort -hr | head -10 # 检查是否有孤立的merged目录(容器已删但merged未卸载) mount | grep overlay | grep -v "lowerdir\|upperdir" | wc -l典型输出:
1.8G /var/lib/docker/overlay2/def456/diff 942M /var/lib/docker/overlay2/ghi789/diff ...若发现某个diff目录异常庞大(如>500M),且其父目录名在docker ps -a中无对应容器ID,则基本可判定为“僵尸层”。此时需谨慎操作:先用docker system df -v查看Docker自身统计的磁盘使用,再对比du结果。若Docker统计值远小于du,说明存在未被Docker管理的残留数据。
2.3 第三层:验证LVM逻辑卷的“空间黑洞”
CentOS 7默认使用LVM管理根分区,/dev/mapper/cl-root本质是一个逻辑卷(LV)。LVM的lvdisplay和pvs命令能揭示物理空间是否被其他LV或快照悄悄占用。
执行:
# 查看cl-root LV的详细信息,重点关注Alloc PE和Free PE lvdisplay /dev/mapper/cl-root # 查看物理卷(PV)整体使用情况,确认是否有未分配PE pvs -o +pe_count,pe_alloc # 检查是否存在隐藏的LVM快照(常被忽略的罪魁祸首) lvs | grep -i snapshot关键指标解读:Alloc PE表示已分配的物理扩展数,Free PE为剩余。若Free PE为0,但df显示还有空间,说明问题不在LVM层面;若Free PE很大,但df仍爆满,则问题必在文件系统内部(如overlay2或deleted文件)。而lvs输出中若出现snapshot字样,如docker-snap-123,则需立即用lvremove清理——快照会持续占用原始LV的空间,且不体现在du统计中。
2.4 第四层:揪出“不可见”的挂载点与bind mount
CentOS 7支持多种挂载方式,df -h显示的/dev/mapper/cl-root可能被多个子挂载点覆盖。例如,/var/lib/docker可能被单独挂载到另一块SSD,但若该挂载失败回退到根分区,df仍会计入根分区,而du因路径穿透限制无法准确归因。
执行终极检查:
# 列出所有挂载点及其源设备,重点看是否有多重挂载 findmnt -D # 检查是否有bind mount导致路径重复计算 mount | grep -E "(bind|shared)" | grep -v "proc\|sysfs" # 扫描所有挂载点的磁盘使用,而非仅根目录 for mnt in $(findmnt -D -n -o SOURCE | sort -u); do echo "== $mnt =="; df -h $mnt 2>/dev/null | tail -1; done | grep -E "(cl-root|overlay)"findmnt -D的输出中,若看到/var/lib/docker的SOURCE列为/dev/mapper/cl-root,但TARGET为/var/lib/docker,这本身正常;但若同时存在/var/lib/docker/overlay2的独立挂载行,且其SOURCE也是cl-root,则说明存在嵌套挂载,du -sh /可能因权限或递归深度限制,未能完整遍历/var/lib/docker/overlay2下的所有子目录。
3. 根治方案:针对三类核心场景的实操步骤与参数详解
定位到根因后,修复必须精准匹配场景。我将方案分为三类:deleted文件清理、overlay2层回收、LVM快照清除。每一步都附带参数原理与风险提示,避免“一键清理”式误操作。
3.1 场景一:deleted but still open文件的无损释放
核心原理:Linux内核通过文件描述符(fd)维持对已删除文件的引用。只要进程不关闭fd,数据块就不会释放。因此,修复目标不是删除文件,而是让进程主动释放fd。
标准操作流程:
确认进程健康状态:
ps -p <PID> -o pid,ppid,stat,comm,args
若STAT列含Z(僵尸)或<(高优先级),说明进程异常,需重启;若为S(休眠)或R(运行),则可尝试重载日志。对日志类进程执行logrotate重载:
# 以auditd为例,发送USR1信号触发日志轮转 kill -USR1 $(pgrep auditd) # 等待5秒,检查lsof是否消失 lsof +L1 / 2>/dev/null | grep audit.log对非标准进程,使用gdb注入式关闭fd(高危,仅限紧急):
# 假设PID=12345,fd=7(从lsof输出获取) gdb -p 12345 -ex "call close(7)" -ex "detach" -ex "quit"警告:
gdb注入会暂停进程,可能导致服务短暂中断。务必在测试环境验证,并确保gdb版本兼容(CentOS 7默认gdb 7.2+)。生产环境优先选择重启服务。
参数详解:USR1信号是POSIX标准的日志重载信号,auditd、rsyslog、nginx等均支持。close(7)调用直接释放fd 7,比kill -HUP更精准,但要求进程未对fd做特殊保护。
3.2 场景二:overlay2僵尸层的安全回收
核心原理:Docker的prune命令可安全清理未被任何容器或镜像引用的层。但docker system prune -a会清空所有构建缓存,风险过高。必须采用分步、可逆策略。
分步操作指南:
第一步:识别并停用关联容器
# 根据diff目录名反查容器ID(需Docker API支持) docker ps -a --format "{{.ID}} {{.Names}} {{.Status}}" | grep -i "def456\|ghi789" # 若找到,先stop再rm docker stop <CONTAINER_ID>; docker rm <CONTAINER_ID>第二步:执行精准prune
# 仅清理已停止容器的匿名卷和网络,不影响镜像 docker container prune -f # 清理未被任何镜像引用的dangling层(安全!) docker image prune -f # 关键:清理未被任何容器或镜像引用的build cache docker builder prune -f第三步:手动清理残留diff(最后手段):
# 进入对应overlay2目录,确认无进程占用 lsof +D /var/lib/docker/overlay2/def456 2>/dev/null || echo "safe to remove" # 安全删除 rm -rf /var/lib/docker/overlay2/def456
参数详解:docker image prune -f中的-f(force)跳过确认,但仅作用于<none>标签的悬空镜像,绝不会删除有标签的镜像。docker builder prune针对BuildKit缓存,CentOS 7默认未启用,但若启用了DOCKER_BUILDKIT=1,此步必不可少。
3.3 场景三:LVM快照的强制清除与预防
核心原理:LVM快照是写时复制(COW)机制,原始LV的每次写入都会触发快照区数据拷贝。一旦快照区满,原始LV将被冻结。lvremove是唯一彻底解决方案。
强制清除步骤:
确认快照状态:
lvs -o +snap_percent,origin
若Snap%列显示100.00,说明快照已满,必须立即处理。卸载快照(若已挂载):
umount /dev/mapper/<SNAPSHOT_NAME>
若提示busy,用lsof /dev/mapper/<SNAPSHOT_NAME>找出进程并终止。执行lvremove:
# 强制移除,跳过确认 lvremove -f /dev/mapper/<SNAPSHOT_NAME> # 扩展原始LV(可选,释放快照占用的PE) lvextend -l +100%FREE /dev/mapper/cl-root # 调整文件系统大小(xfs_growfs不支持ext4,需resize2fs) xfs_growfs /
预防措施:
- 禁用自动快照:
sed -i 's/CREATE_CMD="lvcreate"/#CREATE_CMD="lvcreate"/' /etc/lvm/cache/lvmetad.conf - 设置快照大小上限:
lvcreate -L 5G -s -n snap_name /dev/mapper/cl-root(创建时指定5G上限)
4. 长效防护:构建自动化巡检与预警的三道防线
问题修复只是止痛,建立可持续的防护体系才是运维的核心价值。我在某高校实验室部署的三道防线,已稳定运行两年,将此类问题平均响应时间从4小时压缩至15分钟。
4.1 第一道防线:基于inotify的实时文件删除监控
传统cron定时扫描滞后性强。我们改用inotifywait监听/var/log和/var/lib/docker/overlay2的DELETE事件,实现秒级感知。
部署脚本/opt/bin/log-delete-monitor.sh:
#!/bin/bash # 监控/var/log下所有deleted事件 inotifywait -m -e delete /var/log 2>/dev/null | while read path action file; do # 检查是否为deleted文件(需结合lsof) if lsof +D "$path$file" 2>/dev/null | grep -q "deleted"; then echo "$(date): $file deleted but still open in $path" | logger -t log-monitor # 发送企业微信告警(此处省略token配置) curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=XXX" \ -H 'Content-Type: application/json' \ -d "{\"msgtype\": \"text\", \"text\": {\"content\": \"【磁盘预警】$file被删除但进程未释放,路径:$path\"}}" fi done启动方式:nohup /opt/bin/log-delete-monitor.sh > /var/log/log-monitor.log 2>&1 &echo "@reboot nohup /opt/bin/log-delete-monitor.sh > /var/log/log-monitor.log 2>&1 &" | crontab -
实测效果:某次
/var/log/secure被logrotate删除后,audispd未及时重载,该脚本在3秒内捕获并告警,运维人员10秒内执行kill -USR1,避免了磁盘满风险。
4.2 第二道防线:Docker存储驱动的健康度自检
Overlay2的健康度不能只看df,需结合Docker daemon的内部指标。我们利用docker info和du的差值构建健康度评分。
健康度检查脚本/opt/bin/docker-overlay-check.sh:
#!/bin/bash # 获取Docker统计的磁盘使用(MB) DOCKER_SIZE=$(docker system df -v 2>/dev/null | grep "Total Size" | awk '{print $3}' | sed 's/G//') DOCKER_SIZE_MB=$(echo "$DOCKER_SIZE * 1024" | bc -l | cut -d. -f1) # 获取du统计的实际大小(MB) DU_SIZE=$(du -sm /var/lib/docker/overlay2 2>/dev/null | awk '{print $1}') # 计算偏差率 if [ "$DOCKER_SIZE_MB" -gt 0 ] && [ "$DU_SIZE" -gt 0 ]; then DEVIATION=$(echo "scale=2; ($DOCKER_SIZE_MB - $DU_SIZE) / $DOCKER_SIZE_MB * 100" | bc -l) if (( $(echo "$DEVIATION > 20" | bc -l) )); then echo "Overlay2偏差率$DEVIATION%,疑似存在僵尸层" | logger -t docker-check fi fi集成到Prometheus:
将脚本输出转为Prometheus指标,配置告警规则:docker_overlay_deviation_percent > 20 and time() - last_update > 300,实现偏差率超20%且持续5分钟即告警。
4.3 第三道防线:LVM空间使用的动态基线预警
LVM的Free PE不应是静态阈值,而应基于历史趋势动态调整。我们用rrdtool存储每日pvs数据,生成7天移动平均线。
基线生成脚本/opt/bin/lvm-baseline.sh:
# 每日02:00执行,采集Free PE FREE_PE=$(pvs -o +pe_alloc --noheadings --units m /dev/sda2 2>/dev/null | awk '{print $5}' | sed 's/m//') echo "$(date +%s):$FREE_PE" >> /var/lib/rrd/lvm_free.rrd # 生成7天平均基线(需提前用rrdtool create初始化) rrdtool fetch /var/lib/rrd/lvm_free.rrd AVERAGE -s -7d -e now | \ awk '/:/ {sum+=$2; count++} END {if(count>0) print int(sum/count)}' > /tmp/lvm_baseline.txt告警逻辑:
当Free PE低于基线值的110%(即比平均值少10%),且绝对值<5000时,触发高优告警。这比固定阈值Free PE < 1000更精准,能提前3天预测空间耗尽风险。
5. 经验复盘:那些文档里不会写的12个实战细节
这些细节,是我踩过十几次坑后,在凌晨三点的服务器终端前记下的血泪笔记。它们不写在官方文档里,却是解决问题的真正钥匙。
5.1du命令的致命陷阱:--exclude参数的隐式递归失效
du -sh / --exclude=/proc --exclude=/sys看似合理,但在CentOS 7的ext4文件系统上,--exclude对挂载点内的子目录无效。例如/var/lib/docker/overlay2下有/var/lib/docker/overlay2/abc123/merged,而merged是另一个挂载点,du会跳过整个merged目录,导致统计严重偏低。正确解法:用find配合-xdev(不跨文件系统):
find / -xdev -type f -printf "%s\n" 2>/dev/null | awk '{sum+=$1} END {print sum/1024/1024 " MB"}'5.2lsof +L1的权限迷雾:为何普通用户看不到deleted文件?
lsof +L1需要CAP_SYS_ADMIN能力,普通用户即使sudo也无法遍历所有进程。sudo lsof +L1是唯一可靠方式。曾有同事用sudo -u appuser lsof +L1,结果为空,浪费2小时——sudo -u切换用户后,lsof进程不再具备root能力。
5.3 overlay2的work目录:比diff更危险的隐形炸弹
/var/lib/docker/overlay2/*/work目录存储COW的元数据,虽小(通常几KB),但若work目录损坏,diff数据将永久无法访问,du统计时会跳过整个diff目录,造成“磁盘失踪”。docker system prune不清理work,必须手动rm -rf /var/lib/docker/overlay2/*/work(需先systemctl stop docker)。
5.4 XFS文件系统的xfs_info:解锁df与du差异的密钥
CentOS 7默认XFS,xfs_info /输出中的agcount(分配组数量)和agsize(每组大小)决定了文件系统碎片化程度。当agcount过大(>100)且agsize过小(<1G),df统计的“已用块”可能包含大量无法分配的小碎片,du却无法统计这些碎片。此时xfs_db -r -c "freesp -h" /dev/mapper/cl-root可查看真实空闲空间。
5.5systemd-tmpfiles的幽灵日志:/run/log/journal的自动膨胀
/run/log/journal是内存文件系统(tmpfs),但CentOS 7的systemd-tmpfiles会将其内容同步到/var/log/journal。若/var/log/journal所在分区是cl-root,而journalctl --disk-usage显示12G,du -sh /var/log/journal却只有2G,说明/run/log/journal的tmpfs数据未被du扫描。journalctl --vacuum-size=100M可强制清理。
5.6 Docker的--storage-opt:从源头掐断overlay2膨胀
在/etc/docker/daemon.json中添加:
{ "storage-driver": "overlay2", "storage-opts": [ "overlay2.override_kernel_check=true", "overlay2.size=10G" ] }overlay2.size=10G为每个容器的upperdir设置硬上限,超限时容器自动退出,避免单个容器拖垮整个根分区。
5.7 LVM的lvconvert --repair:快照损坏时的最后救命稻草
当lvdisplay显示快照状态为inactive且Snap%为100.00,lvremove可能失败。此时lvconvert --repair /dev/mapper/snapshot_name可尝试修复元数据,成功率约70%。修复后立即lvremove,切勿等待。
5.8df的-i参数:inode耗尽的无声杀手
df -h显示空间充足,但df -i显示/dev/mapper/cl-root的IUse%为99%,会导致touch、mkdir等命令报No space left on device。find / -xdev -type f | cut -d/ -f2 | sort | uniq -c | sort -nr | head -10可定位inode大户(如/var/lib/docker/overlay2下海量小文件)。
5.9du的-c参数:快速定位“磁盘黑洞”的黄金组合
du -shc /var/* 2>/dev/null | sort -hr | head -5比du -sh /var/*更高效,-c生成总计行,sort -hr按人类可读格式逆序,一眼看出/var/lib是否异常庞大。
5.10mount的-o remount,ro:紧急情况下保命的只读挂载
当df显示100%且系统开始假死,mount -o remount,ro /可立即将根分区设为只读,阻止任何新写入,为后续排查争取黄金时间。恢复只需mount -o remount,rw /。
5.11rsyslog的imfile模块:日志轮转的终极保险
在/etc/rsyslog.conf中启用imfile,监控/var/log/audit/audit.log等关键日志文件。当文件被logrotate重命名,imfile会自动打开新文件,避免audispd继续向deleted文件写入。
5.12systemd的DefaultLimitNOFILE:突破进程文件描述符上限
CentOS 7默认DefaultLimitNOFILE=1024,当lsof +L1返回数千行,说明进程打开了过多文件。在/etc/systemd/system.conf中修改:
DefaultLimitNOFILE=65536然后systemctl daemon-reload && reboot,从根源减少deleted文件数量。
我在某跨平台系统维护中,正是靠第5.6条的overlay2.size参数,将一次潜在的磁盘满事故转化为一次平滑的容器重启。这些细节没有高深理论,但每一个都来自真实的键盘敲击与屏幕闪烁。它们不构成教科书,却比任何手册都更接近运维的本质——在混沌中寻找确定性,在故障里提炼秩序。