标签:MySQL
标签:MySQL结构
标签:MySQL redo
Redo log 相关结构和记入过程:
1. log_sys对象
(1). log_sys作用
log_sys是redo日志系统的关键全局变量。负责三项事情:
log record从mtr_buf复制到logbuffer:
用户线程mtr_commit时,将mtr_buf中的redo record,拷贝到log buffer中;
log record从log buffer复制到logfile:write_mutex控制logbuffer顺序的刷盘;
dirty page从buffer pool刷写到datafile:log_flush_order_mutex控制flushlist的顺序刷盘;执行CHECKPOINT或preflush。
其中,三个锁分别控制
log_t互斥访问(mutex)、
日志刷盘(write_mutex)、
数据刷盘(log_flush_order_mutex)的操作。
(2). 与日志组、日志相关变量
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对应的文件偏移量。在log_group_t里的lsn和lsn_offset字段已经记录了某个日志lsn和其存放在文件内的偏移量之间的对应关系。
计算时需要结合LOG_FILE_START_LSN,判断是否跨了logfile文件。
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信息(下文详细描述)。
只有做完checkpoint后,其之前的日志才可以不再保留,否则系统崩溃时则无法恢复。在系统崩溃后的恢复,需要从checkpoint点开始。
但我们需要把checkpoint的相关信息持久化的保存下来,才能在系统崩溃时不会丢失这些检查点相关的信息。Checkpoint相关的信息只存放在ib _logfile0中。
lsn_t write_lsn:最近一次完成写入到文件的LSN。
lsn_t current_flush_lsn:当前正在fsync到的LSN。
lsn_t flushed_to_disk_lsn:最近一次完成fsync到文件的LSN。
innodb_log_files_in_group=4
innodb_log_file_size=4G
总文件大小: 17179869184
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完成。
(3). 与redo log 内存缓冲区相关的成员变量:
变量名描述
ulint buf_size:Log buffer 大小,受参数innodb_log_buffer_size控制,但可能会自动extend。
byte* buf:Log buffer起始位置指针。
ulint buf_free:Log buffer中当前空闲可写的位置。redo record可拷贝写入的buffer的位置(buf_free)。
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 record到buffer后,也会判断该值,如果超过buf_free,设置log_sys->check_flush_or_checkpoint为true。
ulint buf_next_to_write:Log buffer偏移量,下次写入redo log文件的起始位置,即本次写入的结束位置。redo缓冲区将要向磁盘刷盘的位置(buf_next_to_write)
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就行了)。
(4). 和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产生的数据词典不一致。
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刷到磁盘。
2. 日志文件头
每个日志文件的前2048字节是存放的文件头信息。
其中几个重要的字段在这里加以说明:
日志文件头共占用4个OS_FILE_LOG_BLOCK_SIZE的大小,即2KB。这里对部分字段做简要介绍:
1) LOG_GROUP_ID 这个log文件所属的日志组,占用4个字节,当前都是0;
2) LOG_FILE_START_LSN 这个log文件记录的开始日志的lsn,占用8个字节;
3) LOG_FILE_WAS_CRATED_BY_HOT_BACKUP 备份程序所占用的字节数,共占用32字节;
4) LOG_CHECKPOINT_1/LOG_CHECKPOINT_2 两个记录InnoDB checkpoint信息的字段,分别从文件头的第二个和第四个block开始记录,只使用日志文件组的第一个日志文件。
从地址2KB偏移量开始,其后就是顺序写入的各个日志块(log block)。
3. 日志块
日志块结构
所有的redo日志记录是以日志块为单位组织在一起的,日志块的大小为OS_FILE_LOG_BLOCK_SIZE(默认值为512字节),所有的日志记录以日志块为单位顺序写入日志文件。
每一条记录都有自己的LSN(log sequence number, 表示从日志记录创建开始到特定的日志记录已经写入的字节数)。
每个日志块包含一个日志头段(12字节)、一个尾段(4字节),以及一组日志记录(512 – 12 – 4 = 496字节) 。
二、 日志写入简要过程
1. mtr log生成
2. 从mtr_buf到logbuffer
拷贝mtr中的record
unsigned long buf_free:buf中空闲的第一个位置,按照该位置向logbuffer中拷贝record。
max_buf_free:推荐的buf_free的最大值,会判断该值,如果超过max_buf_free,需要刷logbuffer;并设置log_sys->check_flush_or_checkpoint为true。
3. 从log buffer到logfile
日志的刷盘是通过调用void log_write_up_to( lsn_t lsn, bool flush_to_disk),如果flush_to_disk为True,则表示将参数lsn之前日志都write&flush;同时更新相应偏移量。
三、 mtr
1. Mini transaction
Mini transaction(简称mtr)是InnoDB对物理数据文件操作的最小事务单元,用于管理对Page加锁、修改、释放、以及日志提交到公共buffer等工作。
一个mtr操作必须是原子的,一个事务可以包含多个mtr。每个mtr完成后需要将本地产生的日志拷贝到公共缓冲区,将修改的脏页放到flush list上。
2. mtr_t结构
struct mtr_t {
/* mtr_t 内嵌一个结构体Impl. */
struct Impl {
mtr_buf_t m_memo; /* 一个被加锁的对象以及它加的锁. */
mtr_buf_t m_log; /* mini-transaction 的 log. */
bool m_made_dirty; /* 是否修改了Buffer Pool中的Page为脏页. */
bool m_modifications; /* 是否修改了Buffer Pool中的Page. */
ib_uint32_t m_n_log_recs; /* 该 mini-transaction 包含多少条log. */
mtr_log_t m_log_mode; /* mini-transaction 的工作模式(MTR_LOG_ALL, MTR_LOG_NO_REDO,MTR_LOG_NONE).
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中。*/
mtr_state_t m_state; /* mini-transaction 的状态: MTR_STATE_INIT, MTR_STATE_ACTIVE, MTR_STATE_COMMITTING, MTR_STATE_COMMITTED. */
}
}
3. mtr_t中成员memo
是个latch持有状态的数组列表,采用的是dyn_array_t的动态内存结构来保存的,每个单元存储的是 mtr_memo_slot_t 。
m_memo中元素是mtr_memo_slot_t, 记录加锁的对象和加锁的类型.
object是latch的对象,可以是rw_lock_t对象,也可以是buf_block_t对象。
/** mini-transaction memo stack slot. */
struct mtr_memo_slot_t {
void *object; /* 加锁的对象. */
ulint type; /* 持有的锁类型,W or R. */
};
4. mt_t中的成员log
是也是一个dyn_array_t动态结构的内存,用来保存mtr产生的日志信息。日志的写入是通过mtr0log.h来写入的。
重做日志虽然有很多种类型,但重做的日志格式是统一的,日志格式是有日志头和日志体组成。
日志头信息是由type、space和page no组成,由mlog_write_initial_log_record_fast函数写入到mtr_t的log中的。
log body的数据写入是通过mtr0log.h中的日志写入方法进行写入的,每写入一条操作日志,n_log_recs会加1。
5. mini-transaction 具体流程
mtr_t mtr
mtr.start()
/* ... */
/* 写入数据至 mini-transaction 的 m_log. */
/* ... */
mtr.commit()
(1). mini-transaction 的start()
/** 启动一个 mini-transaction. */
@param synctrue if it is a synchronous mini-transaction
@param read_onlytrue if read only mini-transaction */
void mtr_t::start(bool sync, bool read_only) {
UNIV_MEM_INVALID(this, sizeof(*this));
UNIV_MEM_INVALID(&m_impl, sizeof(m_impl));
m_sync = sync;
m_commit_lsn = 0;
new (&m_impl.m_log) mtr_buf_t(); /* 记录Redo log的mtr本地Buffer. */
new (&m_impl.m_memo) mtr_buf_t();
/* 初始化 mini-transaction 字段. */
m_impl.m_mtr = this;
m_impl.m_log_mode = MTR_LOG_ALL;
m_impl.m_inside_ibuf = false;
m_impl.m_modifications = false;
m_impl.m_made_dirty = false;
m_impl.m_n_log_recs = 0;
m_impl.m_state = MTR_STATE_ACTIVE;
m_impl.m_flush_observer = NULL;
ut_d(m_impl.m_magic_n = MTR_MAGIC_N);
}
(2). 锁的处理
不同的 mini-transaction 如何互斥?
在操作数据前,会根据锁类型,加不同类型的锁,之后将object和锁类型存入m_memo:
mtr_memo_push(mtr, object, type);
commit完成之后调用release_latches(RELEASE_ALL)将数据上的锁释放.
(3). mini-transaction 插入数据
byte *mlog_open(mtr_t *mtr, ulint size): 打开mtr的m_log
mlog_write_initial_log_record_low()函数向m_log中写入type,space id,page no,并增加m_n_log_recs的数量
mtr->get_log()->push()按不同的类型写数据
mlog_close(): 更新m_log中的位置
(4). mini-transaction 的 commit 过程
commit过程将mini-transaction的m_log数据拷贝到Redo Log Buffer中.
将m_state设置为MTR_STATE_COMMITTING后,调用mtr_t::Command::execute()。