「根目录不能写」到底是个什么错误
早上打开网站,首页显示 500,SSH 上去看 nginx 的 error.log,里面是这样的:
2026/10/21 08:14:22 [error] 1832#1832: *99213 open() "/var/www/html/index.php" failed (30: Read-only file system)
或者更让人摸不着头脑的是 PHP 报的错:file_put_contents(): Write of 512 bytes failed with errno=30 Read-only file system。这时候跑一下 df -h,磁盘还有 60% 空闲;ls -l 看文件权限也完全正常;mount | grep " / " 的输出里却可能赫然出现了 ro:
/dev/vda1 on / type ext4 (ro,relatime,errors=remount-ro)
文件系统被挂载成了只读。这是 Linux 运维里非常典型的一类故障,它和「磁盘满」「权限错误」的表现很像(都是写不进去),但根因和处理方式完全不同。更麻烦的是,很多人第一次遇到会本能地 mount -o remount,rw / 然后继续跑,这恰恰是最危险的操作——因为让文件系统变只读的,往往是一个还没解决的底层问题。
为什么内核会把文件系统切成只读
内核不会无缘无故把根分区设成只读。触发这个行为的机制主要有三类,理解它们才能判断该不该马上 remount。
第一类:文件系统检测到不一致。 ext4 在挂载参数里如果有 errors=remount-ro(大多数发行版的默认值),那么当它遇到「无法安全处理的 IO 错误」或者「元数据不一致」时,会主动把文件系统降级为只读,目的是防止损坏继续扩大。这是保护机制,不是故障本身。日志里能找到线索:
dmesg -T | grep -iE 'ext4|I/O error|remount|read-only' | tail -30
典型输出会包含 EXT4-fs error (device vda1): ext4_lookup:...、Remounting filesystem read-only、blk_update_request: I/O error, dev vda, sector ... 这几类。如果看到的是 I/O error,说明底层存储(云盘、物理盘、RAID 控制器)报告了读失败,这时候 remount 成读写很可能在几分钟内再次变只读,甚至直接导致进程崩溃。必须先把存储问题解决掉。
第二类:底层设备真的掉线了。 云服务器上,如果系统盘因为欠费、配额超限、或者宿主机的存储后端故障被摘除,内核会看到设备消失,文件系统自然无法写入。lsblk 里设备还在,但 dmesg 里有一串 device offline、host0: link down。这种情况只能通过云控制台处理,服务器上做什么都没用。
第三类:有人或某个脚本主动 remount 成只读。 比较少但确实存在:一些安全加固脚本、备份脚本、或者误操作会用 mount -o remount,ro /。可以先查一下 journalctl -b | grep remount 和 last -20,看看故障时间点前后有没有人登录、有没有 cron 在跑。
第一步:确认是「只读」而不是「满了」或「inode 用尽」
这三种情况报错文本不同,但站长往往只看「写不进去」就混为一谈。df -h 只看容量,df -i 才看 inode。之前遇到过一台站点的附件目录里有几十万个小文件,容量只用了 40%,inode 却到了 100%,同样写不进去,但报的是 No space left on device。
# 容量和 inode 一起看
df -hT /
df -i /
# 确认挂载参数里是不是真的有 ro
findmnt -no SOURCE,FSTYPE,OPTIONS /
# 或者
awk '$2=="/"{print $4}' /proc/mounts
# 确认挂载是不是只读:直接试写一个小文件
touch /tmp/.rw-test && echo "root writable" || echo "root READ-ONLY"
findmnt 的输出比 mount 干净,一眼能看到 ro 还是 rw。如果显示的是 rw 但依然写不进去,那问题不在挂载状态,回头去查 SELinux/AppArmor、或者目标目录本身挂了另一个只读分区(比如 /var/www 单独挂了一块被设成 ro 的盘)。
还有一种更隐蔽的情况:根分区是可写的,但 /var/www/html 是一个单独挂载的卷,或者它是一个 NFS 挂载。后者在网络中断时会被内核标记为 ro,而且会表现为「读取正常、写入卡住几十秒然后失败」,非常容易被误判成磁盘问题。findmnt /var/www/html 能立刻确认。
第二步:判断能不能立刻 remount 成读写
这一步是分水岭。判断依据只有一条:dmesg 里有没有真正的 IO 错误或者元数据损坏。
如果 dmesg 里干干净净,只有一行 Remounting filesystem read-only 而没有前面的错误,并且你确认没有任何人主动执行过 remount ro,那么大概率是 transient 的 IO 抖动触发了保护机制,可以尝试恢复:
# 1) 先把业务停掉,避免写入竞争
systemctl stop nginx php8.2-fpm mysql
# 2) 尝试重新挂载成读写
mount -o remount,rw /
# 3) 立刻验证
touch /tmp/.rw-test2 && echo OK || echo STILL-RO
findmnt -no OPTIONS /
# 4) 确认没问题再起服务
systemctl start mysql nginx php8.2-fpm
如果 mount -o remount,rw / 报 mount: /: cannot remount /dev/vda1 read-write, is write-protected,那就说明内核拒绝了这次重挂,通常意味着底层设备处于错误状态,只能重启。云服务器上重启前先在控制台确认磁盘状态正常,否则重启后可能连启动都过不去(因为 fstab 检查失败进 emergency mode)。
如果 dmesg 里有 I/O error 或者 EXT4-fs error,不要 remount。这时候正确的操作顺序是:先在云控制台给系统盘打一个快照(哪怕文件系统有问题,快照也能保住当前数据),然后立刻把重要数据从还能读的部分拷出来,最后再考虑修复。用 fsck 直接在挂载状态或者有 IO 故障的盘上跑,失败率很高,而且可能把原本还能恢复的元数据改得更糟。
第三步:抢救数据,从只读状态里把东西拿出来
文件系统是只读的,意味着「读」还能正常工作。这是个好消息——你要做的所有事情都是读:把数据库目录、网站文件、配置打包传到别的地方。
顺序上,数据库优先。MySQL 在只读文件系统上通常已经崩了(InnoDB 无法写 redo log),直接 mysqldump 大概率连不上。这时候分两种情况:如果 MySQL 进程还活着而且能连上,立刻 dump;如果已经挂了,去数据目录做文件级冷拷:
# 情况 A:MySQL 还能连上(只读状态下有时能撑一会儿)
mysqldump -uroot -p --single-transaction --all-databases \
| gzip -1 > /mnt/rescue/all-$(date +%F).sql.gz
# 情况 B:MySQL 已经起不来,冷拷数据目录
systemctl stop mysql || true
tar -C /var/lib -cf - mysql | zstd -3 -T0 > /mnt/rescue/mysql-datadir.tar.zst
注意 /mnt/rescue 必须是另一块可写的盘。如果所有分区都在同一块只读盘上,那就只能往网络传:tar -cf - /var/lib/mysql | ssh user@backup-host "cat > /backup/mysql.tar",或者传到对象存储。这一步之前先算一下数据量,别等传到一半发现目标空间不够。
网站文件里,附件目录(usr/uploads 这类)体积最大但要优先保。程序代码和主题其实可以从版本库或者官方包重新拿,配置文件的修改记录往往有 etc 备份或者 etckeeper。按「不可重建程度」排序来抢:数据库 > 用户上传 > 自己改过的主题文件 > 配置文件 > 程序本体。
第四步:修复与重启,以及怎么避免再次发生
数据抢救完之后再动修复。标准流程是:卸载文件系统、跑 fsck、挂回、验证。在单用户模式或者 rescue 环境里做最稳妥,因为根分区在运行时无法安全 fsck。
# 云控制台切换救援模式,或者在 GRUB 里加 systemd.unit=rescue.target
# 确认分区没挂载
umount /dev/vda1 2>/dev/null || true
# 只做检查,先看有多少错误
fsck.ext4 -fn /dev/vda1
# 确认要修再执行(-y 自动回答 yes,会修改元数据)
e2fsck -fy /dev/vda1
-n 是「只读检查」,不会改任何东西,适合先摸底。-f 强制检查即使文件系统标记为 clean。-y 是自动确认所有修复。有一个参数千万别乱用:e2fsck -p 是「预检模式」,它会在遇到需要人工判断的问题时直接放弃,看起来更安全,但很多人误以为它已经修好了。
重启之后如果一切正常,别急着收工,做三件事防止复发。其一,检查 SMART 或者云盘的健康状态。 如果这块盘已经在报 IO 错误,容量再大也不值得继续用,换盘比修盘便宜。
其二,确认监控覆盖了这个场景。 文件系统变成只读时,CPU、内存、磁盘容量、负载可能全都正常,唯独「写不进去」这个信号没有任何指标能反映。所以需要一个主动探针:每五分钟往 /tmp 之外的一个固定位置写一个文件,写失败就告警。
#!/bin/bash
# /usr/local/bin/rw-probe.sh
F=/var/lib/rw-probe/stamp
mkdir -p "$(dirname "$F")" 2>/dev/null
if date +%s > "$F" 2>/dev/null; then
exit 0
fi
curl -s -m 10 "https://api.telegram.org/bot$TOKEN/sendMessage" \
-d chat_id="$CHAT" \
-d text="$(hostname): root filesystem NOT WRITABLE $(date)" >/dev/null
exit 1
这个探针的价值在于它越过所有应用层。不管故障出在 ext4、云盘、还是 mount 状态,只要根分区写不进去,它立刻报警,而不是等到第二天早上发现所有文章都没入库。
其三,回顾日志找出第一现场。 journalctl --since "6 hours ago" | grep -iE 'error|fail|timeout' 加上 dmesg -T,把故障发生前后三十分钟的所有记录过一遍。很多时候只读只是表象,真正的问题是一个 PHP 进程把内存吃光导致 OOM killer 杀了 mysql,或者一块云盘的 IOPS 配额被打满导致超时。这些问题不解决,换一块盘也只是延后爆发的日期。
几个容易走错的方向
不要用 chattr -i 去解「只读」。 chattr +i 是文件级的「不可变」属性,报错是 Operation not permitted,而不是 Read-only file system。两者混淆的话,会在完全无关的地方浪费几个小时。
不要在生产环境直接 fsck -y 挂载中的分区。 有些教程会说「先 umount 再 fsck」,但对根分区来说 umount 是不行的,只能进救援环境。在挂载状态强行跑 fsck.ext4,它要么拒绝执行,要么在极端情况下进一步破坏元数据。
不要把「remount 成功」当成修好了。 如果触发只读的原因是底层 IO 错误,remount 之后系统可能跑上几个小时再变只读,而你在这几个小时里以为已经恢复,把告警关掉了。正确的处理是 remount 后继续观察 dmesg 至少一天,确认没有任何新的 IO 错误,才算真的恢复。
不要忽略 fstab 里的 errors=remount-ro 其实是可以调的。 改成 errors=continue 能让文件系统在不一致时继续写,看起来「更不容易挂」,但代价是损坏可能扩散到更多 inode,最后连 fsck 都救不回来。默认的 remount-ro 是更安全的选择,不要为了「少一次宕机」把它改掉。真正该做的是把监控和对底层存储的巡检补上。