ARTICLE DETAIL

资讯详情

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

磁盘满导致服务假死?从df -h排查到日志清理的完整治理指南

磁盘满导致服务假死?从df -h排查到日志清理的完整治理指南 半夜被告警吵醒的时候我脑子里已经把代码里可能出问题的点过了一遍甚至开始怀疑是不是新上线的版本有内存泄漏。结果登录服务器一看进程活着、端口在监听、CPU和内存都正常——就差没当场给用户道歉说我们正在排查了。最后让我闭嘴的是一行极其普通的命令df -h。根分区 100%连写日志的空间都没有了。服务器没挂是我的文件太胖了。准确地说是一堆日志、Binlog、临时文件和容器垃圾把磁盘塞到一只脚都迈不出去。这篇文章就把这次完整的排查和治理过程复盘出来从怎么确定是磁盘问题到怎么找到元凶再到怎么清理和预防一条线讲清楚。1. 先别急着怀疑代码一场典型的磁盘满假故障1.1 进程、端口、负载全正常但服务就是假死那次故障的现象特别迷惑用户侧反馈接口超时部分请求直接报 502登录管理后台也转圈。我第一反应是应用层出了问题赶紧 SSH 上去ps aux一看 Java 进程还在netstat -tlnp一看 8080 端口也监听着top看 CPU 不高、内存还剩不少网络也没有异常。那一刻确实懵了一下这看起来就是一个健康的服务为什么用户那边全线崩其实这就是磁盘满最典型的表现服务本身没死但它什么都干不了。Java 应用需要写日志、写临时文件、做磁盘缓存当磁盘空间为 0 时每写一次日志都会报No space left on device线程卡在文件 IO 上数据库要写 Binlog、写临时排序文件磁盘满了直接拒绝写入连接池被快速耗尽Nginx 要写 access.log写不进去时它的表现也未必是当场崩而是 worker 进程卡在日志写入上最终表现为大面积超时。这个现象之所以容易误判是因为进程存活和服务可用是两回事。进程活着只能说明操作系统还在调度它并不代表它依赖的磁盘、网络、依赖服务都健康。所以遇到服务假死先看一眼磁盘再怀疑代码能省掉很多自我怀疑的时间。1.2 我现在排查的第一步df 永远排在业务日志前面经历过那晚之后我的排查顺序就固定下来了任何服务异常先在服务器上跑这几个命令检查对象命令判断点磁盘空间df -h有没有分区达到 80%、95%、100%inode 数量df -iinode 有没有耗尽即使空间还有剩余系统负载uptime/top是不是 CPU 跑满或负载异常内存/交换free -h内存是不是耗尽导致 OOM 或 swap 抖动文件描述符ulimit -n配合lsof统计句柄是否耗尽我见过最容易被忽略的就是df -i。有一次某个服务报磁盘写入失败同事查了半天硬件和权限最后发现是df -h明明还有 20G 空间但df -i已经 100%因为服务器某个目录里存了几百万个小文件把 inode 全部吃光了。空间还有但新文件根本建不出来。所以现在只要报写不了文件上传失败日志没滚动我都是先跑df -h再跑df -i两个一起看。确认不是存储层的问题才进入应用日志排查。2. 文件太胖的诊断链路从知道满到找到最肥的那一坨2.1 空间满和 inode 满是两种满别混在一起查很多人一看到磁盘满第一反应就是找大文件删。但如果满的是 inode找大文件是没有用的因为大文件只占 block 不占 inode真正占 inode 的是几百万个几 KB 的小文件。我习惯用一个类比来区分磁盘空间好比仓库的面积inode 好比仓库里的货架编号。仓库再大货架编号用完了新货也进不来。判断方法很简单df -h看空间使用率关注/dev/sda1之类的挂载点还有多少 Avail。df -i看 inode 使用率关注 IUsed 和 IFree。如果df -h显示 100% 而df -i正常那就是货太胖如果df -h正常而df -i100%那就是货太多。小文件重灾区的常见位置包括邮件服务器队列/var/spool/mail、消息队列积压、PHP session 目录、Java 应用生成的临时缓存目录以及 Docker 的 overlay2 层。定位 inode 消耗大户可以用find / -xdev -printf %h\n 2/dev/null | sort | uniq -c | sort -k1 -n | tail -20这条命令会统计每个目录下的文件数量把最离谱的目录列出来。注意-xdev表示不跨文件系统避免扫到/proc、/sys这些虚拟目录。2.2 用 du 从根目录逐层往下挖别在整块盘上瞎扫确认是空间满之后下一步就是找最肥的目录。常规做法是从根分区往下逐层统计命令组合如下sudo du -x -h --max-depth1 / 2/dev/null | sort -h--max-depth1只统计根下第一层子目录先看/var、/home、/usr、/opt哪个占得最多然后对最大的目录继续深入比如sudo du -x -h --max-depth1 /var 2/dev/null | sort -h sudo du -x -h --max-depth1 /var/log 2/dev/null | sort -h这个从顶往下逐层砍的方式看起来笨但实际上最快因为避免了在一整块盘上做全量扫描。-x参数也很关键只统计当前文件系统避免 du 跨到其他挂载盘导致统计结果虚高。这里提个性能上的经验du 在文件特别多的时候也会造成一定的磁盘 IO生产环境高峰期不要对着整个/跑尽量从怀疑的目标目录比如/var/log、/home开始。真要在整块盘上扫建议放到业务低峰期或者用ionice -c3把 IO 优先级调低再去跑。2.3 用 find 直接抓大文件嫌疑人du 看的是目录总大小但最终清理还得落到具体文件。定位到可疑目录之后用 find 按文件大小直接列出候选人find /var/log -xdev -type f -size 500M -exec ls -lh {} \; | sort -k5 -h-size 500M的意思是找出大于 500MB 的文件-exec ls -lh把结果以人类可读格式列出来sort -k5 -h再按大小排序。这个组合在服务器上非常实用无论是日志目录、缓存目录还是上传目录一把梭就能看到谁最肥。如果怀疑是近期突然涨起来的还想按时间维度筛选可以加上修改时间条件find /var -xdev -type f -size 100M -mtime 3 -exec ls -lh {} \;这条命令会找出/var下最近 3 天没有修改过的大文件。为什么要找最近没修改过的大文件因为一个正在被活跃写入的日志文件直接处理可能有风险而几天没动过的大文件大概率是归档日志、镜像备份、转储文件这类可以安全清理的东西。2.4 幽灵文件rm 之后空间为什么还是不释放还有一种特别容易让人怀疑命令是不是失效了的情况你删了一个很大的日志文件df -h一看空间还是满的。这不是删除失败而是文件被某个进程占用了。Linux 下删除文件是删除目录项但如果进程还持有这个文件的文件描述符文件内容就不会真正释放直到进程关闭它。也就是说rm只是把文件名摘掉了文件本体还在被进程抱着。这时候用lsof看被删除但未被释放的文件lsof L1 lsof | grep deletedL1会列出 link count 为 0 但仍有进程打开的文件通常就是这类幽灵。我遇到过的典型场景Java 应用打开一个大日志文件运维直接rm了它但应用进程一直不关磁盘空间卡着不动直到应用重启才突然释放出几十 G。处理方式不是反复 rm而是确认哪个进程占用然后平滑重启服务或 kill 掉对应的进程。反过来也给我们一个教训清理正在被进程写入的日志时不要用 rm用清空的方式后面详细说否则就可能制造一个删了又不释放的幽灵文件。3. 盘点那些最容易变胖的文件先长后胖的都有谁3.1 应用日志箱catalina.out、nohup.out、access.log服务器上最常见的大胖子就是日志尤其是有 Java 后台的服务器。Tomcat 的catalina.out是一个几乎只涨不缩的文件默认不会自动轮转运行一年两年的老服务几十 G 甚至上百 G 都有可能。还有用nohup java -jar app.jar nohup.out启动的进程nohup.out 同样会无限增长。Nginx 访问日志也经常失控。压测、日志策略没配好、或者业务被恶意刷接口时access.log一天就能写几个 G。我记得有一次排查一台磁盘告警的服务器最后发现就是 Nginx 的访问日志一个月没轮转涨了 40 多 G而业务本身根本没多大。这类文件的特点是单个文件就很大、增长快、基本不需要留太久但很多人根本没意识到它在疯狂增长。3.2 数据库 Binlog 与归档清理时要讲规矩MySQL 的 Binlog 是另一个重量级选手。默认情况下 Binlog 会保留在数据目录下文件名类似mysql-bin.000001、mysql-bin.000002如果开启了 GTID 或者做过主从复制保留时间会更长。有些库写入量大一天就能产生几十 G 的 Binlog如果过期配置没生效Binlog 堆积起来甚至比数据库本身还大。查看当前 Binlog 情况SHOW BINARY LOGS; SHOW VARIABLES LIKE binlog_expire_logs_seconds; SHOW VARIABLES LIKE expire_logs_days;MySQL 8.0 用binlog_expire_logs_seconds控制过期时间单位秒比如设置 7 天SET GLOBAL binlog_expire_logs_seconds 604800;注意这个 8.0 里的SET GLOBAL只对当前实例生效要持久化得写到配置文件my.cnfbinlog_expire_logs_seconds 604800手动清理已经堆积的 Binlog 用PURGE BINARY LOGS比如删掉某个时间点之前的PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;但务必注意主从环境下不要随便手动删除要确认从库已经拉取完否则会导致主从同步断裂。PostgreSQL 的 WAL 日志也有类似问题归档目录pg_wal或archive如果异常堆积往往是归档命令失败或者wal_keep_size设置太大背锅的常常是备份策略而不是数据库本身。3.3 Docker 容器与虚拟化的隐藏分区用 Docker 的服务器磁盘胖子往往藏在/var/lib/docker。一个很反直觉的现象你在容器里执行df -h发现只有 10G但宿主机的/var/lib/docker可能已经吃掉了整个根分区。最常见的元凶是容器日志。/var/lib/docker/containers/容器ID/*-json.log如果没做轮转理论上可以无限增长。其次是镜像层和悬空镜像频繁构建、反复 pull 镜像后旧镜像层不会被自动清理时间一长积攒几十 G 很常见。用docker system df可以看到各类资源的占用用docker system prune可以一键清理不再使用的镜像、容器和缓存比如docker system df docker system prune -a --volumes--volumes会把没有容器使用的匿名卷也删掉这是空间回收的重要来源。但要提醒一句docker system prune是批量清理执行前务必确认没有需要保留的停止容器和本地镜像不然想找回来就麻烦了。虚拟化平台同理虚拟机模板、ISO 镜像、快照文件经常被人遗忘在存储池里一个快照几十 G比正常运行的虚机还占地方。3.4 上传目录、临时文件与 Core Dump除了日志和数据库还有几类容易被漏掉的隐性胖子用户上传目录业务系统允许传图片、附件但没做清理策略几年下来就上 T/tmp和/var/tmp程序崩溃产生的临时文件、安装包残留经常一堆堆在那边Core Dump进程崩溃时系统把内存镜像写到磁盘文件大小等于进程占用内存大小几个 G 一个很常见默认路径通常是core或/var/lib/systemd/coredump包管理器缓存/var/cache/aptUbuntu/Debian、/var/cache/yumCentOS更新安装几次系统后缓存也能到几个 G。排查时可以用 find 按大小和时间找比如find / -xdev -type f \( -name core.* -o -name *.hprof -o -name *.dump \) -size 1G -exec ls -lh {} \;*.hprof是 Java 的内存堆转储文件出过一次 OOM 就能生成几个 G平时根本不会有人注意到它存在。这类文件安全系数最高确认它不是排查故障需要的现场之后可以直接删。4. 给文件瘦身的实操清理动作与避坑4.1 清空日志的正确姿势不是 rm而是 truncate针对正在被进程写入的日志文件正确的清空方式是保留文件、把内容截断而不是删除文件重来。因为删除文件后进程仍然持有旧文件句柄空间不会立刻释放而且很多应用比如某些版本的 Java 服务并不感知日志文件被删后续日志会继续写入那个已删除的 inode看起来就像日志突然消失了。推荐两种方式# 方式一shell 重定向清空 /var/log/application/error.log # 方式二truncate 截断 truncate -s 0 /var/log/application/error.log这两种方式效果一样文件还在原路径大小变成 0进程句柄仍然有效后续日志继续写入。空间立刻释放日志目录也不会出现一个被删但未释放的幽灵文件。如果是那种写入后不再需要的归档日志比如轮转后的.log.1、.log.2这时候才用rm删除。原则就是正在被写的用 truncate确定没进程用的才 rm。4.2 配置 logrotate把随手清理变成自动机制临时清理只能管一阵子真正解决日志膨胀得靠logrotate。它是系统自带的日志轮转工具几乎每个 Linux 发行版都有。比如给 Nginx 写一个轮转配置在/etc/logrotate.d/nginx里/var/log/nginx/*.log { daily rotate 14 compress dateext missingok notifempty sharedscripts postrotate [ -f /var/run/nginx.pid ] kill -USR1 cat /var/run/nginx.pid endscript }简单解释各项含义daily每天轮转一次rotate 14保留 14 个旧日志超过的自动删除compress旧日志压缩成 .gzdateext用日期命名轮转后的文件missingok日志文件不存在时不报错postrotate轮转后执行重载日志句柄的动作Nginx 通过发送 USR1 信号重新打开日志文件。写好配置后用logrotate -d /etc/logrotate.d/nginx做一次 debug 测试确认配置没问题。这套机制的关键在于让系统每天自己轮转而不是等磁盘满了再人工去救火。Java 应用如果用的是 log4j2、logback优先用框架自带的 RollingFile 策略Tomcat 的 catalina.out 则可以通过配置或 logrotate 双管齐下。4.3 数据库日志自动过期与手动清理数据库的日志清理第一原则是配置自动过期而不是等涨大了再去手动删。MySQL 8.0 已在前面提过这里重点说下实操时容易踩的坑。有些时候你改了binlog_expire_logs_seconds但 Binlog 还是没有按预期被清理常见原因有两个一是从库的 IO/SQL 线程拉取进度落后主库认为 Binlog 还不能删二是 MySQL 要保留至少一个 Binlog 文件所以即便全部过期也会留一个。排查时可以看SHOW SLAVE STATUS\G里的Master_Log_File和Read_Master_Log_Pos如果从库落后很多先把主从追平Binlog 才会正常过期。手动清理时优先用 SQL 而不是直接用rmPURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;如果在从库上误删了还没有被拉取的 Binlog同步就会报错后果比磁盘满还要麻烦。所以我的原则是数据库文件的任何手动删除都要先确认复制拓扑再动手。4.4 容器日志的日常上限配置Docker 容器日志的治理核心是在daemon.json里配日志轮转参数。在/etc/docker/daemon.json里加上{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }配置的意思是每个容器日志单文件最多 50MB保留 3 个文件超过就轮转。改完之后执行systemctl restart docker重启 Docker 守护进程新创建的容器就会生效。注意这个配置对已经存在的容器不会自动生效存量容器需要 recreate 或者手动 truncate。对已经特别大的容器日志手动清空truncate -s 0 /var/lib/docker/containers/容器ID/容器ID-json.log在容器不重启的情况下空间立即释放日志写入不受影响。这个操作我在生产上用过很多次是目前最省事的容器日志瘦身方式。4.5 时间与大小双维度的大扫除应急清理时我习惯按时间 大小两个维度同时筛避免误删。比如清理 30 天以前、超过 100MB 的日志类文件先查出来看看再删find /var/log -type f -name *.log* -mtime 30 -size 100M -exec ls -lh {} \;确认无误后把-exec ls -lh换成-deletefind /var/log -type f -name *.log* -mtime 30 -size 100M -delete生产环境做批量删除前更稳妥的做法是把目标文件先移动到 /tmp 或者专门目录观察几天确认业务没有报缺失再真正删除。虽然麻烦但比起删错文件要跑一个通宵去恢复这点谨慎完全值得。5. 别只做消防员让磁盘回到监控里5.1 一个不到 30 行的磁盘监控脚本清理完磁盘如果不做监控过几个月又会回到原点。我习惯给每台服务器放一个简单的磁盘监控脚本检查空间和 inode 使用率超过阈值就告警。脚本内容大致这样#!/bin/bash # 磁盘空间与 inode 使用率检查 THRESHOLD85 MOUNT_POINT/ HOSTNAME$(hostname) usage$(df -h $MOUNT_POINT | awk NR2 {print $5} | sed s/%//) inode_usage$(df -i $MOUNT_POINT | awk NR2 {print $5} | sed s/%//) if [ $usage -ge $THRESHOLD ]; then echo [$(date %Y-%m-%d %H:%M:%S)] $HOSTNAME 根分区空间使用率 ${usage}% 已超阈值 /var/log/disk-check.log fi if [ $inode_usage -ge $THRESHOLD ]; then echo [$(date %Y-%m-%d %H:%M:%S)] $HOSTNAME 根分区 inode 使用率 ${inode_usage}% 已超阈值 /var/log/disk-check.log fi配合 crontab 每 10 分钟执行一次*/10 * * * * /usr/local/bin/disk-check.sh告警方式可以按团队习惯接钉钉、企业微信或者邮件核心是在空间到 100% 之前就把问题暴露出来。我自己的经验是阈值设在 85% 左右比较合理太高了告警到处理的时间太短太低了又容易误报刷屏。5.2 分区规划比事后清理更治本如果服务器还能重装或者重新规划我强烈建议在装系统时就把日志、数据、临时目录单独分区比如挂载点用途独立分区的意义/系统、程序系统盘被日志占满时不会影响数据盘/var日志、缓存、邮件队列日志膨胀被限制在这个分区/home用户上传、业务数据数据量增长独立可控/opt应用、Docker 数据方便对容器数据单独做配额单独分区的好处是某个分区满了只影响它挂载的那部分业务不会一荣俱荣、一损俱损。比如把 Docker 数据目录放到独立盘就算/var/lib/docker塞爆根分区依然健康系统依然能够登录、执行运维操作。紧接着可以配合quota做目录配额对每个业务线限制磁盘用量从根上防止一个业务把全公司的磁盘吃光。5.3 别把空间满误判成权限问题最后再聊一个容易混淆的排查点当服务报写入失败、无法创建文件时到底是磁盘满、inode 满、文件系统只读还是文件权限问题我的排查口诀是先 df再 df -i然后 mount最后才是权限。空间满df -h直接说明报错通常是No space left on deviceinode 满df -i是 100%报错同样是No space left on device但df -h有空间文件系统只读mount查看挂载选项如果是 ro报错是Read-only file system权限问题ls -l和chmod/chown排查报错是Permission denied我见过不少同事在空间满的时候去 chmod/chown 文件折腾半天没效果。其实报错信息已经把答案写得很明确了No space left on device和Permission denied是完全不同的两类问题。权限修复本身不复杂确认归属用户和业务进程一致就够比如chown -R appuser:appuser /data/app chmod -R 755 /data/app但千万别在空间满的时候做权限操作容易掩盖真正的问题。先把存储层治理好权限问题通常会自己浮出水面。经历过几次服务器假死其实是磁盘满的故障后我现在看服务异常的第一反应早就不是翻代码了而是先看监控面板里的磁盘使用率。所有自动化运维工具、监控告警、日志轮转本质上都是在为磁盘瘦身这件事兜底。文件太胖不可怕可怕的是它在没人看见的地方悄悄长胖直到把整台服务器撑到动弹不得。每次清理完空间我都会顺手把日志轮转配置和监控阈值检查一遍这个习惯救过我很多次。
返回列表