检查点 - Checkpoint机制详解
定义
Checkpoint (检查点) 是InnoDB存储引擎的一种持久化机制,它确保在某个时间点之前产生的所有脏页(Dirty Page)都已经从Buffer Pool刷新到磁盘上的数据文件(.ibd)。同时,Checkpoint会记录一个LSN(Log Sequence Number)值,表示在该LSN之前的所有Redo Log都已经应用到数据文件中。
核心目标
| 目标 | 说明 |
|---|---|
| 缩短恢复时间 | 崩溃后只需重做最后一个Checkpoint之后的Redo Log |
| 控制脏页数量 | 定期刷脏页,避免Buffer Pool爆满 |
| 保证数据一致性 | 确保数据文件和Redo Log的LSN对齐 |
| 支持增量备份 | 基于Checkpoint LSN进行增量备份 |
检查点类型对比
锐利检查点 (Sharp Checkpoint)
┌─────────────────────────────────────┐
│ • MySQL启动时执行 │
│ • 等待所有事务完成 │
│ • 将所有脏页刷盘 │
│ • 记录Checkpoint LSN │
│ • 恢复时间 = 0 (无需Redo) │
│ • 缺点: 阻塞时间长 │
└─────────────────────────────────────┘
模糊检查点 (Fuzzy Checkpoint)
┌─────────────────────────────────────┐
│ • MySQL运行时定期执行 │
│ • 只刷部分脏页 │
│ • 不阻塞正常事务 │
│ • 记录Checkpoint LSN │
│ • 恢复时间 = 重放Checkpoint后日志 │
│ • 优点: 不影响在线性能 │
└─────────────────────────────────────┘InnoDB默认使用模糊检查点,有以下几种触发方式:
- Master Thread Checkpoint: 每秒检查一次
- Dirty Page Too Much Checkpoint: 脏页比例超过阈值
- Redo Log Full Checkpoint: Redo Log空间不足
- Async/Sync Flush Checkpoint: 异步/同步刷盘
为什么需要Checkpoint?
问题1: 崩溃恢复时间过长
没有Checkpoint的场景:
时间线:
T0: MySQL启动
T1: 事务A修改Page #100, #200, #300 (提交)
T2: 事务B修改Page #400, #500 (提交)
...
T999999: 事务Z修改Page #9999 (提交)
T1000000: 系统崩溃! 💥
恢复过程:
┌──────────────────────────────────────┐
│ MySQL重启 │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ 扫描所有Redo Log (LSN 1 ~ 1000000) │
│ 重放100万次修改操作 │
│ 耗时: 30分钟 ⏰ │
└──────────────────────────────────────┘使用Checkpoint的场景:
时间线:
T0: MySQL启动
T100: Checkpoint (LSN=100, 所有脏页刷盘)
T200: Checkpoint (LSN=200, 所有脏页刷盘)
...
T999900: Checkpoint (LSN=999900)
T999999: 事务Z修改Page #9999 (提交)
T1000000: 系统崩溃! 💥
恢复过程:
┌──────────────────────────────────────┐
│ MySQL重启 │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ 读取最后一个Checkpoint LSN=999900 │
│ 只重放LSN 999900 ~ 1000000的日志 │
│ 重放100次修改操作 │
│ 耗时: 3秒 ✓ │
└──────────────────────────────────────┘性能提升: 恢复时间从30分钟降低到3秒,提升600倍!
问题2: Buffer Pool空间不足
场景:
Buffer Pool大小: 8GB
数据文件大小: 100GB
workload:
- 持续随机读取不同数据页
- 每页16KB,共625万页
没有Checkpoint:
┌──────────────────────────────────────┐
│ Buffer Pool逐渐被填满 │
│ 脏页积累 → 可用页减少 │
│ 最终: 无法分配新页 ❌ │
└──────────────────────────────────────┘
有Checkpoint:
┌──────────────────────────────────────┐
│ 定期检查脏页比例 │
│ 超过75%时触发刷盘 │
│ 脏页 → 数据文件 │
│ Buffer Pool保持充足可用空间 ✓ │
└──────────────────────────────────────┘问题3: Redo Log循环写入覆盖
场景:
Redo Log配置:
- innodb_log_file_size = 1GB
- innodb_log_files_in_group = 2
- 总容量 = 2GB
写入速度: 100MB/s
没有Checkpoint的风险:
┌──────────────────────────────────────┐
│ T0: Redo Log写入位置: 0GB │
│ T10s: 写入位置: 1GB │
│ T20s: 写入位置: 2GB (回到起点) │
│ 需要覆盖0~1GB的旧日志 │
│ 但这些日志对应的脏页还在内存! │
│ 如果此时崩溃,数据丢失! ❌ │
└──────────────────────────────────────┘
有Checkpoint的保护:
┌──────────────────────────────────────┐
│ T0: Checkpoint LSN = 1000 │
│ 脏页已刷盘 │
│ T10s: Redo Log写入位置: 1GB │
│ T15s: Checkpoint推进到LSN = 500000 │
│ LSN < 500000的脏页已刷盘 │
│ T20s: 可以安全覆盖LSN < 500000的日志 │
│ 因为对应的数据已持久化 ✓ │
└──────────────────────────────────────┘Checkpoint核心数据结构
Checkpoint信息存储
c
/* storage/innobase/include/log0log.h */
/**
* Checkpoint存储在Redo Log文件的固定位置
* 每个日志文件的前两个块专门用于存储Checkpoint
*/
#define LOG_CHECKPOINT_1 0 /* 第一个Checkpoint在块0 */
#define LOG_CHECKPOINT_2 1 /* 第二个Checkpoint在块1 */
/**
* Checkpoint数据结构
*/
struct log_checkpoint_t {
uint64_t checkpoint_no; /* Checkpoint编号 */
uint64_t checkpoint_lsn; /* Checkpoint时的LSN */
uint64_t lsn_offset; /* LSN在日志文件中的偏移 */
/* Buffer Pool状态 */
uint32_t n_pending_flushes; /* 待刷新的页面数 */
uint64_t oldest_modification; /* 最旧的脏页修改LSN */
/* 文件信息 */
uint32_t log_file_number; /* 当前日志文件编号 */
uint64_t log_file_size; /* 日志文件大小 */
/* 校验和 */
uint32_t checksum; /* Checkpoint校验和 */
};
/**
* Checkpoint写入函数
* @param checkpoint_no Checkpoint编号
* @param checkpoint_lsn Checkpoint LSN
*/
void checkpoint_write(
ulint checkpoint_no,
lsn_t checkpoint_lsn)
{
log_checkpoint_t cp;
byte* buf;
/* 1. 填充Checkpoint结构 */
cp.checkpoint_no = checkpoint_no;
cp.checkpoint_lsn = checkpoint_lsn;
cp.oldest_modification = buf_pool_get_oldest_modification();
/* 2. 计算存储位置(交替使用两个Checkpoint块) */
ulint block_no = (checkpoint_no % 2 == 0)
? LOG_CHECKPOINT_1
: LOG_CHECKPOINT_2;
/* 3. 序列化到缓冲区 */
buf = serialize_checkpoint(&cp);
/* 4. 写入日志文件头部 */
os_file_write(
log_sys->files[0],
buf,
block_no * OS_FILE_LOG_BLOCK_SIZE,
OS_FILE_LOG_BLOCK_SIZE);
/* 5. 强制刷盘 */
os_file_flush(log_sys->files[0]);
}Buffer Pool中的脏页管理
c
/* storage/innobase/buf/buf0flu.cc */
/**
* 获取Buffer Pool中最旧的脏页修改LSN
* @return 最旧修改LSN
*/
lsn_t buf_pool_get_oldest_modification(void)
{
lsn_t min_lsn = ULINT_MAX;
/* 遍历所有Buffer Pool实例 */
for (ulint i = 0; i < srv_buf_pool_instances; i++) {
buf_pool_t* buf_pool = buf_pool_from_array(i);
mutex_enter(&buf_pool->flush_state_mutex);
/* 获取flush_list中的第一个节点(最旧的脏页) */
buf_page_t* bpage = UT_LIST_GET_FIRST(buf_pool->flush_list);
if (bpage != NULL) {
lsn_t oldest = bpage->oldest_modification;
if (oldest < min_lsn) {
min_lsn = oldest;
}
}
mutex_exit(&buf_pool->flush_state_mutex);
}
return min_lsn;
}
/**
* 脏页链表(Flush List)
* 按oldest_modification LSN排序
*/
struct buf_page_t {
/* ... 其他字段 ... */
lsn_t oldest_modification; /* 该页第一次被修改时的LSN */
lsn_t newest_modification; /* 该页最后一次被修改时的LSN */
/* flush_list链表节点 */
UT_LIST_NODE_T(buf_page_t, list) flush_list;
};InnoDB源码分析
模糊检查点主流程
cpp
/* storage/innobase/log/log0chkp.cc */
/**
* 执行模糊检查点
* @return 成功返回true
*/
bool log_checkpoint_make(bool sync)
{
lsn_t checkpoint_lsn;
lsn_t oldest_lsn;
/* 1. 获取当前LSN */
mutex_enter(&log_sys->mutex);
checkpoint_lsn = log_sys->lsn;
/* 2. 获取最旧脏页修改LSN */
oldest_lsn = buf_pool_get_oldest_modification();
/* 3. 判断是否需要刷脏页 */
if (oldest_lsn < checkpoint_lsn) {
/* 有脏页需要刷新 */
if (sync) {
/* 同步刷新: 等待所有IO完成 */
buf_flush_list(
ULONG_MAX, /* 刷新所有脏页 */
checkpoint_lsn, /* 目标LSN */
TRUE); /* 同步模式 */
} else {
/* 异步刷新: 后台继续 */
buf_flush_list(
srv_max_dirty_pages_pct_lwm, /* 刷新到阈值以下 */
checkpoint_lsn,
FALSE); /* 异步模式 */
}
}
/* 4. 更新Checkpoint编号 */
log_sys->checkpoint_no++;
/* 5. 写入Checkpoint信息到日志文件 */
checkpoint_write(
log_sys->checkpoint_no,
checkpoint_lsn);
mutex_exit(&log_sys->mutex);
return true;
}
/**
* Master Thread中的Checkpoint逻辑
* 每秒执行一次
*/
void master_thread_checkpoint_loop()
{
while (srv_shutdown_state == SRV_SHUTDOWN_NONE) {
/* 睡眠1秒 */
os_thread_sleep(1000000);
/* 1. 检查脏页比例 */
ulint dirty_pct = buf_get_modified_ratio_pct();
if (dirty_pct > srv_max_dirty_pages_pct) {
/* 脏页过多,触发Checkpoint */
log_checkpoint_make(FALSE); /* 异步 */
}
/* 2. 检查Redo Log使用率 */
ulint log_pct = log_calc_usage_pct();
if (log_pct > 75) {
/* Redo Log使用超过75%,紧急Checkpoint */
log_checkpoint_make(TRUE); /* 同步 */
}
}
}脏页刷新算法
cpp
/* storage/innobase/buf/buf0flu.cc */
/**
* 刷新脏页到磁盘
* @param max_n_pages 最多刷新页数
* @param lsn_limit 只刷新oldest_modification < lsn_limit的页
* @param sync 是否同步等待
* @return 实际刷新页数
*/
ulint buf_flush_list(
ulint max_n_pages,
lsn_t lsn_limit,
bool sync)
{
ulint flushed_count = 0;
buf_page_t* bpage;
mutex_enter(&buf_pool->flush_list_mutex);
/* 1. 从flush_list头部开始遍历(最旧的脏页) */
bpage = UT_LIST_GET_FIRST(buf_pool->flush_list);
while (bpage != NULL && flushed_count < max_n_pages) {
/* 2. 检查LSN条件 */
if (bpage->oldest_modification >= lsn_limit) {
break; /* 后面的页都不需要刷新 */
}
/* 3. 尝试获取页面锁 */
if (!mutex_trylock(&bpage->mutex)) {
/* 页面正被使用,跳过 */
bpage = UT_LIST_GET_NEXT(flush_list, bpage);
continue;
}
/* 4. 再次检查页面状态 */
if (buf_page_is_dirty(bpage) &&
bpage->oldest_modification < lsn_limit) {
/* 5. 发起异步IO写请求 */
buf_flush_page_and_try_neighbors(bpage);
flushed_count++;
}
mutex_unlock(&bpage->mutex);
/* 6. 移动到下一个脏页 */
bpage = UT_LIST_GET_NEXT(flush_list, bpage);
}
mutex_exit(&buf_pool->flush_list_mutex);
/* 7. 同步模式下等待所有IO完成 */
if (sync && flushed_count > 0) {
buf_flush_wait_batch_end(flushed_count);
}
return flushed_count;
}
/**
* 刷新页面及其邻居页(LRU优化)
*/
void buf_flush_page_and_try_neighbors(buf_page_t* bpage)
{
ulint space = bpage->space;
ulint offset = bpage->offset;
/* 1. 计算相邻页面范围 */
ulint start_offset = offset - (offset % 8); /* 对齐到8页边界 */
/* 2. 批量刷新连续脏页 */
for (ulint i = 0; i < 8; i++) {
ulint curr_offset = start_offset + i;
buf_page_t* curr_bpage = buf_page_hash_get(space, curr_offset);
if (curr_bpage != NULL && buf_page_is_dirty(curr_bpage)) {
/* 3. 加入IO队列 */
buf_flush_insert_into_io_queue(curr_bpage);
}
}
}崩溃恢复中的Checkpoint应用
cpp
/* storage/innobase/log/log0recv.cc */
/**
* 崩溃恢复: 找到最后一个有效的Checkpoint
* @return Checkpoint LSN
*/
lsn_t recv_find_last_checkpoint()
{
log_checkpoint_t cp1, cp2;
/* 1. 读取两个Checkpoint */
cp1 = checkpoint_read(LOG_CHECKPOINT_1);
cp2 = checkpoint_read(LOG_CHECKPOINT_2);
/* 2. 验证校验和 */
bool cp1_valid = checkpoint_validate_checksum(&cp1);
bool cp2_valid = checkpoint_validate_checksum(&cp2);
/* 3. 选择最新的 valid Checkpoint */
log_checkpoint_t* last_cp;
if (cp1_valid && cp2_valid) {
last_cp = (cp1.checkpoint_no > cp2.checkpoint_no) ? &cp1 : &cp2;
} else if (cp1_valid) {
last_cp = &cp1;
} else if (cp2_valid) {
last_cp = &cp2;
} else {
/* 两个都损坏,需要从数据文件扫描 */
ut_error;
}
return last_cp->checkpoint_lsn;
}
/**
* 崩溃恢复主流程
*/
void crash_recovery()
{
/* 1. 找到最后一个Checkpoint */
lsn_t checkpoint_lsn = recv_find_last_checkpoint();
printf("Last checkpoint LSN: %lu\n", checkpoint_lsn);
/* 2. 从Checkpoint LSN开始扫描Redo Log */
lsn_t start_lsn = checkpoint_lsn;
lsn_t end_lsn = log_sys->lsn;
printf("Recovering from LSN %lu to %lu\n", start_lsn, end_lsn);
/* 3. REDO阶段: 重放日志 */
recv_recover_from_log(start_lsn, end_lsn);
/* 4. UNDO阶段: 回滚未提交事务 */
recv_rollback_uncommitted_transactions();
printf("Crash recovery completed!\n");
}
/**
* 从日志恢复
*/
void recv_recover_from_log(lsn_t start_lsn, lsn_t end_lsn)
{
ulint n_recovered = 0;
/* 1. 定位到起始LSN */
log_sys->buf_free = start_lsn % log_sys->size;
/* 2. 逐条读取Redo Record */
while (log_sys->buf_free < end_lsn % log_sys->size) {
redo_record_t* rec = log_read_redo_record();
if (rec == NULL) {
break;
}
/* 3. 应用Redo Record到数据页 */
recv_apply_redo_record(rec);
n_recovered++;
}
printf("Recovered %lu pages\n", n_recovered);
}Checkpoint工作流程
完整生命周期
MySQL启动
↓
┌──────────────────────────────────────────┐
│ 锐利检查点 (Sharp Checkpoint) │
│ - 等待所有事务完成 │
│ - 将所有脏页刷盘 │
│ - 记录Checkpoint LSN │
│ - 恢复时间 = 0 │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ MySQL正常运行 │
│ │
│ 每秒检查(Master Thread): │
│ IF 脏页比例 > 75% THEN │
│ 触发模糊检查点 │
│ END IF │
│ │
│ IF Redo Log使用 > 75% THEN │
│ 触发紧急检查点 │
│ END IF │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ 模糊检查点执行 (Fuzzy Checkpoint) │
│ │
│ Step 1: 获取当前LSN │
│ Step 2: 获取最旧脏页LSN │
│ Step 3: 异步刷新脏页到数据文件 │
│ Step 4: 更新Checkpoint编号 │
│ Step 5: 写入Checkpoint信息到日志文件头部 │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ 系统崩溃 │
│ │
│ 恢复流程: │
│ 1. 读取最后一个Checkpoint LSN │
│ 2. 从Checkpoint LSN开始重放Redo Log │
│ 3. 只重放几百条记录(而非几百万条) │
│ 4. 回滚未提交事务 │
│ 5. 数据库恢复一致状态 │
└──────────────────────────────────────────┘Checkpoint与Redo Log的关系
Redo Log文件布局:
┌──────────────────────────────────────────────────────┐
│ Block 0: Checkpoint #1 │
│ checkpoint_no = 1000 │
│ checkpoint_lsn = 500000 │
│ oldest_modification = 450000 │
├──────────────────────────────────────────────────────┤
│ Block 1: Checkpoint #2 (备用) │
│ checkpoint_no = 999 │
│ checkpoint_lsn = 490000 │
├──────────────────────────────────────────────────────┤
│ Block 2+: Redo Records │
│ LSN 450000: MLOG_4BYTES page=100 offset=50 ... │
│ LSN 450001: MLOG_REC_INSERT page=200 ... │
│ ... │
│ LSN 500000: ← Checkpoint标记这里之前的都已刷盘 │
│ ... │
│ LSN 600000: ← 当前写入位置 │
└──────────────────────────────────────────────────────┘
关键关系:
- Checkpoint LSN = 500000
- 当前LSN = 600000
- 最旧脏页LSN = 450000
含义:
✓ LSN < 500000的所有脏页已在数据文件中
✗ LSN 450000 ~ 500000的脏页还在Buffer Pool中
✓ 崩溃后只需重放LSN 500000 ~ 600000的日志监控与调优
监控指标
sql
-- 1. 查看Checkpoint状态
SHOW ENGINE INNODB STATUS\G
-- 输出:
--- LOG ---
Log sequence number 1234567890 -- 当前LSN
Log flushed up to 1234500000 -- 已刷盘的LSN
Pages flushed up to 1234400000 -- Checkpoint LSN
Last checkpoint at 1234400000
-- 计算:
-- Checkpoint年龄 = 1234567890 - 1234400000 = 167890 bytes
-- Redo Log使用率 = 167890 / (2 * 1024 * 1024 * 1024) ≈ 0.008%
-- 2. 监控脏页比例
SELECT
pool_id,
pool_size,
free_buffers,
database_pages,
modified_database_pages,
ROUND(modified_database_pages * 100.0 / pool_size, 2) AS dirty_pct
FROM information_schema.INNODB_BUFFER_POOL_STATS;
-- 输出:
-- +---------+-----------+--------------+------------------+-----------------------+-----------+
-- | pool_id | pool_size | free_buffers | database_pages | modified_database_pages| dirty_pct |
-- +---------+-----------+--------------+------------------+-----------------------+-----------+
-- | 0 | 65536 | 10000 | 50000 | 5000 | 7.62%|
-- +---------+-----------+--------------+------------------+-----------------------+-----------+
-- 3. 实时监控Checkpoint活动
SHOW GLOBAL STATUS LIKE 'Innodb_checkpoint_age';
SHOW GLOBAL STATUS LIKE 'Innodb_max_checkpoint_age';
-- Checkpoint年龄 = 当前LSN - Checkpoint LSN
-- 最大年龄 = Redo Log总容量
-- 4. Prometheus监控
-- alert: CheckpointAgeTooHigh
-- expr: innodb_checkpoint_age / innodb_max_checkpoint_age > 0.75
-- for: 5m
-- annotations:
-- summary: "Checkpoint age too high, risk of blocking"关键配置参数
ini
[mysqld]
# 脏页比例阈值(默认75%)
# 超过此值触发Checkpoint
innodb_max_dirty_pages_pct = 75
# 低水位线(默认0,禁用)
# 脏页降到此值以下停止刷新
innodb_max_dirty_pages_pct_lwm = 10
# Redo Log文件大小
# 越大,Checkpoint间隔越长
innodb_log_file_size = 2048M
# Redo Log文件数量
innodb_log_files_in_group = 2
# IO能力(影响Checkpoint刷盘速度)
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
# 自适应刷新策略(MySQL 5.7+)
# ON: 根据负载自动调整
# OFF: 固定使用innodb_max_dirty_pages_pct
innodb_adaptive_flushing = ON性能调优案例
场景1: Checkpoint频繁导致性能抖动
sql
-- 问题: 每隔几分钟就有一次IO高峰
-- 诊断:
SHOW ENGINE INNODB STATUS\G
-- 发现:
-- Checkpoint age: 1.8GB
-- Max checkpoint age: 2GB
-- 使用率: 90% (过高!)
-- 解决: 增大Redo Log
SET GLOBAL innodb_log_file_size = 4096M;
-- 需要重启MySQL
-- 效果:
-- Max checkpoint age: 4GB
-- 使用率: 45% ✓
-- Checkpoint频率降低场景2: 崩溃恢复时间过长
sql
-- 问题: 重启需要10分钟
-- 诊断:
SHOW ENGINE INNODB STATUS\G
-- 发现:
-- Last checkpoint at: 1000000
-- Log sequence number: 100000000
-- Checkpoint age: 99MB (过大)
-- 原因: innodb_max_dirty_pages_pct = 90 (太高)
-- 解决:
SET GLOBAL innodb_max_dirty_pages_pct = 60;
SET GLOBAL innodb_io_capacity = 4000;
-- 效果:
-- Checkpoint更频繁,age保持在50MB以内
-- 恢复时间: 1分钟 ✓最佳实践
1. 生产环境配置
ini
[mysqld]
# 根据写入负载调整
innodb_log_file_size = 2048M # 高写入用4GB
innodb_log_files_in_group = 2
innodb_max_dirty_pages_pct = 75
innodb_max_dirty_pages_pct_lwm = 10 # 启用低水位
innodb_io_capacity = 2000 # SSD用4000+
innodb_io_capacity_max = 4000
innodb_adaptive_flushing = ON2. 批量导入优化
sql
-- 临时禁用自适应刷新
SET GLOBAL innodb_adaptive_flushing = OFF;
SET GLOBAL innodb_max_dirty_pages_pct = 90;
-- 执行批量导入
LOAD DATA INFILE '/data/huge.csv' INTO TABLE huge_table;
-- 手动触发Checkpoint
SET GLOBAL innodb_adaptive_flushing = ON;
CHECKPOINT; -- 或通过FLUSH TABLES间接触发
-- 恢复配置
SET GLOBAL innodb_max_dirty_pages_pct = 75;3. 快速关闭优化
bash
# 正常关闭: 执行Sharp Checkpoint(慢)
mysqladmin shutdown
# 快速关闭: 跳过Checkpoint(快,但下次启动恢复时间长)
mysqladmin shutdown --fast
# 紧急关闭: 直接kill(危险!)
kill -9 $(pidof mysqld)参考资料
MySQL官方文档
源码文件
storage/innobase/log/log0chkp.cc- Checkpoint实现storage/innobase/buf/buf0flu.cc- 脏页刷新storage/innobase/log/log0recv.cc- 崩溃恢复
相关术语
版本历史:
- 2026-04-12: 初始版本,全面讲解Checkpoint机制