一、磁盘只读不是一个错误,是一个保护
半夜收到告警说站点 500,登录上去发现 mkdir: cannot create directory: Read-only file system,第一反应通常是"磁盘挂了"。但大多数情况下,磁盘没挂,是文件系统自己把自己锁成了只读。
ext4 的默认挂载参数里有一条 errors=remount-ro。它的语义是:一旦文件系统遇到无法安全处理的错误(元数据不一致、日志校验失败、块设备返回 I/O 错误),内核不做"继续写、赌一把"的选择,而是立刻把整个挂载点重新挂载为只读,把损害封在这里,等你去处理。
这个设计是对的。errors=continue 看起来"业务不受影响",实际上是在一个已经损坏的文件系统上继续写入,坏块扩散、索引错乱、数据库页崩溃的概率会显著上升。所以看到只读,不要急着 mount -o remount,rw,那是把报警器拆掉而不是灭火。
正确的顺序是:判断是哪一类只读 → 定位根因 → 处置 → 修复 → 决定是否恢复写入。下面按这个顺序讲。
二、四类只读,先分清是哪一类
"只读"这个现象背后至少有四种完全不同的原因,处置方式差很远。第一分钟就要分清,否则后面的每一步都可能是错的。
类型一:挂载参数就是 ro
最简单的一种,通常是 /etc/fstab 里写错了,或者上一次救援时手工 remount,ro 之后忘了改回来,重启后又按 fstab 生效。
# 看目标路径实际是怎么挂的
findmnt -T / -o TARGET,SOURCE,FSTYPE,OPTIONS
# 或者用 mount 过滤
mount | grep ' / '如果 OPTIONS 里直接就是 ro,而没有别的痕迹,基本就是配置问题。处置很简单:
# 当前会话先恢复写入
mount -o remount,rw /
# 然后确认 fstab 第四列没有留 ro,或 errors 参数是否被改过
grep -v '^#' /etc/fstab改 fstab 时最常犯的错是把 defaults 和 ro 写在了一起,或者手抖把 rw 敲成 ro。改完 mount -a 能立刻验证语法,不用等重启。
类型二:内核因为你踩了错误而 remount
这是最常见的真故障。判据是内核日志里有一串错误,末尾跟着 Remounting filesystem read-only。这时候 mount 会显示 ro,但它不是配置来的,是运行期被改的。
# 抓文件系统错误(Debian/Ubuntu 在 syslog,CentOS 在 messages)
dmesg -T | grep -iE 'ext4|xfs|read-only|I/O error|remount' | tail -40
journalctl -k --since "-2 hours" | grep -iE 'ext4|xfs|I/O error'关键看两件事:
- 有没有
I/O error/blk_update_request。有,说明是块设备层面出了问题(硬盘坏块、云盘异常、驱动报错),文件系统只是被动受害。 - 只有
EXT4-fs error而没有 I/O 错误,多半是元数据损坏(突然断电、内存问题、内核 bug、或者并发写导致的一致性问题),文件系统需要 fsck。
这两个分支的处置完全不同:前者要先保数据、换盘;后者可以尝试修复。
类型三:磁盘满了或 inode 用尽
严格说这不是"只读",报错也常常是 No space left on device。但它和只读的表现非常像——写操作全部失败、日志不再增长、session 写不进去、数据库报 Disk full。排查只读的人有相当比例其实是撞了这一类。
# 空间与 inode 一起看
df -h
df -idf -h 满而 df -i 正常,是空间问题;df -h 还有余量而 df -i 接近 100%,是 inode 用尽。inode 用尽在站点服务器上极常见,因为 PHP 的 cache 目录、session 目录、邮件队列里会堆几十上百万个小文件。
# 找哪个目录文件数最多(大站目录多,注意别扫全盘)
du -x --inodes -d 1 / 2>/dev/null | sort -rn | head -10
du -x --inodes -d 2 /home 2>/dev/null | sort -rn | head -10-x 是关键:只统计同一个文件系统,不跨挂载点,否则一个挂载进来的对象存储目录会让 du 跑上几十分钟。
类型四:写保护或云盘侧限制
云服务器上还有一类:磁盘本身被云厂商置为只读,常见于欠费、云盘配额超限、或者触发了厂商的"保护模式"。这类问题在服务器内部看起来只读,但内核日志干净,没有任何文件系统错误。
# 内核完全没报错但写不进去时,确认设备本身
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,RO
smartctl -a /dev/vda 2>/dev/null | head -30lsblk 的 RO 列显示 1 表示块设备层面就是只读的,这已经不是文件系统的事。此时 remount 会失败或者无效,需要去云控制台看磁盘状态和账单。
三、第一小时的处置顺序
上面分清类型之后,处置顺序有讲究。最忌的是"先 remount 再说",因为 remount 成功会掩盖证据,而如果底下真的是 I/O 错误,继续写入会让修复变得更难。
第一步:留证据
在动任何东西之前,先把内核日志、挂载状态、磁盘状态各存一份。这个动作花不到一分钟,但事后复盘、找云厂商工单时是唯一凭据。
mkdir -p /tmp/fs_incident_$(date +%Y%m%d_%H%M)
D=/tmp/fs_incident_$(date +%Y%m%d_%H%M)
if [ -w /tmp ]; then
dmesg -T > $D/dmesg.txt
findmnt -T / -o TARGET,SOURCE,FSTYPE,OPTIONS > $D/mount.txt
df -h > $D/df_h.txt; df -i > $D/df_i.txt
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,RO > $D/lsblk.txt
echo saved to $D
else
echo "/tmp 也不可写,改用控制台 VNC 或云监控面板取证"
fi注意最后那个分支:如果根分区只读、/tmp 也在根分区上(很多云镜像就是这样),那你连文件都写不了。这时候改用云控制台的 VNC 截图、或者直接把输出复制下来存到本地。这个细节看起来小,但真出事时会卡住很多人。
第二步:判断"能不能继续跑"
只读状态下,读还能用,写全废。对站点意味着:静态页面还能返回(如果 Nginx 读得到),数据库立刻报错,session 全部失效,日志不再写入。这时候要不要抢救,取决于代价:
- 纯静态或者可快速回滚的站点:先切流量到备份节点或维护页,从容处理。
- 有写入的站点:争分夺秒,但要先做备份。只读状态下备份反而更容易——没有任何写入在干扰,直接
tar一份关键数据出来。
# 只读时把关键数据拉到另一块盘或另一台机
tar --ignore-failed-read -czf /data/incident_backup_$(date +%H%M).tgz \
/var/www /etc/nginx /etc/mysql 2>/dev/null
# 或者趁只读直接 rsync 到远端(最安全)
rsync -a --ignore-errors /var/www/ backup@另一台机:/backup/incident//data 若在同一块盘上,tar 会失败,所以目的地必须指向另一个挂载点或远端。这一点务必先确认。
第三步:按类型处置
不同类型的处置动作整理成一张表:
- 配置型:改 fstab,
remount,rw,结束。无需 fsck。 - 纯元数据损坏(无 I/O 错误):先备份,再计划 fsck,重启到单用户或救援模式执行。
- 有 I/O 错误:不要 fsck。先确认数据可读性,做全量复制,再判断是换盘还是找厂商。
- 空间/inode 满:清理,然后把清理动作做成定时任务,防止复发。
- 云盘侧只读:联系厂商,不要在本机折腾。
其中"有 I/O 错误不要 fsck"这条最反直觉也最重要。fsck 会向磁盘写数据,如果底层坏块正在扩散,写入会让情况更糟:原本能读出来的文件,修复过程可能把它变成一片空目录。先复制,再修复,顺序颠倒就是数据丢失的经典剧本。
四、fsck 的正确姿势
确认是元数据损坏、且没有硬件 I/O 错误之后,才能进 fsck。这里有三个前提,缺一个都不要开始。
前提一:必须在未挂载状态下跑
对根分区来说,意味着要么重启进救援/单用户模式,要么用云控制台的"救援模式"把系统盘挂到另一台实例上。在正常运行的系统上对已挂载的根分区跑 fsck,结果基本是二次损坏。
# 云服务器最常见的做法:
# 1. 控制台设置"下次启动进入救援模式"
# 2. 重启,救援系统里系统盘通常挂在 /dev/vdb 或 /dev/sdb
lsblk -f
fsck -n /dev/vdb1 # -n 只检查不修改,先看报告
# 报告里的 bad blocks / orphaned inodes 数量可接受,再执行修复
fsck -y /dev/vdb1-n 这一步千万不要跳。它给出的是"如果修,会动多少东西"的预估。如果报告里有成百上千个错误,说明损坏已经很深,此时强行修可能让系统再也起不来,不如直接从备份恢复。
前提二:确认文件系统类型,用对命令
fsck 是个分发器,它会按 fstab 或超级块自动选具体的检查程序。但在救援模式里 fstab 可能指向不对,所以显式指定更稳:
blkid /dev/vdb1 # 看 TYPE=ext4 还是 xfs
fsck.ext4 -f -y /dev/vdb1 # ext4 用这个
xfs_repair -n /dev/vdb1 # XFS 先干跑
xfs_repair /dev/vdb1 # XFS 确认后再修XFS 的 -n 和 ext4 的 -n 语义一样,但 XFS 没有 -y 参数(xfs_repair 默认就问你要不要修,加 -y 反而是别的意思)。命令记错会白跑一轮。
前提三:备份要先做完
这一条怎么强调都不过分。fsck -y 在遇到孤立 inode 时会直接把它们放进 lost+found,遇到目录结构不一致时会删掉它认为多余的部分。这个过程不可逆。所以无论前一小时多紧张,备份必须先落地。
顺带一个实用细节:如果只读状态下连备份都做不了(因为目标盘也满了),可以用 dd 做一个整盘镜像到另一块盘上,比文件级备份更保险:
# 整盘镜像(目标盘容量必须大于源盘)
dd if=/dev/vdb of=/dev/vdc bs=4M status=progress conv=noerror,syncconv=noerror,sync 是遇到读错误时补零继续,保证镜像长度一致,这样后续可以在镜像上做修复而不碰原盘。慢,但安全。
五、恢复写入之后,别急着庆祝
文件系统修好、remount,rw 成功、站点恢复访问,事情只做了一半。接下来三项检查决定"这次是刚好过去"还是"下周再来一次"。
检查一:数据是否真的完整
fsck 修的是文件系统结构,不是应用数据。数据库文件可能被修成结构完整但内容损坏的状态。所以恢复后要按重要性逐项验证:
# MySQL 表完整性(会读全表,大表慢,挑关键的来)
mysqlcheck -u root -p --check --databases 你的库
# 应用侧:走一遍关键路径的手工测试,别只看首页 200
curl -s -o /dev/null -w '%{http_code}\n' https://你的站/登录后页面
# 最近写入的数据是否还在(看时间戳最新的几行)
mysql -e "SELECT MAX(created) FROM 你的库.文章表;"最容易漏的是"最近一小时的写入"。它们可能在 remount 的瞬间丢失,而表面上系统一切正常。
检查二:根因是否消除
只读是症状。如果是 inode 爆掉,清理完还会再爆;如果是内存问题导致的元数据损坏,那台机器会周期性损坏;如果是云盘异常,换了一块还可能再遇到。所以必须回答"为什么会这样",答案通常在这几个地方:
- 突然断电/强制重启:
last -x | grep shutdown和journalctl --list-boots能看到重启记录,云主机的"强制关机"是元数据损坏的头号来源。 - 内存故障:
dmesg | grep -i 'Machine Check\|MCE\|ECC',云主机看不到物理内存,但会看到内核报的机器检查异常。 - 并发写同一文件:多个进程写同一个日志文件、多个 PHP 进程写同一个缓存文件,是站点服务器上的常见隐患。
- 磁盘寿命:
smartctl -a里看Reallocated_Sector_Ct、Pending_Sector、云盘一般是虚拟化层统计,看不到 SMART,那就要依赖云监控的 IOPS/延迟曲线。
检查三:下次再发生,你能不能更早发现
这次是在用户报 500 之后才知道的。理想情况是"只读发生的那一刻就告警"。实现方式很低成本:每五分钟往 /tmp 或根分区写一个小文件,写失败就告警。
#!/bin/bash
# /opt/scripts/fs_writable_check.sh
P=/var/lib/fscheck
if ! touch "$P/.probe" 2>/dev/null; then
echo "$(date '+%F %T') root fs NOT writable" | mail -s "FS READONLY on $(hostname)" me@example.com
exit 1
fi
rm -f "$P/.probe"
# 顺带检查可用率和 inode
u=$(df -P / | awk 'NR==2{gsub("%","",$5); print $5}')
i=$(df -Pi / | awk 'NR==2{gsub("%","",$5); print $5}')
[ "$u" -ge 88 ] && echo "disk ${u}%" | mail -s "disk high on $(hostname)" me@example.com
[ "$i" -ge 88 ] && echo "inode ${i}%" | mail -s "inode high on $(hostname)" me@example.com注意探测目录要选在你真正关心的那个文件系统上。如果把探测点放在 /tmp 而 /tmp 是 tmpfs,那根分区只读了它照样能写,告警永远不会触发。
六、防复发:四个习惯
做过几次这类故障之后,我的结论是:只读这件事很难完全避免(云环境里不可控因素太多),但"因为只读而丢数据"和"因为误处置而扩大损失"几乎都是可以避免的。四条习惯,都跟具体命令无关,跟顺序和习惯有关。
习惯一:写操作密集的目录单独分区
把 /var/log、站点上传目录、数据库数据目录放在独立的云盘上。这样即使其中一个文件系统出问题,也只影响那一块,根分区依然可写,救援工具全都在。单块盘上所有东西在一起,是"一处损坏、全站失联"的结构性原因。
顺带好处是容量可独立扩容,日志爆掉不会把数据库挤死。
习惯二:日志必须限速和轮转
很多只读的起因不是硬盘,而是"日志写满磁盘后的连锁反应"。Nginx 的错误日志如果和访问日志写同一个文件,或者某个 PHP 警告在循环里疯狂刷,一天能写出几十 GB。配置里至少要有:
# Nginx 主配置
error_log /var/log/nginx/error.log warn;
access_log /var/log/nginx/access.log main buffer=32k flush=5s;
# logrotate 里给一个硬上限
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
rotate 14
maxsize 200M
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}maxsize 是"即使没到时间,超过大小也切",这一条是防"一天写爆一周配额"的关键。kill -USR1 让 Nginx 重新打开日志文件,不加这一句切完之后 Nginx 还握着旧文件句柄,磁盘空间不释放——这个坑几乎每个站点都踩过一次。
习惯三:定时任务里不要做无上限的删除
find / -mtime -1 -delete 这类命令误写一次就是灾难。凡是要删除的,一律先 find ... -print | wc -l 确认数量,再用 -delete。更稳的做法是先 mv 到一个临时目录,观察一天后自动清理。
习惯四:把 fstab 里的 errors 参数看清楚
默认 errors=remount-ro 是对的,不要改。但如果你接手的是别人配的机器,一定要检查有没有被改成 errors=continue 或 errors=panic:前者隐匿故障,后者会让机器直接崩溃重启。findmnt 输出的 options 里能看到,没有显式写出就是内核默认值 remount-ro。
七、小结:只读是报警器,不是故障本身
回到开头的场景。半夜看到"Read-only file system",我希望你脑子里出现的第一个念头是"内核发现了一个它处理不了的错误",而不是"重启一下"。这个念头差别,决定你保住数据的概率。
三条判据记住就够用:
- 看内核日志再动手。有
I/O error是硬件/云盘问题,只有EXT4-fs error是元数据问题,什么都没有就查 fstab 和df -i。 - 先复制再修复。
fsck -n先看报告,确认可修复再上-y,任何情况下备份都在前面。 - 恢复之后查根因和写入丢了多少。只读那段时间的写入就是丢了,要主动找回来,别等用户来告诉你。
最后一句实在话:这类故障里最贵的永远不是磁盘,而是那几小时的写入。 备份做得好,只读就只是一个需要处理的下午;备份做得不好,只读就是一个时代结束的地方。