MySQL · 引擎特性 · InnoDB redo log漫游
前言
InnoDB 有两块非常重要的日志,一个是undo log,另外一个是redo log,前者用来保证事务的原子性以及InnoDB的MVCC,后者用来保证事务的持久性。
和大多数关系型数据库一样,InnoDB记录了对数据文件的物理更改,并保证总是日志先行,也就是所谓的WAL,即在持久化数据文件前,保证之前的redo日志已经写到磁盘。
LSN(log sequence number) 用于记录日志序号,它是一个不断递增的 unsigned long long 类型整数。在 InnoDB 的日志系统中,LSN 无处不在,它既用于表示修改脏页时的日志序号,也用于记录checkpoint,通过LSN,可以具体的定位到其在redo log文件中的位置。
为了管理脏页,在 Buffer Pool 的每个instance上都维持了一个flush list,flush list 上的 page 按照修改这些 page 的LSN号进行排序。因此定期做redo checkpoint点时,选择的 LSN 总是所有 bp instance 的 flush list 上最老的那个page(拥有最小的LSN)。由于采用WAL的策略,每次事务提交时需要持久化 redo log 才能保证事务不丢。而延迟刷脏页则起到了合并多次修改的效果,避免频繁写数据文件造成的性能问题。
由于 InnoDB 日志组的特性已经被废弃(redo日志写多份),归档日志(InnoDB archive log)特性也在5.7被彻底移除,本文在描述相关逻辑时会忽略这些逻辑。另外限于篇幅,InnoDB崩溃恢复的逻辑将在下期讲述,本文重点阐述redo log 产生的生命周期以及MySQL 5.7的一些改进点。
本文的分析基于最新的MySQL 5.7.7-RC版本。
InnoDB 日志文件
InnoDB的redo log可以通过参数innodb_log_files_in_group
配置成多个文件,另外一个参数innodb_log_file_size
表示每个文件的大小。因此总的redo log大小为innodb_log_files_in_group * innodb_log_file_size
。
Redo log文件以ib_logfile[number]
命名,日志目录可以通过参数innodb_log_group_home_dir
控制。Redo log 以顺序的方式写入文件文件,写满时则回溯到第一个文件,进行覆盖写。(但在做redo checkpoint时,也会更新第一个日志文件的头部checkpoint标记,所以严格来讲也不算顺序写)。
在InnoDB内部,逻辑上ib_logfile
被当成了一个文件,对应同一个space id。由于是使用512字节block对齐写入文件,可以很方便的根据全局维护的LSN号计算出要写入到哪一个文件以及对应的偏移量。
Redo log文件是循环写入的,在覆盖写之前,总是要保证对应的脏页已经刷到了磁盘。在非常大的负载下,Redo log可能产生的速度非常快,导致频繁的刷脏操作,进而导致性能下降,通常在未做checkpoint的日志超过文件总大小的76%之后,InnoDB 认为这可能是个不安全的点,会强制的preflush脏页,导致大量用户线程stall住。如果可预期会有这样的场景,我们建议调大redo log文件的大小。可以做一次干净的shutdown,然后修改Redo log配置,重启实例。
除了redo log文件外,InnoDB还有其他的日志文件,例如为了保证truncate操作而产生的中间日志文件,包括 truncate innodb 表以及truncate undo log tablespace,都会产生一个中间文件,来标识这些操作是成功还是失败,如果truncate没有完成,则需要在 crash recovery 时进行重做。有意思的是,根据官方worklog的描述,最初实现truncate操作的原子化时是通过增加新的redo log类型来实现的,但后来不知道为什么又改成了采用日志文件的方式,也许是考虑到低版本兼容的问题吧。
关键结构体
log_sys对象
log_sys
是InnoDB日志系统的中枢及核心对象,控制着日志的拷贝、写入、checkpoint等核心功能。它同时也是大写入负载场景下的热点模块,是连接InnoDB日志文件及log buffer的枢纽,对应结构体为log_t
。
其中与 redo log 文件相关的成员变量包括:
变量名 | 描述 |
---|---|
log_groups | 日志组,当前版本仅支持一组日志,对应类型为 log_group_t ,包含了当前日志组的文件个数、每个文件的大小、space id等信息 |
lsn_t log_group_capacity | 表示当前日志文件的总容量,值为:(Redo log文件总大小 - redo 文件个数 * LOG_FILE_HDR_SIZE) * 0.9,LOG_FILE_HDR_SIZE 为 4*512 字节 |
lsn_t max_modified_age_async | 异步 preflush dirty page 点 |
lsn_t max_modified_age_sync | 同步 preflush dirty page 点 |
lsn_t max_checkpoint_age_async | 异步 checkpoint 点 |
lsn_t max_checkpoint_age | 同步 checkpoint 点 |
上述几个sync/async点的计算方式可以参阅函数log_calc_max_ages
,以如下实例配置为例:
innodb_log_files_in_group=4 innodb_log_file_size=4G 总文件大小: 17179869184
各个成员变量值及占总文件大小的比例:
log_sys->log_group_capacity = 15461874893 (90%) log_sys->max_modified_age_async = 12175607164 (71%) log_sys->max_modified_age_sync = 13045293390 (76%) log_sys->max_checkpoint_age_async = 13480136503 (78%) log_sys->max_checkpoint_age = 13914979615 (81%)
通常的:
当当前未刷脏的最老lsn和当前lsn的距离超过max_modified_age_async
(71%)时,且开启了选项innodb_adaptive_flushing
时,page cleaner线程会去尝试做更多的dirty page flush工作,避免脏页堆积。
当当前未刷脏的最老lsn和当前Lsn的距离超过max_modified_age_sync
(76%)时,用户线程需要去做同步刷脏,这是一个性能下降的临界点,会极大的影响整体吞吐量和响应时间。
当上次checkpoint的lsn和当前lsn超过max_checkpoint_age
(81%),用户线程需要同步地做一次checkpoint,需要等待checkpoint写入完成。
当上次checkpoint的lsn和当前lsn的距离超过max_checkpoint_age_async
(78%)但小于max_checkpoint_age
(81%)时,用户线程做一次异步checkpoint(后台异步线程执行CHECKPOINT信息写入操作),无需等待checkpoint完成。
log_group_t
结构体主要成员如下表所示:
变量名 | 描述 |
---|---|
ulint n_files | Ib_logfile的文件个数 |
lsn_t file_size | 文件大小 |
ulint space_id | Redo log 的space id, 固定大小,值为SRV_LOG_SPACE_FIRST_ID |
ulint state | LOG_GROUP_OK 或者 LOG_GROUP_CORRUPTED |
lsn_t lsn | 该group内写到的lsn |
lsn_t lsn_offset | 上述lsn对应的文件偏移量 |
byte** file_header_bufs | Buffer区域,用于设定日志文件头信息,并写入ib logfile。当切换到新的ib_logfile时,更新该文件的起始lsn,写入头部。 头部信息还包含: LOG_GROUP_ID, LOG_FILE_START_LSN(当前文件起始lsn)、LOG_FILE_WAS_CREATED_BY_HOT_BACKUP(函数log_group_file_header_flush) |
lsn_t scanned_lsn | 用于崩溃恢复时辅助记录扫描到的lsn号 |
byte* checkpoint_buf | Checkpoint缓冲区,用于向日志文件写入checkpoint信息(下文详细描述) |
与redo log 内存缓冲区相关的成员变量包括:
变量名 | 描述 |
---|---|
ulint buf_free | Log buffer中当前空闲可写的位置 |
byte* buf | Log buffer起始位置指针 |
ulint buf_size | Log buffer 大小,受参数innodb_log_buffer_size控制,但可能会自动extend |
ulint max_buf_free | 值为log_sys->buf_size / LOG_BUF_FLUSH_RATIO - LOG_BUF_FLUSH_MARGIN, 其中: LOG_BUF_FLUSH_RATIO=2, LOG_BUF_FLUSH_MARGIN=(4 * 512 + 4* page_size) ,page_size默认为16k,当buf_free超过该值时,可能触发用户线程去写redo;在事务拷redo 到buffer后,也会判断该值,如果超过buf_free,设置log_sys->check_flush_or_checkpoint为true |
ulint buf_next_to_write | Log buffer偏移量,下次写入redo文件的起始位置,即本次写入的结束位置 |
volatile bool is_extending | Log buffer是否正在进行扩展 (防止过大的redo log entry无法写入buffer), 实际上,当写入的redo log长度超过buf_size/2时,就会去调用函数log_buffer_extend,一旦扩展Buffer,就不会在缩减回去了! |
ulint write_end_offset | 本次写入的结束位置偏移量(从逻辑来看有点多余,直接用log_sys->buf_free就行了) |
和Checkpoint检查点相关的成员变量:
变量名 | 描述 |
---|---|
ib_uint64_t next_checkpoint_no | 每完成一次checkpoint递增该值 |
lsn_t last_checkpoint_lsn | 最近一次checkpoint时的lsn,每完成一次checkpoint,将next_checkpoint_lsn的值赋给last_checkpoint_lsn |
lsn_t next_checkpoint_lsn | 下次checkpoint的lsn(本次发起的checkpoint的lsn) |
mtr_buf_t* append_on_checkpoint | 5.7新增,在做DDL时(例如增删列),会先将包含MLOG_FILE_RENAME2日志记录的buf挂到这个变量上。 在DDL完成后,再清理掉。(log_append_on_checkpoint),主要是防止DDL期间crash产生的数据词典不一致。 该变量在如下commit加上: a5ecc38f44abb66aa2024c70e37d1f4aa4c8ace9 |
ulint n_pending_checkpoint_writes | 大于0时,表示有一个checkpoint写入操作正在进行。用户发起checkpoint时,递增该值。后台线程完成checkpoint写入后,递减该值(log_io_complete) |
rw_lock_t checkpoint_lock | checkpoint锁,每次写checkpoint信息时需要加x锁。由异步io线程释放该x锁 |
byte* checkpoint_buf | Checkpoint信息缓冲区,每次checkpoint前,先写该buf,再将buf刷到磁盘 |
其他状态变量
变量名 | 描述 |
---|---|
bool check_flush_or_checkpoint | 当该变量被设置时,用户线程可能需要去检查释放要刷log buffer、或是做preflush、checkpoint等以防止Redo 空间不足 |
lsn_t write_lsn | 最近一次完成写入到文件的LSN |
lsn_t current_flush_lsn | 当前正在fsync到的LSN |
lsn_t flushed_to_disk_lsn | 最近一次完成fsync到文件的LSN |
ulint n_pending_flushes | 表示pending的redo fsync,这个值最大为1 |
os_event_t flush_event | 若当前有正在进行的fsync,并且本次请求也是fsync操作,则需要等待上次fsync操作完成 |
log_sys与日志文件和日志缓冲区的关系可用下图来表示:
Mini transaction
Mini transaction(简称mtr)是InnoDB对物理数据文件操作的最小事务单元,用于管理对Page加锁、修改、释放、以及日志提交到公共buffer等工作。一个mtr操作必须是原子的,一个事务可以包含多个mtr。每个mtr完成后需要将本地产生的日志拷贝到公共缓冲区,将修改的脏页放到flush list上。
mtr事务对应的类为mtr_t
, mtr_t::Impl
中保存了当前mtr的相关信息,包括:
变量名 | 描述 |
---|---|
mtr_buf_t m_memo | 用于存储该mtr持有的锁类型 |
mtr_buf_t m_log | 存储redo log记录 |
bool m_made_dirty | 是否产生了至少一个脏页 |
bool m_inside_ibuf | 是否在操作change buffer |
bool m_modifications | 是否修改了buffer pool page |
ib_uint32_t m_n_log_recs | 该mtr log记录个数 |
mtr_log_t m_log_mode | Mtr的工作模式,包括四种: MTR_LOG_ALL:默认模式,记录所有会修改磁盘数据的操作;MTR_LOG_NONE:不记录redo,脏页也不放到flush list上;MTR_LOG_NO_REDO:不记录redo,但脏页放到flush list上;MTR_LOG_SHORT_INSERTS:插入记录操作REDO,在将记录从一个page拷贝到另外一个新建的page时用到,此时忽略写索引信息到redo log中。(参阅函数page_cur_insert_rec_write_log) |
fil_space_t* m_user_space | 当前mtr修改的用户表空间 |
fil_space_t* m_undo_space | 当前mtr修改的undo表空间 |
fil_space_t* m_sys_space | 当前mtr修改的系统表空间 |
mtr_state_t m_state | 包含四种状态: MTR_STATE_INIT、MTR_STATE_COMMITTING、 MTR_STATE_COMMITTED |
在修改或读一个数据文件中的数据时,一般是通过mtr来控制对对应page或者索引树的加锁,在5.7中,有以下几种锁类型(mtr_memo_type_t
):
变量名 | 描述 |
---|---|
MTR_MEMO_PAGE_S_FIX | 用于PAGE上的S锁 |
MTR_MEMO_PAGE_X_FIX | 用于PAGE上的X锁 |
MTR_MEMO_PAGE_SX_FIX | 用于PAGE上的SX锁,以上锁通过mtr_memo_push 保存到mtr中 |
MTR_MEMO_BUF_FIX | PAGE上未加读写锁,仅做buf fix |
MTR_MEMO_S_LOCK | S锁,通常用于索引锁 |
MTR_MEMO_X_LOCK | X锁,通常用于索引锁 |
MTR_MEMO_SX_LOCK | SX锁,通常用于索引锁,以上3个锁,通过mtr_s/x/sx_lock加锁,通过mtr_memo_release释放锁 |
mtr log生成
InnoDB的redo log都是通过mtr产生的,先写到mtr的cache中,然后再提交到公共buffer中,本小节以INSERT一条记录对page产生的修改为例,阐述一个mtr的典型生命周期。
入口函数:row_ins_clust_index_entry_low
开启mtr
执行如下代码块
mtr_start(&mtr); mtr.set_named_space(index->space);
mtr_start主要包括:
初始化mtr的各个状态变量
默认模式为MTR_LOG_ALL,表示记录所有的数据变更
mtr状态设置为ACTIVE状态(MTR_STATE_ACTIVE)
为锁管理对象和日志管理对象初始化内存(mtr_buf_t),初始化对象链表
mtr.set_named_space
是5.7新增的逻辑,将当前修改的表空间对象fil_space_t
保存下来:如果是系统表空间,则赋值给m_impl.m_sys_space
, 否则赋值给m_impl.m_user_space
。
Tips: 在5.7里针对临时表做了优化,直接关闭redo记录:
mtr.set_log_mode(MTR_LOG_NO_REDO)
定位记录插入的位置
主要入口函数: btr_cur_search_to_nth_level
不管插入还是更新操作,都是先以乐观方式进行,因此先加索引S锁mtr_s_lock(dict_index_get_lock(index),&mtr)
,对应mtr_t::s_lock
函数
如果以悲观方式插入记录,意味着可能产生索引分裂,在5.7之前会加索引X锁,而5.7版本则会加SX锁(但某些情况下也会退化成X锁)
加X锁: mtr_x_lock(dict_index_get_lock(index), mtr)
,对应mtr_t::x_lock
函数
加SX锁:mtr_sx_lock(dict_index_get_lock(index),mtr)
,对应mtr_t::sx_lock
函数
对应到内部实现,实际上就是加上对应的锁对象,然后将该锁的指针和类型构建的mtr_memo_slot_t
对象插入到mtr.m_impl.m_memo
中。
当找到预插入page对应的block,还需要加block锁,并把对应的锁类型加入到mtr:mtr_memo_push(mtr, block, fix_type)
如果对page加的是MTR_MEMO_PAGE_X_FIX或者MTR_MEMO_PAGE_SX_FIX锁,并且当前block是clean的,则将m_impl.m_made_dirty
设置成true,表示即将修改一个干净的page。
如果加锁类型为MTR_MEMO_BUF_FIX,实际上是不加锁对象的,但需要判断临时表的场景,临时表page的修改不加latch,但需要将m_impl.m_made_dirty
设置为true(根据block的成员m_impl.m_made_dirty
来判断),这也是5.7对InnoDB临时表场景的一种优化。
同样的,根据锁类型和锁对象构建mtr_memo_slot_t
加入到m_impl.m_memo
中。
相关推荐
运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算。以上实例中 7、5 和 12 是操作数。关系运算符用于计算结果是否为 true 或者 false。逻辑运算符用于测定变量或值之间的逻辑。