那天下午三点多,客户给我发消息说网站全挂了,后台一片 502,我第一反应是"服务器被人打了",赶紧登上去看。结果一 df -h 傻眼了:系统盘 100% 满。那台机器只有 40G 系统盘,MySQL 的 binlog、Nginx 的 access log、还有一堆没清的 Docker 镜像日志,全堆在同一个分区里。删文件、清日志、重启服务,前前后后折腾了三个小时才把网站救回来。这件事之后,我给手上所有机器重新做了一遍磁盘分区规划,今天就把这套踩坑和规划思路完整写出来。
正在做服务器数据盘规划、或者担心系统盘不够用的朋友,这两家可以关注一下:狗云DOGYUN(高性价比云服务器,机型多、可单独加数据盘)、RackNerd(海外 VPS 与大存储机型,硬盘容量给得比较足),下单前记得确认系统盘和数据盘能不能分开配置,别像我这台一样全挤在 40G 里。

很多人以为磁盘满了顶多是存不了新文件,其实对跑着网站的服务来说,磁盘一满是一连串的连锁反应。最先出问题的是 MySQL:它需要写 redo log、写 binlog、还要建临时表,磁盘没空间时数据库会直接报错甚至崩溃,PHP-FPM 之类连接数据库的程序跟着一起报错,Nginx 再把这些错误码原样吐给访客,页面就变成了 502。
其次是日志本身。你以为删日志能救命,但很多服务的日志文件正被进程打开着,你用 rm 直接删,磁盘空间根本不会释放文件句柄还在,inode 一直占着,必须 truncate 或者重启对应进程。还有更隐蔽的一种:Docker 默认把容器日志写成 json 文件放在 /var/lib/docker/containers 下面,不做任何轮转,跑上几个月就能悄悄吃掉几十 G。
我那次真正的时间浪费在两个地方:一是逐个目录 du -h 找元凶,二是发现是 binlog 之后才发现忘了开 log_bin_expire_logs_seconds,只能临时用 mysqlbinlog 逐个清文件。要是一开始就知道分区规划的重要性,后面这几十次 ssh 完全可以省掉。
因为绝大多数人买机器的时候,注意力全在 CPU 和内存上,硬盘就随手选了默认的 30G 或 40G。系统盘里装的东西可不少:操作系统本身加内核加系统库就吃掉 5G 左右,MySQL 的数据目录和 binlog、日志、Nginx 的站点文件和缓存、Docker 镜像和容器层、还有各种定时任务产生的临时文件,全都在这一块盘上。
更要命的是增长方式。业务数据是缓慢上涨的,但日志和缓存是「只进不出」的:access log 一天几 MB 看着不起眼,一个带参数的爬虫或者一次被扫的登录页就能把它顶到几个 G;Docker 每次拉新镜像再叠一层旧镜像层,几周下来几十 G 很常见;数据库 binlog 如果没开自动清理,单个文件默认 1G 一个,写满一个换下一个,连着写就是几十个 G。这些都属于"安静地长",等你 df 一看已经晚了。
所以我在给新机器做规划时,定了一个很土但有效的规矩:系统盘只放系统和必须随机器重建的东西,业务数据、日志、备份、镜像缓存,全部挪到数据盘上去。
先说结论:系统盘 40G 起步,纯系统跑个网站绰绰有余;数据盘按业务量给,日志、数据库文件、上传的图片视频、备份,全放数据盘。关键不是给多少,而是分区挂载的思路要分清。
具体的挂载点这么规划:/var/log 单独一个分区,日志涨多少都不至于顶爆系统盘,这是最优先的一条;/var/lib/docker 单独一个,镜像缓存和容器层最会疯长;数据库数据目录如果条件允许也单独挂一个分区,MySQL 恢复和迁移都方便;/home 或者 /data 留给你自己的业务数据,网站上传的文件、备份包、临时下载都往这儿放。
如果机器只有一块物理硬盘却被切成多个分区,那好处只是"逻辑隔离",并不能真正防止物理盘写满这一点很多人搞混了。真正的隔离必须靠两块独立的云硬盘或者两块物理盘。选机器时留意一下商家给不给单独加数据盘、能不能在不重装系统的情况下挂上去,这是分区方案能不能落地的关键前提。
挂载点本质上就是"这块盘在系统目录树里的位置"。一个很常见的事故是:数据盘挂到了 /data,然后你把应用的数据库目录配置还是写着 /var/lib/mysql,结果文件全写在系统盘上;或者反过来,数据盘挂到了 /var/www,结果网站文件在盘上,但日志还在系统盘你以为分开两块盘就万事大吉,其实只分开了一半。
还有一个更狠的:挂载前忘了建挂载点目录就直接 mount,系统会在根目录下创建一个同名目录,看起来"挂载成功了",但原来那个目录里的文件(比如 /home 下的用户数据)就被整个盖住看不见了,数据还在盘上没丢,但程序会以为文件不存在。这属于"藏起来"的故障,比直接报错更难查。挂之前先 mkdir -p 挂载点、挂之后用 df -h 确认挂载关系,再把旧数据 rsync 过去,这三步缺一不可。
这是我另一个被坑过的场景。df -h 显示磁盘还剩 40%,但创建文件报 No space left on device。原因是 inode 用光了。inode 是记录文件属性的数据结构,一个目录、一个文件都要占一个。小文件特别多的场景会很快吃掉 inode:某个程序把缓存拆成几万个几 KB 的小文件、日志目录里塞满临时片段、或者某个爬虫任务狂写小文件,都会触发。
排查命令就一条:df -i,看每个分区的 inode 使用率。如果 Use% 是 100% 而 Blocks 还有空间,那就是它。解决办法不是删文件那么简单,得先找出谁在制造海量小文件:find /var -xdev -type f -printf '%h\n' 2>/dev/null | sort | uniq -c | sort -rn | head,能看到哪个目录下的文件数量最离谱,然后针对它改配置或者合并文件。VPS 这种 inode 数偏少的机型(比如 64G 盘只有几万个 inode),更要提前规划日志切割策略。
先别慌着重启所有服务,顺序错了会丢数据。第一时间执行 df -h 找出是哪个分区满了,再 du -h --max-depth=1 / | sort -h 逐层往下钻,找到最大的那几个目录。常见的元凶我一般按这个顺序查:/var/log 下面的服务日志、/var/lib/docker 的容器层和镜像、数据库的 binlog 和 relay log、以及 /tmp 里没清的历史文件。
处理日志有讲究。最省事的是 journalctl --vacuum-size=200M 清 systemd 日志;应用日志用 : > /var/log/xxx.log 清空而不是 rm;Docker 要 docker system df 先看占用,再 docker system prune 清理没用的镜像和容器,别在生产机器上一把梭哈。数据库 binlog 千万别乱删正在写的那个,用 PURGE BINARY LOGS 或者 mysqlbinlog 清理历史文件才安全。删完之后记得加一条日志轮转的定时任务,比如 logrotate 或者 logrotate+size 轮转,让它以后自己滚动。
我的那次事故最后是靠删掉 30G 的历史 binlog 和 Docker 悬空镜像解决的,前后确实花了三个小时,其中一半时间在找文件。事后我做的唯一改动,就是把所有机器的日志目录挂到了数据盘上。
已经买了一台只有系统盘的机器、现在想加数据盘的,最稳的做法是 rsync 而不是 mv。先把数据盘格式化挂载好(记得写进 /etc/fstab,否则重启后不挂载),然后 rsync -avz --progress 源目录/ 挂载点/,注意源目录末尾的斜杠不能漏,不然会把源目录本身复制进去变成子目录。拷完核对文件数量和 du 大小,再改配置文件指向新路径,重启服务验证访问正常,最后才把旧数据删掉。
整个过程中有两点要特别注意:一是 fstab 写错了会导致系统起不来,写之前先 mount -a 测试一遍;二是迁移期间服务还在写数据的话,rsync 会漏掉最后那部分,所以正式切换前要停服务再 rsync 一次增量(加 --delete 保证一致),然后再启动。这个顺序我走过一次错,拷完就停了服务,结果数据库少了几分钟的订单数据。
再强调一遍:分区做得再漂亮,也不是备份。系统盘和数据盘在同一台机器上,机器一坏、商家跑路、机房出一次故障,两边数据一起没。分区解决的是"某一部分写满了不会拖垮全局"这种问题,属于可用性设计;真正防丢的是快照、定时备份到异地、或者往对象存储上同步一份。这两件事是并列关系,不是互相替代。
顺带提一句,如果你要在自己电脑上处理分区或者看磁盘占用,Windows 下用 MiniTool 分区助手这类工具做分区调整和容量查看,Linux 下 ncdu 这种命令行工具扫目录占用效率很高,都比自己手敲 du -h 递归省事得多。
回头看那三个小时,本质上不是磁盘太小,是我在服务器上留了太多"没有位置的默认值"。日志、镜像、数据库文件全默认往系统盘上跑,等出事了才知道挪。如果你手上的机器还没做过规划,现在花半小时做一遍分区挂载,比下次花三个小时救火划算得多。