MySQL8.0新特性:增加系统文件追踪space ID和物理文件的映射

MySQL 8.0引入了一种新的崩溃恢复机制,通过维护一个系统映射文件来跟踪表空间更改,从而提高了恢复过程的效率。文章详细介绍了这一机制的工作原理及其在打开、关闭、创建和删除表空间时的具体实现。

点击有惊喜


Note1: 本文所有代码相关的内容都是基于MySQL8.0.3,而目前版本还处于RC和快速开发的状态,不排除后面的版本逻辑,函数名等发生变化。
Note2: 主要代码在这个commit 中,感兴趣的也可以自行阅读代码
Note3: 本文仅是本人的阅码笔记,记录的比较凌乱。。。

前面我们提到了MySQL5.7的几个崩溃恢复产生的性能退化 为了解决崩溃恢复的效率问题, MySQL8.0对crash recovery的逻辑进行了进一步的优化。 在之前的版本中,InnoDB通过向redo log中写入日志来追踪在一次checkpoint后修改过的表空间信息,这样就无需在crash recovery时打开所有的表空间,只需搜集哪些被影响到的表空间。而到了8.0新版本里,采用了一种全新的方式:单独创建了系统映射文件, 将space id及路径信息轮换着写到两个指定的系统文件tablespaces.open.1 and tablespaces.open.2中(ref Fil_Open::write)

实现的思路其实不复杂,就是将所有的表空间ID和对应的路径信息存储到系统文件中,在崩溃恢复时再按需打开。

系统文件更新

那么如何保证所有的表空间信息都一个不漏的存储到系统文件了呢 ? 实际上他跟踪了所有的表空间文件操作,并更新内存cache中(Fil_Open::m_spaces), 如下:

a. fil_node_open_file
    fil_system->m_open.enter();
    fil_system->m_open.log(node->space->id, node->name);
    fil_system->m_open.exit();

打开表空间文件后,写一条日志MLOG_FILE_OPEN, 并将表空间状态 Nodes::OPEN以及日志end lsn在内存中进行更新(Fil_Open::Nodes::load)

b. fil_node_close_file
    fil_system->m_open.enter();
    fil_system->m_open.close(node->space->id, node->name);
    fil_system->m_open.exit();

关闭表空间文件后, 将缓存的表空间信息LSN重置为0,并将状态设置为CLOSED (Fil_Open::Nodes::close)

c. fil_name_write_rename
        fil_system->m_open.enter();
        fil_system->m_open.log(space_id, new_name);
        fil_system->m_open.to_file();
        fil_system->m_open.exit();

在物理rename文件之前, 将新的表空间名通过MLOG_FILE_OPEN写到redo log中,记录新文件的状态到内存。

随后就将缓存的表空间信息写到系统映射文件中(Fil_Open::to_file)

d. fil_delete_tablespace
        fil_system->m_open.enter();
        fil_system->m_open.deleted(id);
        fil_system->m_open.exit();

在物理删除文件之后,将对应的表空间状态设置为DELETED (Fil_Open::deleted)

e. fil_ibd_create
    fil_system->m_open.enter();
           fil_system->m_open.open(space_id, file->name, log_get_lsn());
           fil_system->m_open.exit();

在物理创建表空间文件之后, 调用Fil_Open::open 将新文件的信息存储到内存中。同样的包含创建文件时的LSN

可见InnoDB在对文件进行打开,关闭,创建,删除,重命名这些操作时都进行了追踪,其中CREATE/DELETE/RENAME的cache更新均发生在记录对应的MLOG_FILE_*日志之前。

另外我们也可以看到,表空间信息不是直接写入的,而是经过zip压缩后再写的,以减少磁盘空间占用。

那么何时将缓存的信息刷到磁盘呢 ?
第一种情况是rename tablespace时,会做一次写文件
第二种情况是做checkpoint之前会去做一次flush(fil_tablespace_open_sync_to_disk), 相比第一种情况,这里先做一次清理(Fil_Open::purge -> Fil_Open::Nodes::purge),将状态为DELETED/MISSING的无效表空间记录删除掉,再刷到磁盘

当系统正常关闭时,InnoDB会去将系统文件中的信息全部清除掉(fil_tablespace_open_clear),因为崩溃恢复无需用到。

崩溃恢复

那么崩溃恢复时,如何使用该文件呢?

首先在启动时(srv_start), 当确定了需要崩溃恢复时(recv_recovery_from_checkpoint_start),就会去从系统映射文件中载入表空间信息到内存中(fil_tablespace_open_init_for_recovery --> Fil_Open::from_file)。

随后开始读redo log并解析, 如下堆栈:

recv_recovery_begin
    |--> recv_scan_log_recs
        |--> recv_parse_log_recs
            |--> recv_single_rec
            |--> recv_multi_rec

在将redo log加入到hash table之前,会先进行判断,只有在文件中找到的表空间,才需要去apply日志。

if (space_id == TRX_SYS_SPACE
    || fil_tablespace_lookup_for_recovery(space_id)) {

    recv_add_to_hash_table(
        type, space_id, page_no, body,
        ptr + len, old_lsn, recv_sys->recovered_lsn);

} else {

    recv_sys->missing_ids.insert(space_id);
}

由于系统文件不是实时flush的,因此在解析到MLOG_FILE_*类型的redo时, 也要对缓存的表空间信息进行修正(fil_tablespace_name_recover --> fil_name_process_for_recovery) ,以确保所有需要apply redo的tablespace都load到内存中。

在执行崩溃恢复时,InnoDB会按需去打开表空间文件,然后再去apply日志。(recv_apply_hashed_log_recs --> fil_tablespace_open_for_recovery),只有那些需要做崩溃恢复的文件,才会被打开。


点击有惊喜


<think> 我们面对的是一个MySQL启动失败的问题,错误信息包括两部分: 1. 启动时报错:'Starting MySQL ... ERROR! The server quit without updating PID file' 2. 日志文件中报错:'could not find any file associated with tablespace ID: 768' 同时,还有一个约束条件:本机磁盘空间不足,无法进行全量数据库备份。 首先,分析错误原因: - 表空间ID(tablespace ID)768对应的文件丢失或损坏。在InnoDB中,每个表空间都有一个唯一的ID,这个错误通常意味着InnoDB无法找到该ID对应的数据文件。 - 可能的原因包括:数据文件被误删除、文件系统损坏、磁盘空间不足导致文件无法访问等。 由于磁盘空间不足,我们不能直接进行全量备份,因此需要寻找一个占用磁盘空间较小的解决方案。 解决步骤建议: 1. **释放磁盘空间(如果可能)**: 首先,尝试清理一些非必要的文件,释放磁盘空间。即使不能释放足够全量备份的空间,也可能为后续操作提供一些空间。 2. **定位问题表空间**: 表空间ID 768对应的是哪个表?我们可以通过查询`information_schema`库中的`INNODB_SYS_TABLESPACES`表来获取表空间ID表名的映射关系。但是,由于MySQL无法启动,我们无法直接查询。 替代方案:我们可以通过查看数据目录(通常是`/var/lib/mysql`)中的文件来推测。每个InnoDB表都有一个对应的`.ibd`文件(如果使用了独立表空间)。但是,表空间ID并不直接体现在文件名上,因此这种方法不一定可行。 另一种方法是查看MySQL的错误日志,看是否有更详细的信息。或者,我们可以尝试在MySQL的配置文件(my.cnf)中设置`innodb_force_recovery`参数来进行恢复模式启动。 3. **使用innodb_force_recovery尝试启动**: `innodb_force_recovery`参数可以让InnoDB在损坏的情况下仍然尝试启动。该参数有6个级别(1-6),级别越高,尝试的恢复操作越多,但也越危险。通常建议从1开始尝试。 在my.cnf(或my.ini)的`[mysqld]`部分添加: ``` innodb_force_recovery=1 ``` 然后尝试启动MySQL。如果不行,逐渐增加级别(2、3...)直到能启动为止。 注意:在强制恢复模式下,MySQL是只读的,只能进行SELECT操作。我们的目标是在启动后能够查询到哪个表空间ID对应哪个表。 4. **启动后定位问题表**: 一旦MySQL在强制恢复模式下启动,我们可以执行查询: ```sql SELECT * FROM information_schema.INNODB_SYS_TABLESPACES WHERE SPACE = 768; ``` 或者(MySQL 8.0中,该视图可能有所变化): ```sql SELECT * FROM information_schema.INNODB_TABLESPACES WHERE SPACE = 768; ``` 这将告诉我们哪个表或系统表空间出现了问题。 5. **处理问题表**: 假设我们找到了问题表(例如,表名为`mydb.mytable`),我们可以尝试以下操作: - 如果该表不重要,我们可以直接删除它(在强制恢复模式下不能写,所以需要先正常启动)。 - 如果该表重要,我们需要修复它。 为了删除表,我们需要先让MySQL正常启动。但此时因为表空间问题,正常启动失败。因此,我们可以考虑手动删除问题表空间文件,然后让MySQL启动后自动重建(如果是非系统表空间)。 步骤: a. 停止MySQL服务。 b. 在配置文件中移除`innodb_force_recovery`设置(如果之前设置了)。 c. 删除问题表对应的`.ibd`文件(在对应数据库目录下)。注意:系统表空间(ibdata1)不能删除。 d. 在配置文件中添加`innodb_force_recovery=0`(或移除该行)以恢复正常模式。 e. 启动MySQL。此时,由于删除了表空间文件MySQL可能无法启动,或者启动后该表会损坏。但是,如果该表不是存在于独立的表空间文件中(例如,是系统表空间中的表),则不能直接删除文件。 另一种方法是:如果问题表是独立表空间,我们可以删除该表对应的.frm.ibd文件(如果有的话),然后尝试启动。 但是,如果表空间ID 768对应的是系统表空间(比如数据字典表)或者撤销表空间(undo log),那么删除文件可能会导致更严重的问题。此时,可能需要从备份中恢复。 6. **磁盘空间不足的应对**: 由于磁盘空间不足,我们无法做全量备份。我们可以考虑以下方法: - 将部分数据文件移动到其他磁盘(如果有的话),并创建符号链接。 - 删除一些不需要的日志文件(如二进制日志,错误日志等)或旧的数据。 - 如果问题表空间是一个用户表,并且不重要,我们可以尝试直接删除其文件,然后启动MySQL,再重建该表。 7. **极端情况:重建数据字典**(如果问题表空间是系统表空间): 如果问题表空间是系统表空间(ibdata1),那么情况更复杂。我们可以尝试使用`innodb_force_recovery`启动并导出数据(如果可能),然后重新初始化MySQL实例并导入数据。但是,由于磁盘空间不足,导出数据可能困难。 8. **使用外部存储**: 如果有可能,将部分数据文件移动到外部存储(如USB硬盘),然后通过符号链接指向它们,或者修改配置文件中的路径指向外部存储。 具体操作步骤(假设磁盘空间不足,无法全备份,且问题表空间是一个用户表): 1. 清理磁盘空间(如删除错误日志、二进制日志等): - 删除旧的二进制日志:`sudo rm /var/lib/mysql/binlog.*` - 删除旧的错误日志:先停止MySQL(如果启动的话),然后删除错误日志文件(确保在配置中允许重新生成)。 2. 尝试使用`innodb_force_recovery`启动: - 在my.cnf中添加: ``` [mysqld] innodb_force_recovery=1 ``` - 启动MySQL:`sudo systemctl start mysql` 或 `sudo service mysql start` - 如果失败,逐渐增加级别(2,3,4...)直到启动成功。 3. 一旦启动成功,连接到MySQL并查询表空间768对应的表: ```sql SELECT NAME, SPACE, FILE_FORMAT FROM information_schema.INNODB_TABLESPACES WHERE SPACE = 768; ``` 假设查询结果为:`mydb/mytable` 4. 记录下问题表,然后停止MySQL。 5. 删除问题表的数据文件: ``` cd /var/lib/mysql/mydb rm mytable.ibd ``` 同时,如果存在`mytable.frm`文件,也删除(在MySQL 8.0中,数据字典存储在InnoDB中,可能没有.frm文件,但如果有则删除)。 6. 修改配置文件,移除`innodb_force_recovery`参数。 7. 启动MySQL:此时MySQL启动时可能会报告该表不存在,我们可以尝试重建这个表(如果表结构已知)。或者,如果该表不再需要,则可以忽略。 8. 如果MySQL启动成功,检查数据库,删除或重建问题表。 注意:如果问题表空间是系统表空间(ibdata1)的一部分,则不能删除该文件。此时,可能需要从备份中恢复,或者尝试修复系统表空间(非常复杂,且无保证)。如果系统表空间损坏,并且没有备份,数据可能丢失。 因此,在无法备份的情况下,我们只能尝试强制恢复模式导出数据,然后重新初始化MySQL实例。但如果导出数据需要的空间不足,则可能需要扩展磁盘空间或使用外部存储。 总结:由于磁盘空间不足,我们优先尝试清理空间,然后使用强制恢复模式启动,定位问题表,删除问题表(如果非关键且可以丢失),然后正常启动。如果问题表是关键表,则可能需要导出数据(在强制恢复模式下只读),但导出数据需要空间。如果问题表空间属于系统表空间,则情况更复杂。 由于用户磁盘空间不足,需要谨慎操作,避免产生更多数据。 相关问题: 1. 如何清理MySQL的二进制日志以释放磁盘空间? 2. 如何确定表空间ID对应的具体表? 3. 在磁盘空间不足的情况下,如何安全地移动MySQL数据文件到其他磁盘? 4. 除了`innodb_force_recovery`,还有哪些方法可以修复损坏的InnoDB表? 5. 如果系统表空间(ibdata1)损坏,且没有备份,如何尽可能恢复数据? </think> 针对MySQL 8.0启动报错问题(PID文件未更新、表空间ID:768丢失、磁盘空间不足),以下是分步解决方案: --- ### **1. 释放磁盘空间(优先操作)** - **清理MySQL日志**: ```bash sudo rm /var/log/mysql/error.log.* # 删除历史错误日志 sudo rm /var/lib/mysql/ib_logfile* # 删除旧的InnoDB日志(启动后会重建) ``` - **清空二进制日志**(如果启用): ```bash sudo mysql -e "RESET MASTER;" # 需在MySQL能启动时执行 ``` - **临时文件清理**: ```bash sudo rm -rf /tmp/* sudo journalctl --vacuum-size=100M # 清理系统日志 ``` --- ### **2. 定位并修复表空间问题** #### **步骤1:强制恢复模式启动** 编辑配置文件 `my.cnf`: ```ini [mysqld] innodb_force_recovery=6 # 最高恢复级别(只读模式) ``` 尝试启动: ```bash sudo systemctl start mysql ``` #### **步骤2:定位损坏的表** - 若启动成功,查询表空间ID 768对应的表名: ```sql SELECT NAME FROM information_schema.INNODB_TABLESPACES WHERE SPACE = 768; ``` > 示例输出:`mydb/corrupted_table`[^1] #### **步骤3:丢弃损坏的表** ```sql -- 若表可丢弃(非核心系统表) ALTER TABLE mydb.corrupted_table DISCARD TABLESPACE; ``` > ⚠️ 此操作会**永久删除表数据**,仅用于非关键表! --- ### **3. 跳过损坏的表(系统表或关键表)** 若表空间属于系统表(如`mysql`库): 1. 停止MySQL: ```bash sudo systemctl stop mysql ``` 2. **手动注释损坏表**(高风险): - 备份表结构文件(`.frm`): ```bash sudo cp /var/lib/mysql/mysql/*.frm ~/backup/ ``` - 删除损坏文件: ```bash sudo rm /var/lib/mysql/ibdata1 # 系统表空间 sudo rm /var/lib/mysql/mysql/*.ibd # 关联的独立表空间 ``` 3. **重建系统表**: ```bash sudo mysql_upgrade --force sudo mysqld --initialize-insecure ``` --- ### **4. 最小化备份策略(磁盘不足时)** 仅备份核心数据: ```bash # 导出表结构(不备份数据) sudo mysqldump --no-data -u root -p > schema.sql # 选择性导出关键表数据 sudo mysqldump -u root -p mydb critical_table1 > critical_data.sql ``` --- ### **5. 重建InnoDB表空间** 若以上无效,尝试重建表空间: 1. 移动原始数据目录: ```bash sudo mv /var/lib/mysql /var/lib/mysql_old ``` 2. 重新初始化MySQL: ```bash sudo mysqld --initialize-insecure --user=mysql sudo chown -R mysql:mysql /var/lib/mysql ``` 3. **导入部分数据**: ```bash sudo mysql -u root < schema.sql # 表结构 sudo mysql -u root mydb < critical_data.sql # 关键数据 ``` --- ### **关键注意事项** 1. **`innodb_force_recovery`模式**: - 级别1~6逐级尝试,成功后立即备份数据。 - 完成后**必须**移除该配置,否则无法写入[^1]。 2. **系统表损坏**: - 若`mysql`库损坏,需从其他实例复制同名表文件(同版本)。 3. **空间监控**: ```bash df -h /var # 实时监控磁盘使用 ``` > 💡 **预防建议**: > - 定期清理二进制日志:`expire_logs_days=7` > - 启用`innodb_file_per_table`分离表空间 > - 预留20%磁盘空间防崩溃 --- **相关问题** 1. 如何安全清理MySQL二进制日志释放空间? 2. `innodb_force_recovery`各恢复级别的区别是什么? 3. MySQL表空间损坏的常见原因有哪些? 4. 磁盘不足时如何选择性地备份关键数据? 5. 如何预防InnoDB表空间损坏问题? [^1]: MySQL官方文档:InnoDB强制恢复模式 [^2]: 表空间ID物理文件映射机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值