上篇写完文件描述符,我在结尾预告了一个"更安静的死法"——磁盘明明没存多少东西,空间却满了。结果话音落下没几天,我自己就撞上了真人版:客户的影像归档系统,凌晨批处理集体失败,报错清一色是 No space left on device(设备上没有剩余空间)。运维小哥打电话过来的时候语气都快哭了:"磁盘四百多个 G 的空地空着,它凭什么跟我说满?"
我到现场先自己摸了一遍。df -h(disk free,看磁盘剩余空间的命令)显示 500G 的盘才用了 47G,空得能跑马;换个目录写文件,还是报同一个错。再用 du(disk usage,统计各目录实际占了多少空间)一层层翻下去,没有任何一个目录异常膨胀。大文件嫌疑、日志暴涨嫌疑、备份写穿嫌疑,挨个排除。排除到最后,我想起上篇 fd 的教训:常规嫌疑人都干干净净的时候,剩下的那个,往往是最没存在感的。
df -h 看的是仓库,df -i 看的是户口本。后者一跑出来,真相大白:IUse%(inode 使用率)100%,八百二十万张卡,一张不剩。
户籍卡这个词不是我随口打的比方。inode(索引节点)就是文件系统给每个文件发的卡——文件多大、谁有权限读、数据实际躺在哪几个磁盘块上,全写在卡上。有个反直觉的细节:文件名压根不住在卡上,它登记在目录里,卡和名字是分开两本账。所以上篇结尾我说"连文件名本身都成了负担",严格讲不严谨,负担是卡,不是名字。而这里真正要记住的规则只有一条:一个文件占一张卡,文件再小,哪怕只有一个字节,卡也照占。卡的总量在格式化那会儿基本就定死了,ext4(Linux 上最常见的文件系统格式)默认按"文件平均个头比较大"来配卡,粗算是每 16KB 空间配一张。于是几百万个小文件涌进来,空间还没看见影儿,卡先发光了。仓库明明有地,户籍科发不出新户口,新货就是进不了场。
接着揪是谁在囤卡。find / -xdev -type f(find 是找文件的命令,-xdev 表示只在当前文件系统里翻,别把挂载的其它盘也捎上)按目录统计文件个数,一路排除,最后停在 /data/queue/retry/:七百多万个几 KB 的小 json,全在里头蹲着。顺便打岔一句,我在那个目录里敲了个 ls,回车之后屏幕僵了快半分钟才吐出结果——八百万个文件,连"点名"这件事本身都是一场小型灾难。
这堆小文件的来历,查完我哭笑不得。半年前上游接口不稳,客户加了个重试逻辑:任务失败就往 retry 目录落一个 .retry 文件,等后面补跑。又配了个清理脚本,说好只删 30 天以前的旧文件。结果脚本里日期匹配写错了一个字符,正则从来没命中过,半年下来一个都没删成。这病阴就阴在它是渐进的:卡一张一张少下去,平时任务照跑不误,谁也察觉不到;等最后一张卡被领走,所有要写文件的环节一夜之间集体塌方,而且塌得毫无征兆——没有任何一条日志会告诉你"卡不够了"。
动手删。直接 rm(删文件命令)那批文件,shell 甩给我一个 argument list too long(参数列表太长——rm 一口气收到七百万个文件名,当场噎住)。换成 find /data/queue/retry -name "*.retry" -delete,让它自己分批慢慢删,删了小半个下午,IUse% 从 100% 掉到 3%,写入立刻满血复活。删完不是完事,监控里我先把 df -i 的采集加进去,跟 df -h 平级,IUse% 超过 80% 就告警;清理脚本也改了,按文件修改时间判断新旧,不再看文件名;上游那个重试逻辑顺手重构成攒够一批再合并落盘,小文件从源头上就少生了一多半。
再说句诚实的话,这病没有一劳永逸的药。有的系统天生就产出海量小文件——邮件队列、PHP 的 session 文件(每个访客一份的会话数据)、图床的缩略图,都是这个体质。遇到这种,要么配定期清理,要么把小文件挪去对象存储,要么格式化的时候用 -N 参数手动把卡多配一些。但改 inode 数量必须重新格式化,属于伤筋动骨的手术,千万别在半夜灵机一动。
再打岔一回:修完当天,我鬼使神差地回自己电脑上也跑了个 df -i,好家伙,Home 目录的卡用掉六成,全是这几年攒下的 node_modules(前端项目的依赖目录,小文件制造大户)和缓存碎片。这病真不止服务器有,你哪天自己的电脑突然喊"磁盘已满"、可照片视频明明没多大,可以想起今天这篇。
这套排查适合谁?只要你管着任何一台跑 Linux 的机器,就把 df -i 记进肌肉记忆,成本就一条命令。要是你平时只在托管平台上写写函数,碰不着文件系统,那这篇当个冷知识看看也行。另外替它说句公道话:xfs(另一种常见的文件系统)的卡是动态发的,基本不会得这病,但 ext4 的存量服务器还多得很,别拿运气赌。
现在再看 fd 和 inode 的区别就清楚多了:fd 是进程手里临时的号牌,进程一死全部上缴;inode 是文件系统的户口,文件不删,卡就永远占着。一个管"打开的东西",一个管"存在的东西"。
你呢?有没有撞见过"df -h 明明有空间、写入却喊满"的灵异现场?最后元凶蹲在哪个目录?评论区聊聊——我赌一大半在 /tmp 和某个忘了清理的队列目录里。
预告:号牌讲过了,户口也讲过了,还有更瘆人的一种死法——进程明明已经死透了,"尸体"却一直留在系统进程表里没人收,越积越多,最后把整台机器拖垮。下篇聊聊僵尸进程(Zombie Process)。