innodb重做日志实现原理(上)
1.概念介绍
重做日志主要用来故障恢复数据的修复纠正,保证实现事务的持久性。为了实现持久性,innodb需要将bufferpool数据刷新到磁盘,但是这是一个随机写,性能很差,所以为了减少bufferpool写入磁盘的io开销,innodb引入redolog,把随机写转化成顺序写,推迟了bufferpool的刷新,提高数据库写性能。本文内容:redolog架构,细节概念介绍,日志的写入。日志恢复逻辑在下一篇文章介绍。
重做日志和二进制日志的区别
二进制日志在mysql server层记录,而重做日志在存储引擎层记录。二进制日志在事务提交之后一次写入,记录的是逻辑日志(SQL语句)。而重做日志记录的是物理日志(对页的修改),并且是在事务执行的时候不断的写入。
重做日志实现机制
分为两个部分,一个是内存中的缓存,另外一个是持久化的文件。事务修改先被写入缓冲,事务提交的时候刷新到磁盘。为了保证事务的持久性,每次commit的时候需将数据持久化到磁盘件,因此数据库的写性能比较差,尽管redo log是顺序读写。当然可以通过设置innodb_flush_log_at_trx_commit参数来改变持久化数据的时机。
innodb_flush_log_at_trx_commit参数:
- 设置为0:启动线程每1s写入磁盘
- 设置为1:每次commit会写入
- 设置为2:写入文件系统缓存不进行fsync操作
实现细节:(这个方法在mysql commit事务的时候回调用,主要就是将根据策略将重做日志缓冲区的数据刷新到磁盘)
if (!trx->must_flush_log_later){
/* Do nothing */
}
else if (srv_flush_log_at_trx_commit == 0){
/* Do nothing */
}
else if (srv_flush_log_at_trx_commit == 1){
if (srv_unix_file_flush_method == SRV_UNIX_NOSYNC){
/* Write the log but do not flush it to disk */
log_write_up_to(lsn, LOG_WAIT_ONE_GROUP, FALSE);
}
else{
/* Write the log to the log files AND flush them to
disk */
log_write_up_to(lsn, LOG_WAIT_ONE_GROUP, TRUE);
}
}
else if (srv_flush_log_at_trx_commit == 2){
/* Write the log but do not flush it to disk */
log_write_up_to(lsn, LOG_WAIT_ONE_GROUP, FALSE);
}
else{
ut_error;
}
检查点
为了数据库的持久性,数据需要写入磁盘,但是写入磁盘是随机写,性能很差,所以采用异步写的解决方案,为了应对异步场景数据丢失问题,引入了重新日志,可以通过重做日志应对故障场景。
通过检查点和对应重做日志的lsn,我们就可以知道应该将哪些数据写入磁盘。引入检查点可以缩短恢复时间(只需要恢复从检查点到宕机区间的数据)。
2.整体架构
\
- redo log buffer :内存中的数据,大小可以通过innodb_log_buffer_size控制
- group:通过多个组来提高存储引擎可用性。也就是如果组1损坏,组2依旧可以提供服务。所以每个组存储的数据是一样的。
- redolog file:每个组存放多个redolog文件,可以通过参数设置路径和大小。默认5mb。
Redo Log Buffer
内存中的数据结构,以块(block)的方式保存,每个块占用512字节。分配的是一块连续内存。
block有头有尾,头部由4部分组成,占12字节,尾部占用4个字节。
头部主要描述了该block的信息。
// LOG_BLOCK_HDR_NO在block的偏移起始位置。从0~4,该值代表block在block数组的位置。
#define LOG_BLOCK_HDR_NO 0
//LOG_BLOCK_HDR_DATA_LEN 同上 代表当前block占用的大小
#define LOG_BLOCK_HDR_DATA_LEN 4
//LOG_BLOCK_FIRST_REC_GROUP 同上 代表block第一个日志所在的偏移量
#define LOG_BLOCK_FIRST_REC_GROUP 6
#define LOG_BLOCK_CHECKPOINT_NO 8
//头部长度
#define LOG_BLOCK_HDR_SIZE 12
举个例子,假如我们要获取这个block当前被写入的字节数。
UNIV_INLINE
ulint
log_block_get_data_len(byte* log_block)
{
return(mach_read_from_2(log_block + LOG_BLOCK_HDR_DATA_LEN));
}
通过LOG_BLOCK_HDR_DATA_LEN以及这个log_block内存指针即可获取到。
具体重做日志数据存储在block的头尾之间。如果某个事务日志大小太大,一个block存不下,就需要两个甚至更多block去存储。
Redo Log File
主要是磁盘中的文件。innodb根据flush策略将内存中的数据刷新到文件中。
redo log file通过log group管理,每个log group由多个文件。log group只是逻辑存在。
log group的物理信息(id)保存在redo log file的header中。
header后面为ckeckpoint,保存了检查点的一些信息。
redo log file是循环使用的。
数据结构
核心数据结构是log_struct,该数据结构包括buffer内存,以及各种状态数据。因此log_struct控制着重做缓冲、重做日志文件、归档文件的写入,以及在线备份的操作。
log_struct中Redo Buf写入参数介绍:
buf_next_to_write:写入重做日志文件中的位置
buf_free:可以被写入的位置
max_buf_free:buf_free大于该值,会强制进行一次写入重做日志文件。
redo buf写入时机
mtr_commit方法中会触发buf的写入,其实就是mini-transaction提交的时候。将mini-transaction过程产生的日志数据写入到Redobuf。mini-transaction在innodb中非常重要,一个数据的插入会产生多个mini-transaction,直到整个事务提交。后续具体分析。
写入redo buf代码分析
mlog = &(mtr->log);
if (mtr->log_mode == MTR_LOG_ALL) {
block = mlog;
while (block != NULL) {
log_write_low(dyn_block_get_data(block),
dyn_block_get_used(block));
block = dyn_array_get_next_block(mlog, block);
}
} else {
ut_ad(mtr->log_mode == MTR_LOG_NONE);
/* Do nothing */
}
上面是一些主要逻辑代码,其实就是拿到mini-transaction的log数据(动态字节数组),然后遍历调用log_write_low写入。当然写入之前还会调用log_reserve_and_open,保证重做日志缓冲可以写入。不可写入的时候调用刷盘方法讲数据flush到磁盘。这个方法后面介绍。
log_write_low
void
log_write_low(byte* str,ulint str_len)
{
log_t* log = log_sys;
ulint len;
ulint data_len;
byte* log_block;
#ifdef UNIV_SYNC_DEBUG
ut_ad(mutex_own(&(log->mutex)));
#endif /* UNIV_SYNC_DEBUG */
part_loop:
/* Calculate a part length */
data_len = (log->buf_free % OS_FILE_LOG_BLOCK_SIZE) + str_len;
if (data_len <= OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE) {
/* The string fits within the current log block */
len = str_len;
} else {
data_len = OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE;
len = OS_FILE_LOG_BLOCK_SIZE
- (log->buf_free % OS_FILE_LOG_BLOCK_SIZE)
- LOG_BLOCK_TRL_SIZE;
}
ut_memcpy(log->buf + log->buf_free, str, len);
str_len -= len;
str = str + len;
log_block = ut_align_down(log->buf + log->buf_free,
OS_FILE_LOG_BLOCK_SIZE);
log_block_set_data_len(log_block, data_len);
if (data_len == OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE) {
/* This block became full */
log_block_set_data_len(log_block, OS_FILE_LOG_BLOCK_SIZE);
log_block_set_checkpoint_no(log_block,
log_sys->next_checkpoint_no);
len += LOG_BLOCK_HDR_SIZE + LOG_BLOCK_TRL_SIZE;
log->lsn = ut_dulint_add(log->lsn, len);
/* Initialize the next block header */
log_block_init(log_block + OS_FILE_LOG_BLOCK_SIZE, log->lsn);
} else {
log->lsn = ut_dulint_add(log->lsn, len);
}
log->buf_free += len;
ut_ad(log->buf_free <= log->buf_size);
if (str_len > 0) {
goto part_loop;
}
srv_log_write_requests++;
}
1.根据数据长度进行计算
len:对于当前数据(str),如果str长度比当前block剩余空间大,len赋值为block剩余长度,否则为str的长度。因此如果str一个block写不下,会写入到下一个block。
2.将数据copy到对应的block
3.更新len和str_len
4.判断block是否full。如果full则分配一个新的block。更新log的lsn,更新buff_free
5.如果str没有完全写入重复执行上面流程。
重做日志刷盘
主要逻辑在log_write_up_to中。
如果待写入的lsn已经被刷新到磁盘则直接返回。
没抢到锁、或者正在flush、或正在写入则挂起等待,因为flush操作会释放锁(所以flush和此逻辑可能并行执行)。
start_offset = log_sys->buf_next_to_write;
end_offset = log_sys->buf_free;
area_start = ut_calc_align_down(start_offset, OS_FILE_LOG_BLOCK_SIZE);
area_end = ut_calc_align(end_offset, OS_FILE_LOG_BLOCK_SIZE);
ut_ad(area_end - area_start > 0);
log_sys->write_lsn = log_sys->lsn;
if (flush_to_disk) {
log_sys->current_flush_lsn = log_sys->lsn;
}
log_sys->one_flushed = FALSE;
log_block_set_flush_bit(log_sys->buf + area_start, TRUE);
log_block_set_checkpoint_no(
log_sys->buf + area_end - OS_FILE_LOG_BLOCK_SIZE,
log_sys->next_checkpoint_no);
/* Copy the last, incompletely written, log block a log block length
up, so that when the flush operation writes from the log buffer, the
segment to write will not be changed by writers to the log */
ut_memcpy(log_sys->buf + area_end,
log_sys->buf + area_end - OS_FILE_LOG_BLOCK_SIZE,
OS_FILE_LOG_BLOCK_SIZE);
log_sys->buf_free += OS_FILE_LOG_BLOCK_SIZE;
log_sys->write_end_offset = log_sys->buf_free;
group = UT_LIST_GET_FIRST(log_sys->log_groups);
/* Do the write to the log files */
while (group) {
log_group_write_buf(group,
log_sys->buf + area_start,
area_end - area_start,
ut_dulint_align_down(log_sys->written_to_all_lsn,
OS_FILE_LOG_BLOCK_SIZE),
start_offset - area_start);
log_group_set_fields(group, log_sys->write_lsn);
group = UT_LIST_GET_NEXT(log_groups, group);
}
mutex_exit(&(log_sys->mutex));
上面的代码为执行写入逻辑
1.获取待写入的startoffset和endoffset
2.更新log_sys对应的数据(current_flush_lsn,write_lsn)
3.设置讲检查点编号写入block中
4.将buf最后512字节数据往后拷贝一份,避免在flush的时候,其他线程对内存的的修改,因为为了提高并发,flush在释放锁之后执行。
ut_memcpy(log_sys->buf + area_end,log_sys->buf + area_end - OS_FILE_LOG_BLOCK_SIZE,OS_FILE_LOG_BLOCK_SIZE);
5.获取到group调用log_group_write_buf将数据写入redolog file。这个方法主要就是构成redo log文件的格式(上面提到的header等),然后将数据写入文件缓冲区(os缓冲)
6.释放锁,如果需要flush到disk则调用fil_flush方法将刚才写入的数据flush到磁盘。
总结
重做日志的写入逻辑相对简单,对于ckeckpoint本文并没有详细介绍,下一篇重做日志恢复的时候回对checkpoint进行介绍。
转载自:https://juejin.cn/post/7007803137457127454