刚收到告警的时候,我还以为又有年纪足够大的宿主机坏了。
登录 xxx-db-slave-107 ,ssh可以连接,大概率不是硬件问题,心里就踏实了一半。再一看MySQL 进程没了。重启试一下,发现起来了,刚想看看主从有没有自动恢复,结果:
SQLmysql> show slave status\G No connection. Trying to reconnect... ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/home/mysql/data/mysql.sock' (111) ERROR: Can't connect to the server
看来从库崩溃,真的从来都不是"重启就完事"的。它是结果,不是原因。
原由:上次的加盘,我欠了元数据一笔债
这事儿得接上我前阵子写的那篇《给数据库备份"减负"》。
那篇里说过,库膨胀到 TB 级之后,光是备份就能把本地盘吃满。这台 xxx-db-slave-107 就是专门背备份的从机,为了扛住膨胀,我之前在 VMware 上给它加了硬盘,把空间一路扩到了 1.99T。
但当时我有个思维盲区:只把新盘并进数据空间,完全忘了 thin pool 还有一块独立的元数据 LV。
原理其实不难,一句话:thin pool(精简置备)底下有两块东西,一块是装数据的 pool00_tdata,另一块是记账的 pool00_tmeta。tmeta 不存你的数据,它存的是"哪些数据块被分出去了"这张映射表。它的消耗只跟"分配了多少次块、块分布有多碎"有关,跟数据总量基本是两码事。
所以那次加盘,我欢天喜地地把新空间全喂给了数据,元数据 LV 还是当初那个 520 MiB 的小身板。数据空间涨到 1.99T,元数据还是 520 MiB —— 它就是个被遗忘的角落,直到这次被写爆。
换句话说,这次的崩不是突然的,是上一次加盘时埋下的雷。 库一直在长,表一直在插,chunk 映射表默默胀了大半年,终于把 520 MiB 撑到 100%,MySQL 才替它挨了这一枪。
栈帧吓死人,但锅不在 MySQL
报错的核心是一段 InnoDB 的 backtrace,我贴个最关键的几帧:
/usr/local/mysql/bin/mysqld(_Z18os_file_flush_funci+0x223)[0x1381423] /usr/local/mysql/bin/mysqld(_Z9fil_flushm+0x4c8)[0x1517ba8] /usr/local/mysql/bin/mysqld(_Z16fil_space_extendP11fil_space_tm+0x24a)[0x151a5da] /usr/local/mysql/bin/mysqld(_Z24fsp_reserve_free_extentsPmmm13fsp_reserve_tP5mtr_tm+0x4a1)[0x15267f1] /usr/local/mysql/bin/mysqld(_Z26btr_cur_pessimistic_insertm...)[0x148cba9] ... /usr/local/mysql/bin/mysqld(_ZN20Write_rows_log_event9write_rowEPK14Relay_log_infob+0x12e)[0xf2552e]
翻译一下:SQL 线程正在回放一条 Write_rows(主从复制的 insert),InnoDB 要往聚簇索引里插一行,需要先 fil_space_extend 把表空间文件拉长一点,再 fsp_reserve_free_extents 预留空闲区。结果在 os_file_flush 这一步直接 ib::fatal 了。
mysqld 不是被 kill 的,是它自己 abort 的。 一个正常的写入路径,在"刷盘/扩展文件"时撞上了致命 I/O 错误,InnoDB 选择就地自尽。这就解释了一件事:为什么你 restart 之后它看起来活了,但只要复制线程一追数据,一写盘,就又崩。
真正的坑从来不在报错的那一层。MySQL 只是那个替存储层挨枪子的。
我试过,但没用的三招
别急着看答案。先说我这个业余运维半夜拍脑袋试过的失败方案,每一条都"看起来很有道理":
- 直接重启 MySQL。 最本能的反应。结果:进程是起来了,复制线程一追 binlog 就崩,循环自杀。
- 怀疑表坏了,想
innodb_force_recovery起一下。 没敢真上,因为栈帧明确指向"写盘失败",不是"读页面损坏",强制恢复救不了下层存储。 - 想直接扩元数据:
lvextend --poolmetadatasize +2G nlas/pool00。 这句最离谱,系统回我:
BashInsufficient free space: 512 extents needed, but only 0 available
连扩元数据都要先有空闲空间,而 VG 里 VFree = 0。也就是说,我想给存储层打补丁,但存储层已经穷得连补丁都贴不上。
根因:数据没满,元数据先满了
lvs 一打,真相浮出水面:
LV VG Attr LSize Pool Origin Data% Meta% pool00 nlas twi-aotzM- 1.99t 47.86 100.00 root nlas Vwi-aotz-- 1.99t pool00 47.86
注意这俩数字:Data% 47.86,Meta% 100.00。
df -h / 当时显示的是 已用 46%、可用 1.1T —— 它只看文件系统层面的数据占用。但 LVM 这层用的是 thin pool(精简置备),底层有一块专门的元数据 LV(pool00_tmeta,当时只有 520 MiB)在记录"哪些数据块被分配了"。
元数据消耗的是"分配次数和块的分布",不是"数据总量"。一张表频繁 fil_space_extend,小写入,长事务,chunk 映射表天天长,元数据就先被撑爆。等 Meta% = 100%,任何"需要新分配块"的写入(比如 InnoDB 扩表空间)都无法落盘 -> 触发 ib::fatal -> mysqld 自尽。
Data 47% 是假象,Meta 100% 才是死刑。
我的方案:先把盘加上,再自下往上修
既然 VG 没空间,第一步必须真的加一块物理盘。这机器跑在 VMware 上,加的是 1T 的 sdc(lsblk 里能看到 sdb 已经并进去,sdc 是新挂的)。
然后自下而上,一层层修:
Bash# 1. 新盘做成 PV,并进 VG,先解决 VFree=0 pvcreate /dev/sdc vgextend nlas /dev/sdc vgs nlas # VG #PV #LV #SN Attr VSize VFree # nlas 3 3 0 wz--n- <3.00t <1024.00g # 2. 关键一步:扩元数据 LV,从 520 MiB 涨到 2.51 GiB lvextend --poolmetadatasize +2G nlas/pool00 lvs nlas -o lv_name,metadata_size,metadata_percent,data_percent # LV MSize Meta% Data% # pool00 <2.51g 20.25 47.86 # 3. 顺手把 thin 数据池和 root 卷也扩了(各 +900G) lvextend nlas/pool00 -L +900G lvextend nlas/root -L +900G # 4. 在线刷新文件系统,不用 umount xfs_growfs / df -h / # /dev/mapper/nlas-root 2.9T 922G 2.0T 32% / # 5. 元数据修好,空间有了,再重启 MySQL /etc/init.d/mysqld restart
第 2 步之后我再打 lvs,Meta% 从 100.00 掉到 20.25 —— 那一刻我就知道,稳了。后面 root 扩到 2.87T,xfs_growfs 把文件系统从 534276096 块撑到 770205696 块,都是顺手的事。最后 vgs 一看 VFree 还剩 <122.00g,心里那块石头才算落了地。
之前的我 vs 现在的我
| 之前 (半夜拍脑袋) | 现在 (先查存储层) |
|---|---|
df -h 看 46% 就以为磁盘没事 | 一看 thin pool 就先 lvs ... Meta% |
崩了就 restart,循环自杀 | 先定位 ib::fatal 在 os_file_flush |
想扩元数据被 VFree=0 拒了才傻眼 | 先 vgs 确认空闲,没空间就先加盘 |
| 以为 MySQL 的锅 | 知道是 LVM thin metadata 的锅 |
反思:加盘不是终点,监控才是
得说清楚,这次能救是因为还有 VMware 能加盘,VG 还能扩。要是物理机,硬盘槽满了,或者元数据 LV 本身就是独立小盘且没冗余,加盘的退路都没有,那会儿就不是敲命令,是写故障报告了。
另外,扩完不是结束。thin pool 的元数据该监控起来 —— Meta% 这个指标比 df 早死半小时就能报警,省的是你后半夜的命。还有,元数据 LV 本身建议一开始就给到 1G~几 G(取决于卷数量和写入模式),别等 520 MiB 撑爆了才想起它。
写在最后
这次最讽刺的地方在于:df 告诉我磁盘还剩 1T,可 MySQL 已经因为"没地方写"自杀了。
存储这东西,越往下层,越不能用上层的眼睛看。你以为的"还有空间",可能只是数据层还有空间;真正卡死你的那块元数据,连 520 MiB 都守不住。
工程的本质不是会敲命令,是在每次半夜被叫醒之后,把"看起来能跑"的直觉,换成"先查哪一层"的纪律。
这次没敢全用完,留了点空间备用。
